Skip to content

Commit 606d189

Browse files
committed
- The get_undeclared_template_variables method now analyzes the original template, regardless of whether it has been rendered.
- Added optional context parameter to return only variables not present in the provided context. - Added test tests/get_undeclared_variables.py: - Verifies behavior before rendering (all variables) - Verifies after rendering with incomplete context (only missing variables) - Verifies after rendering with complete context (empty set) - Verifies compatibility with custom Jinja2 environment - All tests use asserts and are ready for CI integration. Closes #585
1 parent 399761f commit 606d189

File tree

4 files changed

+176
-11
lines changed

4 files changed

+176
-11
lines changed

docs/index.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -406,9 +406,9 @@ In order to get the missing variables after rendering use ::
406406

407407
tpl=DocxTemplate('your_template.docx')
408408
tpl.render(context_dict)
409-
set_of_variables = tpl.get_undeclared_template_variables()
409+
set_of_variables = tpl.get_undeclared_template_variables(context=context_dict)
410410

411-
**IMPORTANT** : You may use the method before rendering to get a set of keys you need, e.g. to be prompted to a user or written in a file for manual processing.
411+
**IMPORTANT** : If `context` is not passed, you will get a set with all keys you need, e.g. to be prompted to a user or written in a file for manual processing.
412412

413413
Multiple rendering
414414
------------------

docxtpl/template.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -887,20 +887,34 @@ def save(self, filename: Union[IO[bytes], str, PathLike], *args, **kwargs) -> No
887887
self.is_saved = True
888888

889889
def get_undeclared_template_variables(
890-
self, jinja_env: Optional[Environment] = None
890+
self, jinja_env: Optional[Environment] = None, context: Optional[Dict[str, Any]] = None
891891
) -> Set[str]:
892-
self.init_docx(reload=False)
893-
xml = self.get_xml()
892+
# Create a temporary document to analyze the template without affecting the current state
893+
temp_doc = Document(self.template_file)
894+
895+
# Get XML from the temporary document
896+
xml = self.xml_to_string(temp_doc._element.body)
894897
xml = self.patch_xml(xml)
898+
899+
# Add headers and footers
895900
for uri in [self.HEADER_URI, self.FOOTER_URI]:
896-
for relKey, part in self.get_headers_footers(uri):
897-
_xml = self.get_part_xml(part)
898-
xml += self.patch_xml(_xml)
901+
for relKey, val in temp_doc._part.rels.items():
902+
if (val.reltype == uri) and (val.target_part.blob):
903+
_xml = self.xml_to_string(parse_xml(val.target_part.blob))
904+
xml += self.patch_xml(_xml)
905+
899906
if jinja_env:
900907
env = jinja_env
901908
else:
902909
env = Environment()
910+
903911
parse_content = env.parse(xml)
904-
return meta.find_undeclared_variables(parse_content)
905-
906-
undeclared_template_variables = property(get_undeclared_template_variables)
912+
all_variables = meta.find_undeclared_variables(parse_content)
913+
914+
# If context is provided, return only variables that are not in the context
915+
if context is not None:
916+
provided_variables = set(context.keys())
917+
return all_variables - provided_variables
918+
919+
# If no context provided, return all variables (original behavior)
920+
return all_variables

tests/get_undeclared_variables.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
"""
4+
Test for get_undeclared_template_variables method
5+
6+
This test demonstrates the correct behavior of get_undeclared_template_variables:
7+
1. Before rendering - finds all template variables
8+
2. After rendering with incomplete context - finds missing variables
9+
3. After rendering with complete context - returns empty set
10+
"""
11+
12+
from docxtpl import DocxTemplate
13+
14+
def test_before_render():
15+
"""Test that get_undeclared_template_variables finds all variables before rendering"""
16+
print("=== Test 1: Before render ===")
17+
tpl = DocxTemplate('templates/get_undeclared_variables.docx')
18+
undeclared = tpl.get_undeclared_template_variables()
19+
print(f"Variables found: {undeclared}")
20+
21+
# Should find all variables
22+
expected_vars = {
23+
'name', 'age', 'email', 'is_student', 'has_degree', 'degree_field',
24+
'skills', 'projects', 'company_name', 'page_number', 'generation_date', 'author'
25+
}
26+
27+
if undeclared == expected_vars:
28+
print("PASS: Found all expected variables before render")
29+
else:
30+
print(f"FAIL: Expected {expected_vars}, got {undeclared}")
31+
32+
return undeclared == expected_vars
33+
34+
def test_after_incomplete_render():
35+
"""Test that get_undeclared_template_variables finds missing variables after incomplete render"""
36+
print("\n=== Test 2: After incomplete render ===")
37+
tpl = DocxTemplate('templates/get_undeclared_variables.docx')
38+
39+
# Provide only some variables (missing several)
40+
context = {
41+
'name': 'John Doe',
42+
'age': 25,
43+
'email': 'john@example.com',
44+
'is_student': True,
45+
'skills': ['Python', 'Django'],
46+
'company_name': 'Test Corp',
47+
'author': 'Test Author'
48+
}
49+
50+
tpl.render(context)
51+
undeclared = tpl.get_undeclared_template_variables(context=context)
52+
print(f"Missing variables: {undeclared}")
53+
54+
# Should find missing variables
55+
expected_missing = {
56+
'has_degree', 'degree_field', 'projects', 'page_number', 'generation_date'
57+
}
58+
59+
if undeclared == expected_missing:
60+
print("PASS: Found missing variables after incomplete render")
61+
else:
62+
print(f"FAIL: Expected missing {expected_missing}, got {undeclared}")
63+
64+
return undeclared == expected_missing
65+
66+
def test_after_complete_render():
67+
"""Test that get_undeclared_template_variables returns empty set after complete render"""
68+
print("\n=== Test 3: After complete render ===")
69+
tpl = DocxTemplate('templates/get_undeclared_variables.docx')
70+
71+
# Provide all variables
72+
context = {
73+
'name': 'John Doe',
74+
'age': 25,
75+
'email': 'john@example.com',
76+
'is_student': True,
77+
'has_degree': True,
78+
'degree_field': 'Computer Science',
79+
'skills': ['Python', 'Django', 'JavaScript'],
80+
'projects': [
81+
{'name': 'Project A', 'year': 2023, 'description': 'A great project'},
82+
{'name': 'Project B', 'year': 2024, 'description': 'Another great project'}
83+
],
84+
'company_name': 'Test Corp',
85+
'page_number': 1,
86+
'generation_date': '2024-01-15',
87+
'author': 'Test Author'
88+
}
89+
90+
tpl.render(context)
91+
undeclared = tpl.get_undeclared_template_variables(context=context)
92+
print(f"Undeclared variables: {undeclared}")
93+
94+
# Should return empty set
95+
if undeclared == set():
96+
print("PASS: No undeclared variables after complete render")
97+
else:
98+
print(f"FAIL: Expected empty set, got {undeclared}")
99+
100+
return undeclared == set()
101+
102+
def test_with_custom_jinja_env():
103+
"""Test that get_undeclared_template_variables works with custom Jinja environment"""
104+
print("\n=== Test 4: With custom Jinja environment ===")
105+
from jinja2 import Environment
106+
107+
tpl = DocxTemplate('templates/get_undeclared_variables.docx')
108+
custom_env = Environment()
109+
110+
undeclared = tpl.get_undeclared_template_variables(jinja_env=custom_env)
111+
print(f"Variables found with custom env: {undeclared}")
112+
113+
# Should find all variables
114+
expected_vars = {
115+
'name', 'age', 'email', 'is_student', 'has_degree', 'degree_field',
116+
'skills', 'projects', 'company_name', 'page_number', 'generation_date', 'author'
117+
}
118+
119+
if undeclared == expected_vars:
120+
print("PASS: Custom Jinja environment works correctly")
121+
else:
122+
print(f"FAIL: Expected {expected_vars}, got {undeclared}")
123+
124+
return undeclared == expected_vars
125+
126+
if __name__ == "__main__":
127+
print("Testing get_undeclared_template_variables method...")
128+
print("=" * 50)
129+
130+
# Run all tests
131+
test1_passed = test_before_render()
132+
test2_passed = test_after_incomplete_render()
133+
test3_passed = test_after_complete_render()
134+
test4_passed = test_with_custom_jinja_env()
135+
136+
print("\n" + "=" * 50)
137+
print("SUMMARY:")
138+
print(f"Test 1 (Before render): {'PASS' if test1_passed else 'FAIL'}")
139+
print(f"Test 2 (After incomplete render): {'PASS' if test2_passed else 'FAIL'}")
140+
print(f"Test 3 (After complete render): {'PASS' if test3_passed else 'FAIL'}")
141+
print(f"Test 4 (Custom Jinja env): {'PASS' if test4_passed else 'FAIL'}")
142+
143+
all_passed = test1_passed and test2_passed and test3_passed and test4_passed
144+
145+
if all_passed:
146+
print("ALL TESTS PASSED!")
147+
else:
148+
print("SOME TESTS FAILED!")
149+
150+
print("\nNote: This test demonstrates that get_undeclared_template_variables")
151+
print("now correctly analyzes the original template, not the rendered document.")
17.9 KB
Binary file not shown.

0 commit comments

Comments
 (0)