From a45cc806d8960dd6c8fceddd8830e40e085417f2 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 28 Sep 2021 15:12:29 -0300 Subject: [PATCH 1/9] Added basic functionality to make the module executable --- docxtpl/__main__.py | 116 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 docxtpl/__main__.py diff --git a/docxtpl/__main__.py b/docxtpl/__main__.py new file mode 100644 index 0000000..796f2d7 --- /dev/null +++ b/docxtpl/__main__.py @@ -0,0 +1,116 @@ +from .template import DocxTemplate,TemplateError +import argparse, json +from pathlib import Path +from argparse import ArgumentError + + +def make_arg_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + usage='docxtpl template_path json_path output_filename', + description='Make docx file from existing template docx and json data.', + add_help=True) + parser.add_argument('Template', + metavar='template_path', + type=str, + help='The path to the template docx file.') + parser.add_argument('Json', + metavar='json_path', + type=str, + help='The path to the json file with the data.') + parser.add_argument('Output', + metavar='output_filename', + type=str, + help='The filename to save the generated docx.') + return parser + + +def get_args(parser: argparse.ArgumentParser) -> dict: + try: + parsed_args = vars(parser.parse_args()) + return parsed_args + # There is a bug that prevents argparser from catching ArgumentError + # manually. For more info: https://bugs.python.org/issue41255 + # I know bare exceptions are wrong, could not find another way to catch + # wrong arguments. + except: + raise RuntimeError(f'Correct usage is:\n{parser.usage}') + + +def is_argument_valid(arg_name: str, arg_value: str) -> bool: + if arg_name == 'Template': + return Path(arg_value).is_file() and arg_value.endswith('.docx') + elif arg_name == 'Json': + return Path(arg_value).is_file() and arg_value.endswith('.json') + elif arg_name == 'Output': + return arg_value.endswith('.docx') and check_exists_ask_overwrite( + arg_value) + + +def check_exists_ask_overwrite(arg_value) -> bool: + if Path(arg_value).exists(): + try: + if input(f'File {arg_value} already exists, would you like to overwrite the existing file? (y/n)').lower() == 'y': + return True + else: + raise FileExistsError + except FileExistsError as e: + raise RuntimeError(f'File {arg_value} already exists, please choose a different name.') from e + else: + return True + + +def validate_all_args(parsed_args) -> None: + try: + for arg_name, arg_value in parsed_args.items(): + if not is_argument_valid(arg_name, arg_value): + raise AttributeError + except ArgumentError as e: + raise RuntimeError( + f'The specified {arg_name} "{arg_value}" is not valid.') + + +def get_json_data(json_path: Path) -> dict: + with open(json_path) as file: + try: + json_data = json.load(file) + return json_data + except json.JSONDecodeError as e: + print( + f'There was an error on line {e.lineno}, column {e.colno} while trying to parse file {json_path}') + raise RuntimeError('Failed to get json data.') + + +def make_docxtemplate(template_path: Path) -> DocxTemplate: + try: + return DocxTemplate(template_path) + except TemplateError as e: + raise RuntimeError('Could not create docx template.') from e + + +def save_file(doc: DocxTemplate, output_path: Path) -> None: + try: + doc.save(output_path) + print(f'Document successfully generated and saved at {output_path}') + except PermissionError as e: + print(f'{e.strerror}. Could not save file {e.filename}.') + raise RuntimeError('Failed to save file.') from e + + +def main() -> None: + parser = make_arg_parser() + try: + parsed_args = get_args(parser) + validate_all_args(parsed_args) + json_data = get_json_data(Path(parsed_args['Json']).resolve()) + doc = make_docxtemplate(Path(parsed_args['Template']).resolve()) + doc.render(json_data) + save_file(doc, Path(parsed_args['Output']).resolve()) + except RuntimeError as e: + print(e) + return + finally: + print('Exiting program!') + + +if __name__ == '__main__': + main() From 8004fcf6bca9819102374bbf8e19f8656c8a9d99 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 28 Sep 2021 15:25:09 -0300 Subject: [PATCH 2/9] Added basic functionality to make the module executable --- docxtpl/__main__.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/docxtpl/__main__.py b/docxtpl/__main__.py index 796f2d7..181841e 100644 --- a/docxtpl/__main__.py +++ b/docxtpl/__main__.py @@ -7,8 +7,7 @@ from argparse import ArgumentError def make_arg_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( usage='docxtpl template_path json_path output_filename', - description='Make docx file from existing template docx and json data.', - add_help=True) + description='Make docx file from existing template docx and json data.') parser.add_argument('Template', metavar='template_path', type=str, @@ -28,11 +27,8 @@ def get_args(parser: argparse.ArgumentParser) -> dict: try: parsed_args = vars(parser.parse_args()) return parsed_args - # There is a bug that prevents argparser from catching ArgumentError - # manually. For more info: https://bugs.python.org/issue41255 - # I know bare exceptions are wrong, could not find another way to catch - # wrong arguments. - except: + # Argument errors cause SystemExit error, not ArgumentError + except SystemExit: raise RuntimeError(f'Correct usage is:\n{parser.usage}') @@ -63,7 +59,7 @@ def validate_all_args(parsed_args) -> None: try: for arg_name, arg_value in parsed_args.items(): if not is_argument_valid(arg_name, arg_value): - raise AttributeError + raise ArgumentError except ArgumentError as e: raise RuntimeError( f'The specified {arg_name} "{arg_value}" is not valid.') From a78462ee0f4b9bb0a9403cf671661ca0fd5558c6 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 29 Sep 2021 09:46:16 -0300 Subject: [PATCH 3/9] Reformat, added comments and small fixes in error catching --- docxtpl/__main__.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/docxtpl/__main__.py b/docxtpl/__main__.py index 181841e..39980e7 100644 --- a/docxtpl/__main__.py +++ b/docxtpl/__main__.py @@ -1,10 +1,11 @@ -from .template import DocxTemplate,TemplateError import argparse, json from pathlib import Path -from argparse import ArgumentError + +from .template import DocxTemplate, TemplateError def make_arg_parser() -> argparse.ArgumentParser: + # TODO add flag for overwrite without asking if filename already exists parser = argparse.ArgumentParser( usage='docxtpl template_path json_path output_filename', description='Make docx file from existing template docx and json data.') @@ -27,12 +28,17 @@ def get_args(parser: argparse.ArgumentParser) -> dict: try: parsed_args = vars(parser.parse_args()) return parsed_args - # Argument errors cause SystemExit error, not ArgumentError - except SystemExit: - raise RuntimeError(f'Correct usage is:\n{parser.usage}') + # Argument errors raise a SystemExit with code 2. Normal usage of the + # --help or -h flag raises a SystemExit with code 0. + except SystemExit as e: + if e.code == 0: + raise RuntimeError() + else: + raise RuntimeError(f'Correct usage is:\n{parser.usage}') def is_argument_valid(arg_name: str, arg_value: str) -> bool: + # Basic checks for the three arguments if arg_name == 'Template': return Path(arg_value).is_file() and arg_value.endswith('.docx') elif arg_name == 'Json': @@ -42,7 +48,10 @@ def is_argument_valid(arg_name: str, arg_value: str) -> bool: arg_value) -def check_exists_ask_overwrite(arg_value) -> bool: +def check_exists_ask_overwrite(arg_value:str) -> bool: + # If output file does not exist, returns True, else asks for overwrite + # confirmation. If overwrite confirmed returns True, else raises + # FileExistsError if Path(arg_value).exists(): try: if input(f'File {arg_value} already exists, would you like to overwrite the existing file? (y/n)').lower() == 'y': @@ -55,12 +64,13 @@ def check_exists_ask_overwrite(arg_value) -> bool: return True -def validate_all_args(parsed_args) -> None: +def validate_all_args(parsed_args:dict) -> None: + # Raises ArgumentError if any of the arguments is not validated try: for arg_name, arg_value in parsed_args.items(): if not is_argument_valid(arg_name, arg_value): - raise ArgumentError - except ArgumentError as e: + raise argparse.ArgumentError + except argparse.ArgumentError as e: raise RuntimeError( f'The specified {arg_name} "{arg_value}" is not valid.') @@ -94,6 +104,9 @@ def save_file(doc: DocxTemplate, output_path: Path) -> None: def main() -> None: parser = make_arg_parser() + # Everything is in a try-except block that cacthes a RuntimeError that is + # raised if any of the individual functions called cause an error + # themselves, terminating the main function. try: parsed_args = get_args(parser) validate_all_args(parsed_args) From b6f9c992f53e2b4b5751cb217a341367988fd896 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 29 Sep 2021 10:35:26 -0300 Subject: [PATCH 4/9] Added function for rendering docx instead of directly calling method on DocxTemplate instance. Added exception chaining to a few functions there were missing it. --- docxtpl/__main__.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/docxtpl/__main__.py b/docxtpl/__main__.py index 39980e7..b2f0d0d 100644 --- a/docxtpl/__main__.py +++ b/docxtpl/__main__.py @@ -32,9 +32,9 @@ def get_args(parser: argparse.ArgumentParser) -> dict: # --help or -h flag raises a SystemExit with code 0. except SystemExit as e: if e.code == 0: - raise RuntimeError() + raise RuntimeError() from e else: - raise RuntimeError(f'Correct usage is:\n{parser.usage}') + raise RuntimeError(f'Correct usage is:\n{parser.usage}') from e def is_argument_valid(arg_name: str, arg_value: str) -> bool: @@ -72,7 +72,7 @@ def validate_all_args(parsed_args:dict) -> None: raise argparse.ArgumentError except argparse.ArgumentError as e: raise RuntimeError( - f'The specified {arg_name} "{arg_value}" is not valid.') + f'The specified {arg_name} "{arg_value}" is not valid.') from e def get_json_data(json_path: Path) -> dict: @@ -83,7 +83,7 @@ def get_json_data(json_path: Path) -> dict: except json.JSONDecodeError as e: print( f'There was an error on line {e.lineno}, column {e.colno} while trying to parse file {json_path}') - raise RuntimeError('Failed to get json data.') + raise RuntimeError('Failed to get json data.') from e def make_docxtemplate(template_path: Path) -> DocxTemplate: @@ -93,6 +93,14 @@ def make_docxtemplate(template_path: Path) -> DocxTemplate: raise RuntimeError('Could not create docx template.') from e +def render_docx(doc:DocxTemplate, json_data) -> DocxTemplate: + try: + doc.render(json_data) + return doc + except TemplateError as e: + raise RuntimeError(f'An error ocurred while trying to render the docx') from e + + def save_file(doc: DocxTemplate, output_path: Path) -> None: try: doc.save(output_path) @@ -112,7 +120,7 @@ def main() -> None: validate_all_args(parsed_args) json_data = get_json_data(Path(parsed_args['Json']).resolve()) doc = make_docxtemplate(Path(parsed_args['Template']).resolve()) - doc.render(json_data) + doc = render_docx(doc,json_data) save_file(doc, Path(parsed_args['Output']).resolve()) except RuntimeError as e: print(e) From 5f3fe031084ea756ffc26662cc15abfbcfe79f0f Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 29 Sep 2021 11:04:42 -0300 Subject: [PATCH 5/9] Small change in error catching in validate function --- docxtpl/__main__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docxtpl/__main__.py b/docxtpl/__main__.py index b2f0d0d..6c03e06 100644 --- a/docxtpl/__main__.py +++ b/docxtpl/__main__.py @@ -65,12 +65,12 @@ def check_exists_ask_overwrite(arg_value:str) -> bool: def validate_all_args(parsed_args:dict) -> None: - # Raises ArgumentError if any of the arguments is not validated + # Raises AssertionError if any of the arguments is not validated try: for arg_name, arg_value in parsed_args.items(): if not is_argument_valid(arg_name, arg_value): - raise argparse.ArgumentError - except argparse.ArgumentError as e: + raise AssertionError + except AssertionError as e: raise RuntimeError( f'The specified {arg_name} "{arg_value}" is not valid.') from e From 2dee2ddd1c86134dbcccff82bff81b10a29e1524 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 29 Sep 2021 13:04:47 -0300 Subject: [PATCH 6/9] Small change in error message for main() --- docxtpl/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docxtpl/__main__.py b/docxtpl/__main__.py index 6c03e06..6b6ea07 100644 --- a/docxtpl/__main__.py +++ b/docxtpl/__main__.py @@ -123,7 +123,7 @@ def main() -> None: doc = render_docx(doc,json_data) save_file(doc, Path(parsed_args['Output']).resolve()) except RuntimeError as e: - print(e) + print('Error: '+e.__str__()) return finally: print('Exiting program!') From 7b2d7e1946c6eff50fbc2c6f414358f604129979 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 30 Sep 2021 09:39:40 -0300 Subject: [PATCH 7/9] Added argument for overwrite without confirmation and constants for easier referencing of arguments --- docxtpl/__main__.py | 57 +++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/docxtpl/__main__.py b/docxtpl/__main__.py index 6b6ea07..2cf5427 100644 --- a/docxtpl/__main__.py +++ b/docxtpl/__main__.py @@ -3,24 +3,28 @@ from pathlib import Path from .template import DocxTemplate, TemplateError +TEMPLATE_ARG = 'template_path' +JSON_ARG = 'json_path' +OUTPUT_ARG = 'output_filename' +OVERWRITE_ARG = 'overwrite' + def make_arg_parser() -> argparse.ArgumentParser: - # TODO add flag for overwrite without asking if filename already exists parser = argparse.ArgumentParser( - usage='docxtpl template_path json_path output_filename', + usage=f'docxtpl [-h] [-o] {TEMPLATE_ARG} {JSON_ARG} {OUTPUT_ARG}', description='Make docx file from existing template docx and json data.') - parser.add_argument('Template', - metavar='template_path', + parser.add_argument(TEMPLATE_ARG, type=str, help='The path to the template docx file.') - parser.add_argument('Json', - metavar='json_path', + parser.add_argument(JSON_ARG, type=str, help='The path to the json file with the data.') - parser.add_argument('Output', - metavar='output_filename', + parser.add_argument(OUTPUT_ARG, type=str, help='The filename to save the generated docx.') + parser.add_argument('-' + OVERWRITE_ARG[0], '--' + OVERWRITE_ARG, + action='store_true', + help='If output file already exists, overwrites without asking for confirmation') return parser @@ -32,27 +36,29 @@ def get_args(parser: argparse.ArgumentParser) -> dict: # --help or -h flag raises a SystemExit with code 0. except SystemExit as e: if e.code == 0: - raise RuntimeError() from e + raise SystemExit from e else: raise RuntimeError(f'Correct usage is:\n{parser.usage}') from e -def is_argument_valid(arg_name: str, arg_value: str) -> bool: - # Basic checks for the three arguments - if arg_name == 'Template': +def is_argument_valid(arg_name: str, arg_value: str,overwrite: bool) -> bool: + # Basic checks for the arguments + if arg_name == TEMPLATE_ARG: return Path(arg_value).is_file() and arg_value.endswith('.docx') - elif arg_name == 'Json': + elif arg_name == JSON_ARG: return Path(arg_value).is_file() and arg_value.endswith('.json') - elif arg_name == 'Output': + elif arg_name == OUTPUT_ARG: return arg_value.endswith('.docx') and check_exists_ask_overwrite( - arg_value) + arg_value, overwrite) + elif arg_name == OVERWRITE_ARG: + return arg_value in [True, False] -def check_exists_ask_overwrite(arg_value:str) -> bool: - # If output file does not exist, returns True, else asks for overwrite - # confirmation. If overwrite confirmed returns True, else raises - # FileExistsError - if Path(arg_value).exists(): +def check_exists_ask_overwrite(arg_value:str, overwrite: bool) -> bool: + # If output file does not exist or command was run with overwrite option, + # returns True, else asks for overwrite confirmation. If overwrite is + # confirmed returns True, else raises FileExistsError. + if Path(arg_value).exists() and not overwrite: try: if input(f'File {arg_value} already exists, would you like to overwrite the existing file? (y/n)').lower() == 'y': return True @@ -65,10 +71,11 @@ def check_exists_ask_overwrite(arg_value:str) -> bool: def validate_all_args(parsed_args:dict) -> None: + 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 not is_argument_valid(arg_name, arg_value): + if not is_argument_valid(arg_name, arg_value,overwrite): raise AssertionError except AssertionError as e: raise RuntimeError( @@ -93,7 +100,7 @@ def make_docxtemplate(template_path: Path) -> DocxTemplate: raise RuntimeError('Could not create docx template.') from e -def render_docx(doc:DocxTemplate, json_data) -> DocxTemplate: +def render_docx(doc:DocxTemplate, json_data: dict) -> DocxTemplate: try: doc.render(json_data) return doc @@ -118,10 +125,10 @@ def main() -> None: try: parsed_args = get_args(parser) validate_all_args(parsed_args) - json_data = get_json_data(Path(parsed_args['Json']).resolve()) - doc = make_docxtemplate(Path(parsed_args['Template']).resolve()) + json_data = get_json_data(Path(parsed_args[JSON_ARG]).resolve()) + doc = make_docxtemplate(Path(parsed_args[TEMPLATE_ARG]).resolve()) doc = render_docx(doc,json_data) - save_file(doc, Path(parsed_args['Output']).resolve()) + save_file(doc, Path(parsed_args[OUTPUT_ARG]).resolve()) except RuntimeError as e: print('Error: '+e.__str__()) return From e4737ddfbb8823fb837d477d44731a3107b3f890 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 30 Sep 2021 09:41:58 -0300 Subject: [PATCH 8/9] Added test for main function, template and json for running test --- tests/main.py | 21 ++++++++++++++++++++ tests/templates/json_for_main_test.json | 8 ++++++++ tests/templates/template_for_main_test.docx | Bin 0 -> 14418 bytes 3 files changed, 29 insertions(+) create mode 100644 tests/main.py create mode 100644 tests/templates/json_for_main_test.json create mode 100644 tests/templates/template_for_main_test.docx diff --git a/tests/main.py b/tests/main.py new file mode 100644 index 0000000..adae5ee --- /dev/null +++ b/tests/main.py @@ -0,0 +1,21 @@ +from docxtpl.__main__ import main +import sys +from pathlib import Path + +TEMPLATE_PATH = 'templates/template_for_main_test.docx' +JSON_PATH = 'templates/json_for_main_test.json' +OUTPUT_FILENAME = 'output/output_for_main_test.docx' +OVERWRITE = '-o' + + +# Simulate command line arguments, running with overwrite flag so test can be +# run repeatedly without need for confirmation: +sys.argv[1:] = [TEMPLATE_PATH, JSON_PATH, OUTPUT_FILENAME, OVERWRITE] + +main() + +output_path = Path(OUTPUT_FILENAME) + +if output_path.exists(): + print(f'File {output_path.resolve()} exists.') + diff --git a/tests/templates/json_for_main_test.json b/tests/templates/json_for_main_test.json new file mode 100644 index 0000000..535e175 --- /dev/null +++ b/tests/templates/json_for_main_test.json @@ -0,0 +1,8 @@ +{"json_dict_var" : {"json_dict_var":"successfully inserted"}, +"json_array_var": ["json","array","var","successfully", "inserted"], +"json_string_var":"json_string_var successfully inserted", +"json_int_var":123, +"json_float_var":1.234, +"json_true_var":true, +"json_false_var":false, +"json_none_var":null} \ No newline at end of file diff --git a/tests/templates/template_for_main_test.docx b/tests/templates/template_for_main_test.docx new file mode 100644 index 0000000000000000000000000000000000000000..88b6250069af7dc281689630de76acba8fa79f06 GIT binary patch literal 14418 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 literal 0 HcmV?d00001 From 1f3ef9b2368e59f83341f3089d44106b213d466d Mon Sep 17 00:00:00 2001 From: Eric Lapouyade Date: Thu, 30 Sep 2021 17:08:05 +0200 Subject: [PATCH 9/9] Add quiet option, update doc, modify test --- CHANGES.rst | 6 +++++ docs/index.rst | 24 ++++++++++++++++++ docxtpl/__init__.py | 2 +- docxtpl/__main__.py | 21 ++++++++++----- tests/main.py | 21 --------------- tests/module_execute.py | 22 ++++++++++++++++ ...for_main_test.json => module_execute.json} | 0 ...main_test.docx => module_execute_tpl.docx} | Bin 8 files changed, 67 insertions(+), 29 deletions(-) delete mode 100644 tests/main.py create mode 100644 tests/module_execute.py rename tests/templates/{json_for_main_test.json => module_execute.json} (100%) rename tests/templates/{template_for_main_test.docx => module_execute_tpl.docx} (100%) diff --git a/CHANGES.rst b/CHANGES.rst index 8e69e64..513eddb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +0.14.0 (2021-09-30) +------------------- +- One can now use python -m docxtpl on command line + to generate a docx from a template and a json file as a context + Thanks to Lcrs123@github + 0.12.0 (2021-08-15) ------------------- - Code has be split into many files for better readability diff --git a/docs/index.rst b/docs/index.rst index 0c328c3..900c460 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -387,6 +387,30 @@ Then in your template, you will be able to use:: Euros price : {{ price_dollars|multiply_by(0.88) }} + +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 + + 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. + + 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 + + +See tests/module_execute.py for an example. + Examples -------- diff --git a/docxtpl/__init__.py b/docxtpl/__init__.py index 280de22..563b398 100644 --- a/docxtpl/__init__.py +++ b/docxtpl/__init__.py @@ -4,7 +4,7 @@ Created : 2015-03-12 @author: Eric Lapouyade """ -__version__ = '0.12.0' +__version__ = '0.14.0' # flake8: noqa from .inline_image import InlineImage diff --git a/docxtpl/__main__.py b/docxtpl/__main__.py index 2cf5427..0bc685c 100644 --- a/docxtpl/__main__.py +++ b/docxtpl/__main__.py @@ -7,11 +7,12 @@ TEMPLATE_ARG = 'template_path' JSON_ARG = 'json_path' OUTPUT_ARG = 'output_filename' OVERWRITE_ARG = 'overwrite' +QUIET_ARG = 'quiet' def make_arg_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( - usage=f'docxtpl [-h] [-o] {TEMPLATE_ARG} {JSON_ARG} {OUTPUT_ARG}', + usage=f'python -m docxtpl [-h] [-o] [-q] {TEMPLATE_ARG} {JSON_ARG} {OUTPUT_ARG}', description='Make docx file from existing template docx and json data.') parser.add_argument(TEMPLATE_ARG, type=str, @@ -25,6 +26,9 @@ def make_arg_parser() -> argparse.ArgumentParser: parser.add_argument('-' + OVERWRITE_ARG[0], '--' + OVERWRITE_ARG, action='store_true', help='If output file already exists, overwrites without asking for confirmation') + parser.add_argument('-' + QUIET_ARG[0], '--' + QUIET_ARG, + action='store_true', + help='Do not display unnecessary messages') return parser @@ -50,7 +54,7 @@ def is_argument_valid(arg_name: str, arg_value: str,overwrite: bool) -> bool: elif arg_name == OUTPUT_ARG: return arg_value.endswith('.docx') and check_exists_ask_overwrite( arg_value, overwrite) - elif arg_name == OVERWRITE_ARG: + elif arg_name in [OVERWRITE_ARG, QUIET_ARG]: return arg_value in [True, False] @@ -108,10 +112,12 @@ def render_docx(doc:DocxTemplate, json_data: dict) -> DocxTemplate: raise RuntimeError(f'An error ocurred while trying to render the docx') from e -def save_file(doc: DocxTemplate, output_path: Path) -> None: +def save_file(doc: DocxTemplate, parsed_args: dict) -> None: try: + output_path = parsed_args[OUTPUT_ARG] doc.save(output_path) - print(f'Document successfully generated and saved at {output_path}') + if not parsed_args[QUIET_ARG]: + print(f'Document successfully generated and saved at {output_path}') except PermissionError as e: print(f'{e.strerror}. Could not save file {e.filename}.') raise RuntimeError('Failed to save file.') from e @@ -122,18 +128,19 @@ def main() -> None: # Everything is in a try-except block that cacthes a RuntimeError that is # raised if any of the individual functions called cause an error # themselves, terminating the main function. + parsed_args = get_args(parser) try: - parsed_args = get_args(parser) validate_all_args(parsed_args) json_data = get_json_data(Path(parsed_args[JSON_ARG]).resolve()) doc = make_docxtemplate(Path(parsed_args[TEMPLATE_ARG]).resolve()) doc = render_docx(doc,json_data) - save_file(doc, Path(parsed_args[OUTPUT_ARG]).resolve()) + save_file(doc, parsed_args) except RuntimeError as e: print('Error: '+e.__str__()) return finally: - print('Exiting program!') + if not parsed_args[QUIET_ARG]: + print('Exiting program!') if __name__ == '__main__': diff --git a/tests/main.py b/tests/main.py deleted file mode 100644 index adae5ee..0000000 --- a/tests/main.py +++ /dev/null @@ -1,21 +0,0 @@ -from docxtpl.__main__ import main -import sys -from pathlib import Path - -TEMPLATE_PATH = 'templates/template_for_main_test.docx' -JSON_PATH = 'templates/json_for_main_test.json' -OUTPUT_FILENAME = 'output/output_for_main_test.docx' -OVERWRITE = '-o' - - -# Simulate command line arguments, running with overwrite flag so test can be -# run repeatedly without need for confirmation: -sys.argv[1:] = [TEMPLATE_PATH, JSON_PATH, OUTPUT_FILENAME, OVERWRITE] - -main() - -output_path = Path(OUTPUT_FILENAME) - -if output_path.exists(): - print(f'File {output_path.resolve()} exists.') - diff --git a/tests/module_execute.py b/tests/module_execute.py new file mode 100644 index 0000000..f33f611 --- /dev/null +++ b/tests/module_execute.py @@ -0,0 +1,22 @@ +import sys, os +from pathlib import Path + +TEMPLATE_PATH = 'templates/module_execute_tpl.docx' +JSON_PATH = 'templates/module_execute.json' +OUTPUT_FILENAME = 'output/module_execute.docx' +OVERWRITE = '-o' +QUIET = '-q' + + +output_path = Path(OUTPUT_FILENAME) +if output_path.exists(): + output_path.unlink() + +os.chdir(Path(__file__).parent) +cmd = f'python -m docxtpl {TEMPLATE_PATH} {JSON_PATH} {OUTPUT_FILENAME} {OVERWRITE} {QUIET}' +print(f'Executing "{cmd}" ...') +os.system(cmd) + +if output_path.exists(): + print(f' --> File {output_path.resolve()} has been generated.') + diff --git a/tests/templates/json_for_main_test.json b/tests/templates/module_execute.json similarity index 100% rename from tests/templates/json_for_main_test.json rename to tests/templates/module_execute.json diff --git a/tests/templates/template_for_main_test.docx b/tests/templates/module_execute_tpl.docx similarity index 100% rename from tests/templates/template_for_main_test.docx rename to tests/templates/module_execute_tpl.docx