diff --git a/docs/index.rst b/docs/index.rst index 292218d..c4bbec4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -406,9 +406,9 @@ In order to get the missing variables after rendering use :: tpl=DocxTemplate('your_template.docx') tpl.render(context_dict) - set_of_variables = tpl.get_undeclared_template_variables() + set_of_variables = tpl.get_undeclared_template_variables(context=context_dict) -**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. +**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. Multiple rendering ------------------ diff --git a/docxtpl/template.py b/docxtpl/template.py index 95b4a2d..2e12d53 100644 --- a/docxtpl/template.py +++ b/docxtpl/template.py @@ -887,20 +887,34 @@ def save(self, filename: Union[IO[bytes], str, PathLike], *args, **kwargs) -> No self.is_saved = True def get_undeclared_template_variables( - self, jinja_env: Optional[Environment] = None + self, jinja_env: Optional[Environment] = None, context: Optional[Dict[str, Any]] = None ) -> Set[str]: - self.init_docx(reload=False) - xml = self.get_xml() + # Create a temporary document to analyze the template without affecting the current state + temp_doc = Document(self.template_file) + + # Get XML from the temporary document + xml = self.xml_to_string(temp_doc._element.body) xml = self.patch_xml(xml) + + # Add headers and footers for uri in [self.HEADER_URI, self.FOOTER_URI]: - for relKey, part in self.get_headers_footers(uri): - _xml = self.get_part_xml(part) - xml += self.patch_xml(_xml) + for relKey, val in temp_doc._part.rels.items(): + if (val.reltype == uri) and (val.target_part.blob): + _xml = self.xml_to_string(parse_xml(val.target_part.blob)) + xml += self.patch_xml(_xml) + if jinja_env: env = jinja_env else: env = Environment() + parse_content = env.parse(xml) - return meta.find_undeclared_variables(parse_content) - - undeclared_template_variables = property(get_undeclared_template_variables) + all_variables = meta.find_undeclared_variables(parse_content) + + # If context is provided, return only variables that are not in the context + if context is not None: + provided_variables = set(context.keys()) + return all_variables - provided_variables + + # If no context provided, return all variables (original behavior) + return all_variables \ No newline at end of file diff --git a/tests/get_undeclared_variables.py b/tests/get_undeclared_variables.py new file mode 100644 index 0000000..9f165c2 --- /dev/null +++ b/tests/get_undeclared_variables.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Test for get_undeclared_template_variables method + +This test demonstrates the correct behavior of get_undeclared_template_variables: +1. Before rendering - finds all template variables +2. After rendering with incomplete context - finds missing variables +3. After rendering with complete context - returns empty set +""" + +from docxtpl import DocxTemplate + +def test_before_render(): + """Test that get_undeclared_template_variables finds all variables before rendering""" + print("=== Test 1: Before render ===") + tpl = DocxTemplate('templates/get_undeclared_variables.docx') + undeclared = tpl.get_undeclared_template_variables() + print(f"Variables found: {undeclared}") + + # Should find all variables + expected_vars = { + 'name', 'age', 'email', 'is_student', 'has_degree', 'degree_field', + 'skills', 'projects', 'company_name', 'page_number', 'generation_date', 'author' + } + + if undeclared == expected_vars: + print("PASS: Found all expected variables before render") + else: + print(f"FAIL: Expected {expected_vars}, got {undeclared}") + + return undeclared == expected_vars + +def test_after_incomplete_render(): + """Test that get_undeclared_template_variables finds missing variables after incomplete render""" + print("\n=== Test 2: After incomplete render ===") + tpl = DocxTemplate('templates/get_undeclared_variables.docx') + + # Provide only some variables (missing several) + context = { + 'name': 'John Doe', + 'age': 25, + 'email': 'john@example.com', + 'is_student': True, + 'skills': ['Python', 'Django'], + 'company_name': 'Test Corp', + 'author': 'Test Author' + } + + tpl.render(context) + undeclared = tpl.get_undeclared_template_variables(context=context) + print(f"Missing variables: {undeclared}") + + # Should find missing variables + expected_missing = { + 'has_degree', 'degree_field', 'projects', 'page_number', 'generation_date' + } + + if undeclared == expected_missing: + print("PASS: Found missing variables after incomplete render") + else: + print(f"FAIL: Expected missing {expected_missing}, got {undeclared}") + + return undeclared == expected_missing + +def test_after_complete_render(): + """Test that get_undeclared_template_variables returns empty set after complete render""" + print("\n=== Test 3: After complete render ===") + tpl = DocxTemplate('templates/get_undeclared_variables.docx') + + # Provide all variables + context = { + 'name': 'John Doe', + 'age': 25, + 'email': 'john@example.com', + 'is_student': True, + 'has_degree': True, + 'degree_field': 'Computer Science', + 'skills': ['Python', 'Django', 'JavaScript'], + 'projects': [ + {'name': 'Project A', 'year': 2023, 'description': 'A great project'}, + {'name': 'Project B', 'year': 2024, 'description': 'Another great project'} + ], + 'company_name': 'Test Corp', + 'page_number': 1, + 'generation_date': '2024-01-15', + 'author': 'Test Author' + } + + tpl.render(context) + undeclared = tpl.get_undeclared_template_variables(context=context) + print(f"Undeclared variables: {undeclared}") + + # Should return empty set + if undeclared == set(): + print("PASS: No undeclared variables after complete render") + else: + print(f"FAIL: Expected empty set, got {undeclared}") + + return undeclared == set() + +def test_with_custom_jinja_env(): + """Test that get_undeclared_template_variables works with custom Jinja environment""" + print("\n=== Test 4: With custom Jinja environment ===") + from jinja2 import Environment + + tpl = DocxTemplate('templates/get_undeclared_variables.docx') + custom_env = Environment() + + undeclared = tpl.get_undeclared_template_variables(jinja_env=custom_env) + print(f"Variables found with custom env: {undeclared}") + + # Should find all variables + expected_vars = { + 'name', 'age', 'email', 'is_student', 'has_degree', 'degree_field', + 'skills', 'projects', 'company_name', 'page_number', 'generation_date', 'author' + } + + if undeclared == expected_vars: + print("PASS: Custom Jinja environment works correctly") + else: + print(f"FAIL: Expected {expected_vars}, got {undeclared}") + + return undeclared == expected_vars + +if __name__ == "__main__": + print("Testing get_undeclared_template_variables method...") + print("=" * 50) + + # Run all tests + test1_passed = test_before_render() + test2_passed = test_after_incomplete_render() + test3_passed = test_after_complete_render() + test4_passed = test_with_custom_jinja_env() + + print("\n" + "=" * 50) + print("SUMMARY:") + print(f"Test 1 (Before render): {'PASS' if test1_passed else 'FAIL'}") + print(f"Test 2 (After incomplete render): {'PASS' if test2_passed else 'FAIL'}") + print(f"Test 3 (After complete render): {'PASS' if test3_passed else 'FAIL'}") + print(f"Test 4 (Custom Jinja env): {'PASS' if test4_passed else 'FAIL'}") + + all_passed = test1_passed and test2_passed and test3_passed and test4_passed + + if all_passed: + print("ALL TESTS PASSED!") + else: + print("SOME TESTS FAILED!") + + print("\nNote: This test demonstrates that get_undeclared_template_variables") + print("now correctly analyzes the original template, not the rendered document.") \ No newline at end of file diff --git a/tests/templates/get_undeclared_variables.docx b/tests/templates/get_undeclared_variables.docx new file mode 100644 index 0000000..e112a90 Binary files /dev/null and b/tests/templates/get_undeclared_variables.docx differ