From c1259ab66ec7e4ee6964a901b1486a730f1029eb Mon Sep 17 00:00:00 2001 From: Alessandro Ranellucci Date: Wed, 3 Jun 2026 20:52:18 +0200 Subject: [PATCH] Add new --validate option for the CLI mode --- docs/index.rst | 13 +++++-- docxtpl/__main__.py | 90 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 96 insertions(+), 7 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 46cf69f..427d101 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -471,21 +471,28 @@ Command-line execution One can use `docxtpl` module directly on command line to generate a docx from a template and a json file as a context:: - usage: python -m docxtpl [-h] [-o] [-q] template_path json_path output_filename + usage: python -m docxtpl [-h] [-o] [-q] [--validate] [--report FILE] template_path [json_path] [output_filename] Make docx file from existing template docx and json data. positional arguments: template_path The path to the template docx file. - json_path The path to the json file with the data. - output_filename The filename to save the generated docx. + json_path The path to the json file with the data (not required with --validate). + output_filename The filename to save the generated docx (not required with --validate). optional arguments: -h, --help show this help message and exit -o, --overwrite If output file already exists, overwrites without asking for confirmation -q, --quiet Do not display unnecessary messages + --validate Check if the template Jinja2 syntax is valid + --report FILE Write validation result as JSON to FILE (requires --validate) +To validate a template without generating a document:: + + python -m docxtpl template.docx --validate + +If the syntax is invalid, the error is printed to the console. With ``--report FILE``, a JSON report is written, for example ``{"valid": false, "error": "..."}`` or ``{"valid": true}``. See tests/module_execute.py for an example. diff --git a/docxtpl/__main__.py b/docxtpl/__main__.py index 59cf049..61f9a5d 100644 --- a/docxtpl/__main__.py +++ b/docxtpl/__main__.py @@ -1,6 +1,7 @@ import argparse import json import os +import sys from .template import DocxTemplate, TemplateError @@ -9,11 +10,13 @@ OUTPUT_ARG = "output_filename" OVERWRITE_ARG = "overwrite" QUIET_ARG = "quiet" +VALIDATE_ARG = "validate" +REPORT_ARG = "report" def make_arg_parser(): parser = argparse.ArgumentParser( - usage="python -m docxtpl [-h] [-o] [-q] {} {} {}".format( + usage="python -m docxtpl [-h] [-o] [-q] [--validate] [--report FILE] {} [{}] [{}]".format( TEMPLATE_ARG, JSON_ARG, OUTPUT_ARG ), description="Make docx file from existing template docx and json data.", @@ -22,10 +25,30 @@ def make_arg_parser(): TEMPLATE_ARG, type=str, help="The path to the template docx file." ) parser.add_argument( - JSON_ARG, type=str, help="The path to the json file with the data." + JSON_ARG, + type=str, + nargs="?", + default=None, + help="The path to the json file with the data.", ) parser.add_argument( - OUTPUT_ARG, type=str, help="The filename to save the generated docx." + OUTPUT_ARG, + type=str, + nargs="?", + default=None, + help="The filename to save the generated docx.", + ) + parser.add_argument( + "--" + VALIDATE_ARG, + action="store_true", + help="Check if the template Jinja2 syntax is valid.", + ) + parser.add_argument( + "--" + REPORT_ARG, + type=str, + metavar="FILE", + default=None, + help="Write validation result as JSON to FILE (requires --validate).", ) parser.add_argument( "-" + OVERWRITE_ARG[0], @@ -67,8 +90,10 @@ def is_argument_valid(arg_name, arg_value, overwrite): return arg_value.endswith(".docx") and check_exists_ask_overwrite( arg_value, overwrite ) - elif arg_name in [OVERWRITE_ARG, QUIET_ARG]: + elif arg_name in [OVERWRITE_ARG, QUIET_ARG, VALIDATE_ARG]: return arg_value in [True, False] + elif arg_name == REPORT_ARG: + return arg_value is None or isinstance(arg_value, str) def check_exists_ask_overwrite(arg_value, overwrite): @@ -94,10 +119,30 @@ def check_exists_ask_overwrite(arg_value, overwrite): def validate_all_args(parsed_args): + if parsed_args.get(REPORT_ARG) and not parsed_args.get(VALIDATE_ARG): + raise RuntimeError("--report requires --validate.") + + if parsed_args[VALIDATE_ARG]: + template_path = parsed_args[TEMPLATE_ARG] + if not is_argument_valid(TEMPLATE_ARG, template_path, False): + raise RuntimeError( + 'The specified {arg_name} "{arg_value}" is not valid.'.format( + arg_name=TEMPLATE_ARG, arg_value=template_path + ) + ) + return + + if not parsed_args[JSON_ARG] or not parsed_args[OUTPUT_ARG]: + raise RuntimeError( + "json_path and output_filename are required in render mode." + ) + overwrite = parsed_args[OVERWRITE_ARG] # Raises AssertionError if any of the arguments is not validated try: for arg_name, arg_value in parsed_args.items(): + if arg_name in (VALIDATE_ARG, REPORT_ARG): + continue if not is_argument_valid(arg_name, arg_value, overwrite): raise AssertionError except AssertionError: @@ -108,6 +153,41 @@ def validate_all_args(parsed_args): ) +def validate_template_syntax(template_path): + try: + doc = DocxTemplate(template_path) + doc.get_undeclared_template_variables() + return None + except TemplateError as e: + return str(e) + except Exception as e: + return str(e) + + +def write_validation_report(report_path, valid, error=None): + payload = {"valid": valid} + if error is not None: + payload["error"] = error + with open(report_path, "w") as f: + json.dump(payload, f) + + +def run_validation(parsed_args): + template_path = os.path.abspath(parsed_args[TEMPLATE_ARG]) + error = validate_template_syntax(template_path) + valid = error is None + + if not valid: + print(error) + elif not parsed_args[QUIET_ARG]: + print("Template syntax is valid.") + + if parsed_args.get(REPORT_ARG): + write_validation_report(parsed_args[REPORT_ARG], valid, error) + + return 0 if valid else 1 + + def get_json_data(json_path): with open(json_path) as file: try: @@ -159,6 +239,8 @@ def main(): parsed_args = get_args(parser) try: validate_all_args(parsed_args) + if parsed_args[VALIDATE_ARG]: + sys.exit(run_validation(parsed_args)) json_data = get_json_data(os.path.abspath(parsed_args[JSON_ARG])) doc = make_docxtemplate(os.path.abspath(parsed_args[TEMPLATE_ARG])) doc = render_docx(doc, json_data)