diff --git a/docxtpl/__init__.py b/docxtpl/__init__.py index 5ae5209..b2d6d3a 100644 --- a/docxtpl/__init__.py +++ b/docxtpl/__init__.py @@ -4,7 +4,7 @@ @author: Eric Lapouyade """ -__version__ = '0.17.0' +__version__ = "0.17.0" # flake8: noqa from .inline_image import InlineImage diff --git a/docxtpl/__main__.py b/docxtpl/__main__.py index 17b3b05..6295641 100644 --- a/docxtpl/__main__.py +++ b/docxtpl/__main__.py @@ -4,32 +4,41 @@ from .template import DocxTemplate, TemplateError -TEMPLATE_ARG = 'template_path' -JSON_ARG = 'json_path' -OUTPUT_ARG = 'output_filename' -OVERWRITE_ARG = 'overwrite' -QUIET_ARG = 'quiet' +TEMPLATE_ARG = "template_path" +JSON_ARG = "json_path" +OUTPUT_ARG = "output_filename" +OVERWRITE_ARG = "overwrite" +QUIET_ARG = "quiet" def make_arg_parser(): parser = argparse.ArgumentParser( - usage='python -m docxtpl [-h] [-o] [-q] {} {} {}'.format(TEMPLATE_ARG, JSON_ARG, OUTPUT_ARG), - description='Make docx file from existing template docx and json data.') - parser.add_argument(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.') - 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') - parser.add_argument('-' + QUIET_ARG[0], '--' + QUIET_ARG, - action='store_true', - help='Do not display unnecessary messages') + usage="python -m docxtpl [-h] [-o] [-q] {} {} {}".format( + TEMPLATE_ARG, JSON_ARG, OUTPUT_ARG + ), + description="Make docx file from existing template docx and json data.", + ) + parser.add_argument( + 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." + ) + 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", + ) + parser.add_argument( + "-" + QUIET_ARG[0], + "--" + QUIET_ARG, + action="store_true", + help="Do not display unnecessary messages", + ) return parser @@ -43,18 +52,21 @@ def get_args(parser): if e.code == 0: raise SystemExit else: - raise RuntimeError('Correct usage is:\n{parser.usage}'.format(parser=parser)) + raise RuntimeError( + "Correct usage is:\n{parser.usage}".format(parser=parser) + ) def is_argument_valid(arg_name, arg_value, overwrite): # Basic checks for the arguments if arg_name == TEMPLATE_ARG: - return os.path.isfile(arg_value) and arg_value.endswith('.docx') + return os.path.isfile(arg_value) and arg_value.endswith(".docx") elif arg_name == JSON_ARG: - return os.path.isfile(arg_value) and arg_value.endswith('.json') + return os.path.isfile(arg_value) and arg_value.endswith(".json") elif arg_name == OUTPUT_ARG: - return arg_value.endswith('.docx') and check_exists_ask_overwrite( - arg_value, overwrite) + return arg_value.endswith(".docx") and check_exists_ask_overwrite( + arg_value, overwrite + ) elif arg_name in [OVERWRITE_ARG, QUIET_ARG]: return arg_value in [True, False] @@ -65,13 +77,18 @@ def check_exists_ask_overwrite(arg_value, overwrite): # confirmed returns True, else raises OSError. if os.path.exists(arg_value) and not overwrite: try: - msg = 'File %s already exists, would you like to overwrite the existing file? (y/n)' % arg_value - if input(msg).lower() == 'y': + msg = ( + "File %s already exists, would you like to overwrite the existing file? (y/n)" + % arg_value + ) + if input(msg).lower() == "y": return True else: raise OSError except OSError: - raise RuntimeError('File %s already exists, please choose a different name.' % arg_value) + raise RuntimeError( + "File %s already exists, please choose a different name." % arg_value + ) else: return True @@ -87,7 +104,8 @@ def validate_all_args(parsed_args): raise RuntimeError( 'The specified {arg_name} "{arg_value}" is not valid.'.format( arg_name=arg_name, arg_value=arg_value - )) + ) + ) def get_json_data(json_path): @@ -97,17 +115,18 @@ def get_json_data(json_path): return json_data except json.JSONDecodeError as e: print( - 'There was an error on line {e.lineno}, column {e.colno} while trying to parse file {json_path}'.format( + "There was an error on line {e.lineno}, column {e.colno} while trying to parse file {json_path}".format( e=e, json_path=json_path - )) - raise RuntimeError('Failed to get json data.') + ) + ) + raise RuntimeError("Failed to get json data.") def make_docxtemplate(template_path): try: return DocxTemplate(template_path) except TemplateError: - raise RuntimeError('Could not create docx template.') + raise RuntimeError("Could not create docx template.") def render_docx(doc, json_data): @@ -115,7 +134,7 @@ def render_docx(doc, json_data): doc.render(json_data) return doc except TemplateError: - raise RuntimeError('An error ocurred while trying to render the docx') + raise RuntimeError("An error ocurred while trying to render the docx") def save_file(doc, parsed_args): @@ -123,10 +142,14 @@ def save_file(doc, parsed_args): output_path = parsed_args[OUTPUT_ARG] doc.save(output_path) if not parsed_args[QUIET_ARG]: - print('Document successfully generated and saved at {output_path}'.format(output_path=output_path)) + print( + "Document successfully generated and saved at {output_path}".format( + output_path=output_path + ) + ) except OSError as e: - print('{e.strerror}. Could not save file {e.filename}.'.format(e=e)) - raise RuntimeError('Failed to save file.') + print("{e.strerror}. Could not save file {e.filename}.".format(e=e)) + raise RuntimeError("Failed to save file.") def main(): @@ -142,12 +165,12 @@ def main(): doc = render_docx(doc, json_data) save_file(doc, parsed_args) except RuntimeError as e: - print('Error: '+e.__str__()) + print("Error: " + e.__str__()) return finally: if not parsed_args[QUIET_ARG]: - print('Exiting program!') + print("Exiting program!") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/docxtpl/inline_image.py b/docxtpl/inline_image.py index e96eec6..f860749 100644 --- a/docxtpl/inline_image.py +++ b/docxtpl/inline_image.py @@ -7,11 +7,13 @@ from docx.oxml import OxmlElement, parse_xml from docx.oxml.ns import qn + class InlineImage(object): """Class to generate an inline image This is much faster than using Subdoc class. """ + tpl = None image_descriptor = None width = None @@ -25,17 +27,21 @@ def __init__(self, tpl, image_descriptor, width=None, height=None, anchor=None): def _add_hyperlink(self, run, url, part): # Create a relationship for the hyperlink - r_id = part.relate_to(url, 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink', is_external=True) + r_id = part.relate_to( + url, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", + is_external=True, + ) # Find the and element - docPr = run.xpath('.//wp:docPr')[0] - cNvPr = run.xpath('.//pic:cNvPr')[0] + docPr = run.xpath(".//wp:docPr")[0] + cNvPr = run.xpath(".//pic:cNvPr")[0] # Create the element - hlinkClick1 = OxmlElement('a:hlinkClick') - hlinkClick1.set(qn('r:id'), r_id) - hlinkClick2 = OxmlElement('a:hlinkClick') - hlinkClick2.set(qn('r:id'), r_id) + hlinkClick1 = OxmlElement("a:hlinkClick") + hlinkClick1.set(qn("r:id"), r_id) + hlinkClick2 = OxmlElement("a:hlinkClick") + hlinkClick2.set(qn("r:id"), r_id) # Insert the element right after the element docPr.append(hlinkClick1) @@ -51,12 +57,16 @@ def _insert_image(self): ).xml if self.anchor: run = parse_xml(pic) - if run.xpath('.//a:blip'): - hyperlink = self._add_hyperlink(run, self.anchor, self.tpl.current_rendering_part) + if run.xpath(".//a:blip"): + hyperlink = self._add_hyperlink( + run, self.anchor, self.tpl.current_rendering_part + ) pic = hyperlink.xml - return '%s' \ - '' % pic + return ( + "%s" + '' % pic + ) def __unicode__(self): return self._insert_image() diff --git a/docxtpl/listing.py b/docxtpl/listing.py index 4a7f85c..afe637e 100644 --- a/docxtpl/listing.py +++ b/docxtpl/listing.py @@ -5,6 +5,7 @@ @author: Eric Lapouyade """ import six + try: from html import escape except ImportError: @@ -19,6 +20,7 @@ class Listing(object): use {{ mylisting }} in your template and context={ mylisting:Listing(the_listing_with_newlines) } """ + def __init__(self, text): # If not a string : cast to string (ex: int, dict etc...) if not isinstance(text, (six.text_type, six.binary_type)): diff --git a/docxtpl/richtext.py b/docxtpl/richtext.py index 02ab0b9..453ce2a 100644 --- a/docxtpl/richtext.py +++ b/docxtpl/richtext.py @@ -5,6 +5,7 @@ @author: Eric Lapouyade """ import six + try: from html import escape except ImportError: @@ -13,29 +14,33 @@ class RichText(object): - """ class to generate Rich Text when using templates variables + """class to generate Rich Text when using templates variables This is much faster than using Subdoc class, but this only for texts INSIDE an existing paragraph. """ + def __init__(self, text=None, **text_prop): - self.xml = '' + self.xml = "" if text: self.add(text, **text_prop) - def add(self, text, - style=None, - color=None, - highlight=None, - size=None, - subscript=None, - superscript=None, - bold=False, - italic=False, - underline=False, - strike=False, - font=None, - url_id=None): + def add( + self, + text, + style=None, + color=None, + highlight=None, + size=None, + subscript=None, + superscript=None, + bold=False, + italic=False, + underline=False, + strike=False, + font=None, + url_id=None, + ): # If a RichText is added if isinstance(text, RichText): @@ -46,55 +51,65 @@ def add(self, text, if not isinstance(text, (six.text_type, six.binary_type)): text = six.text_type(text) if not isinstance(text, six.text_type): - text = text.decode('utf-8', errors='ignore') + text = text.decode("utf-8", errors="ignore") text = escape(text) - prop = u'' + prop = "" if style: - prop += u'' % style + prop += '' % style if color: - if color[0] == '#': + if color[0] == "#": color = color[1:] - prop += u'' % color + prop += '' % color if highlight: - if highlight[0] == '#': + if highlight[0] == "#": highlight = highlight[1:] - prop += u'' % highlight + prop += '' % highlight if size: - prop += u'' % size - prop += u'' % size + prop += '' % size + prop += '' % size if subscript: - prop += u'' + prop += '' if superscript: - prop += u'' + prop += '' if bold: - prop += u'' + prop += "" if italic: - prop += u'' + prop += "" if underline: - if underline not in ['single', 'double', 'thick', 'dotted', 'dash', 'dotDash', 'dotDotDash', 'wave']: - underline = 'single' - prop += u'' % underline + if underline not in [ + "single", + "double", + "thick", + "dotted", + "dash", + "dotDash", + "dotDotDash", + "wave", + ]: + underline = "single" + prop += '' % underline if strike: - prop += u'' + prop += "" if font: - regional_font = u'' - if ':' in font: - region, font = font.split(':', 1) - regional_font = u' w:{region}="{font}"'.format(font=font, region=region) - prop += ( - u'' - .format(font=font, regional_font=regional_font) + regional_font = "" + if ":" in font: + region, font = font.split(":", 1) + regional_font = ' w:{region}="{font}"'.format(font=font, region=region) + prop += ''.format( + font=font, regional_font=regional_font ) - xml = u'' + xml = "" if prop: - xml += u'%s' % prop - xml += u'%s' % text + xml += "%s" % prop + xml += '%s' % text if url_id: - xml = (u'%s' - % (url_id, xml)) + xml = '%s' % ( + url_id, + xml, + ) self.xml += xml def __unicode__(self): diff --git a/docxtpl/subdoc.py b/docxtpl/subdoc.py index 84a1777..e8da093 100644 --- a/docxtpl/subdoc.py +++ b/docxtpl/subdoc.py @@ -18,8 +18,8 @@ class SubdocComposer(Composer): def attach_parts(self, doc, remove_property_fields=True): - """ Attach docx parts instead of appending the whole document - thus subdoc insertion can be delegated to jinja2 """ + """Attach docx parts instead of appending the whole document + thus subdoc insertion can be delegated to jinja2""" self.reset_reference_mapping() # Remove custom property fields but keep the values @@ -51,22 +51,23 @@ def attach_parts(self, doc, remove_property_fields=True): def add_diagrams(self, doc, element): # While waiting docxcompose 1.3.3 - dgm_rels = xpath(element, './/dgm:relIds[@r:dm]') + dgm_rels = xpath(element, ".//dgm:relIds[@r:dm]") for dgm_rel in dgm_rels: for item, rt_type in ( - ('dm', RT.DIAGRAM_DATA), - ('lo', RT.DIAGRAM_LAYOUT), - ('qs', RT.DIAGRAM_QUICK_STYLE), - ('cs', RT.DIAGRAM_COLORS) + ("dm", RT.DIAGRAM_DATA), + ("lo", RT.DIAGRAM_LAYOUT), + ("qs", RT.DIAGRAM_QUICK_STYLE), + ("cs", RT.DIAGRAM_COLORS), ): - dm_rid = dgm_rel.get('{%s}%s' % (NS['r'], item)) + dm_rid = dgm_rel.get("{%s}%s" % (NS["r"], item)) dm_part = doc.part.rels[dm_rid].target_part new_rid = self.doc.part.relate_to(dm_part, rt_type) - dgm_rel.set('{%s}%s' % (NS['r'], item), new_rid) + dgm_rel.set("{%s}%s" % (NS["r"], item), new_rid) class Subdoc(object): - """ Class for subdocument to insert into master document """ + """Class for subdocument to insert into master document""" + def __init__(self, tpl, docpath=None): self.tpl = tpl self.docx = tpl.get_docx() @@ -83,8 +84,13 @@ def __getattr__(self, name): def _get_xml(self): if self.subdocx.element.body.sectPr is not None: self.subdocx.element.body.remove(self.subdocx.element.body.sectPr) - xml = re.sub(r']*>', '', etree.tostring( - self.subdocx.element.body, encoding='unicode', pretty_print=False)) + xml = re.sub( + r"]*>", + "", + etree.tostring( + self.subdocx.element.body, encoding="unicode", pretty_print=False + ), + ) return xml def __unicode__(self): diff --git a/docxtpl/template.py b/docxtpl/template.py index 31e9671..98d66ec 100644 --- a/docxtpl/template.py +++ b/docxtpl/template.py @@ -18,6 +18,7 @@ from docx.opc.constants import RELATIONSHIP_TYPE as REL_TYPE from jinja2 import Environment, Template, meta from jinja2.exceptions import TemplateError + try: from html import escape # noqa: F401 except ImportError: @@ -31,10 +32,14 @@ class DocxTemplate(object): - """ Class for managing docx files as they were jinja2 templates """ + """Class for managing docx files as they were jinja2 templates""" - HEADER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" - FOOTER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" + HEADER_URI = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" + ) + FOOTER_URI = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" + ) def __init__(self, template_file: Union[IO[bytes], str, PathLike]) -> None: self.template_file = template_file @@ -58,10 +63,10 @@ def render_init(self): def __getattr__(self, name): return getattr(self.docx, name) - def xml_to_string(self, xml, encoding='unicode'): + def xml_to_string(self, xml, encoding="unicode"): # Be careful : pretty_print MUST be set to False, otherwise patch_xml() # won't work properly - return etree.tostring(xml, encoding='unicode', pretty_print=False) + return etree.tostring(xml, encoding="unicode", pretty_print=False) def get_docx(self): self.init_docx() @@ -71,121 +76,178 @@ def get_xml(self): return self.xml_to_string(self.docx._element.body) def write_xml(self, filename): - with open(filename, 'w') as fh: + with open(filename, "w") as fh: fh.write(self.get_xml()) def patch_xml(self, src_xml): - """ Make a lots of cleaning to have a raw xml understandable by jinja2 : + """Make a lots of cleaning to have a raw xml understandable by jinja2 : strip all unnecessary xml tags, manage table cell background color and colspan, - unescape html entities, etc... """ + unescape html entities, etc...""" # replace {{ by {{ ( works with {{ }} {% and %} {# and #}) - src_xml = re.sub(r'(?<={)(<[^>]*>)+(?=[\{%\#])|(?<=[%\}\#])(<[^>]*>)+(?=\})', '', - src_xml, flags=re.DOTALL) + src_xml = re.sub( + r"(?<={)(<[^>]*>)+(?=[\{%\#])|(?<=[%\}\#])(<[^>]*>)+(?=\})", + "", + src_xml, + flags=re.DOTALL, + ) # replace {{jinja2 stuff}} by {{jinja2 stuff}} # same thing with {% ... %} and {# #} # "jinja2 stuff" could a variable, a 'if' etc... anything jinja2 will understand def striptags(m): - return re.sub('.*?(|]*>)', '', - m.group(0), flags=re.DOTALL) - src_xml = re.sub(r'{%(?:(?!%}).)*|{#(?:(?!#}).)*|{{(?:(?!}}).)*', striptags, - src_xml, flags=re.DOTALL) + return re.sub( + ".*?(|]*>)", "", m.group(0), flags=re.DOTALL + ) + + src_xml = re.sub( + r"{%(?:(?!%}).)*|{#(?:(?!#}).)*|{{(?:(?!}}).)*", + striptags, + src_xml, + flags=re.DOTALL, + ) # manage table cell colspan def colspan(m): cell_xml = m.group(1) + m.group(3) - cell_xml = re.sub(r'](?:(?!]).)*.*?', - '', cell_xml, flags=re.DOTALL) - cell_xml = re.sub(r'', '', cell_xml, count=1) - return re.sub(r'(]*>)', r'\1' - % m.group(2), cell_xml) - src_xml = re.sub(r'(](?:(?!]).)*){%\s*colspan\s+([^%]*)\s*%}(.*?)', - colspan, src_xml, flags=re.DOTALL) + cell_xml = re.sub( + r"](?:(?!]).)*.*?", + "", + cell_xml, + flags=re.DOTALL, + ) + cell_xml = re.sub(r"", "", cell_xml, count=1) + return re.sub( + r"(]*>)", + r'\1' % m.group(2), + cell_xml, + ) + + src_xml = re.sub( + r"(](?:(?!]).)*){%\s*colspan\s+([^%]*)\s*%}(.*?)", + colspan, + src_xml, + flags=re.DOTALL, + ) # manage table cell background color def cellbg(m): cell_xml = m.group(1) + m.group(3) - cell_xml = re.sub(r'](?:(?!]).)*.*?', - '', cell_xml, flags=re.DOTALL) - cell_xml = re.sub(r'', '', cell_xml, count=1) - return re.sub(r'(]*>)', - r'\1' - % m.group(2), cell_xml) - src_xml = re.sub(r'(](?:(?!]).)*){%\s*cellbg\s+([^%]*)\s*%}(.*?)', - cellbg, src_xml, flags=re.DOTALL) + cell_xml = re.sub( + r"](?:(?!]).)*.*?", + "", + cell_xml, + flags=re.DOTALL, + ) + cell_xml = re.sub(r"", "", cell_xml, count=1) + return re.sub( + r"(]*>)", + r'\1' % m.group(2), + cell_xml, + ) + + src_xml = re.sub( + r"(](?:(?!]).)*){%\s*cellbg\s+([^%]*)\s*%}(.*?)", + cellbg, + src_xml, + flags=re.DOTALL, + ) # ensure space preservation - src_xml = re.sub(r'((?:(?!).)*)({{.*?}}|{%.*?%})', - r'\1\2', - src_xml, flags=re.DOTALL) - src_xml = re.sub(r'({{r\s.*?}}|{%r\s.*?%})', - r'\1', - src_xml, flags=re.DOTALL) + src_xml = re.sub( + r"((?:(?!).)*)({{.*?}}|{%.*?%})", + r'\1\2', + src_xml, + flags=re.DOTALL, + ) + src_xml = re.sub( + r"({{r\s.*?}}|{%r\s.*?%})", + r'\1', + src_xml, + flags=re.DOTALL, + ) # {%- will merge with previous paragraph text - src_xml = re.sub(r'(?:(?!).)*?{%-', '{%', src_xml, flags=re.DOTALL) + src_xml = re.sub(r"(?:(?!).)*?{%-", "{%", src_xml, flags=re.DOTALL) # -%} will merge with next paragraph text - src_xml = re.sub(r'-%}(?:(?!]|{%|{{).)*?]*?>', '%}', src_xml, flags=re.DOTALL) + src_xml = re.sub( + r"-%}(?:(?!]|{%|{{).)*?]*?>", "%}", src_xml, flags=re.DOTALL + ) - for y in ['tr', 'tc', 'p', 'r']: + for y in ["tr", "tc", "p", "r"]: # replace into xml code the row/paragraph/run containing # {%y xxx %} or {{y xxx}} template tag # by {% xxx %} or {{ xx }} without any surrounding tags : # This is mandatory to have jinja2 generating correct xml code - pat = r'](?:(?!]).)*({%%|{{)%(y)s ([^}%%]*(?:%%}|}})).*?' % {'y': y} - src_xml = re.sub(pat, r'\1 \2', src_xml, flags=re.DOTALL) + pat = ( + r"](?:(?!]).)*({%%|{{)%(y)s ([^}%%]*(?:%%}|}})).*?" + % {"y": y} + ) + src_xml = re.sub(pat, r"\1 \2", src_xml, flags=re.DOTALL) - for y in ['tr', 'tc', 'p']: + for y in ["tr", "tc", "p"]: # same thing, but for {#y xxx #} (but not where y == 'r', since that # makes less sense to use comments in that context - pat = r'](?:(?!]).)*({#)%(y)s ([^}#]*(?:#})).*?' % {'y': y} - src_xml = re.sub(pat, r'\1 \2', src_xml, flags=re.DOTALL) + pat = ( + r"](?:(?!]).)*({#)%(y)s ([^}#]*(?:#})).*?" + % {"y": y} + ) + src_xml = re.sub(pat, r"\1 \2", src_xml, flags=re.DOTALL) # add vMerge # use {% vm %} to make this table cell and its copies be vertically merged within a {% for %} def v_merge_tc(m): def v_merge(m1): return ( - '' + - m1.group(1) + # Everything between ```` and ````. - "{% if loop.first %}" + - m1.group(2) + # Everything before ``{% vm %}``. - m1.group(3) + # Everything after ``{% vm %}``. - "{% endif %}" + - m1.group(4) # ````. + '' + + m1.group(1) # Everything between ```` and ````. + + "{% if loop.first %}" + + m1.group(2) # Everything before ``{% vm %}``. + + m1.group(3) # Everything after ``{% vm %}``. + + "{% endif %}" + + m1.group(4) # ````. ) + return re.sub( - r'(].*?)(.*?)(?:{%\s*vm\s*%})(.*?)()', + r"(].*?)(.*?)(?:{%\s*vm\s*%})(.*?)()", v_merge, m.group(), # Everything between ```` and ```` with ``{% vm %}`` inside. flags=re.DOTALL, ) - src_xml = re.sub(r'](?:(?!]).)*?{%\s*vm\s*%}.*?]', - v_merge_tc, src_xml, flags=re.DOTALL) + + src_xml = re.sub( + r"](?:(?!]).)*?{%\s*vm\s*%}.*?]", + v_merge_tc, + src_xml, + flags=re.DOTALL, + ) # Use ``{% hm %}`` to make table cell become horizontally merged within # a ``{% for %}``. def h_merge_tc(m): - xml_to_patch = m.group() # Everything between ```` and ```` with ``{% hm %}`` inside. + xml_to_patch = ( + m.group() + ) # Everything between ```` and ```` with ``{% hm %}`` inside. def with_gridspan(m1): return ( - m1.group(1) + # ``w:gridSpan w:val="``. - '{{ ' + m1.group(2) + ' * loop.length }}' + # Content of ``w:val``, multiplied by loop length. - m1.group(3) # Closing quotation mark. + m1.group(1) # ``w:gridSpan w:val="``. + + "{{ " + + m1.group(2) + + " * loop.length }}" # Content of ``w:val``, multiplied by loop length. + + m1.group(3) # Closing quotation mark. ) def without_gridspan(m2): return ( - '' + - m2.group(1) + # Everything between ```` and ````. - m2.group(2) + # Everything before ``{% hm %}``. - m2.group(3) + # Everything after ``{% hm %}``. - m2.group(4) # ````. + '' + + m2.group(1) # Everything between ```` and ````. + + m2.group(2) # Everything before ``{% hm %}``. + + m2.group(3) # Everything after ``{% hm %}``. + + m2.group(4) # ````. ) - if re.search(r'w:gridSpan', xml_to_patch): + if re.search(r"w:gridSpan", xml_to_patch): # Simple case, there's already ``gridSpan``, multiply its value. xml = re.sub( @@ -195,15 +257,15 @@ def without_gridspan(m2): flags=re.DOTALL, ) xml = re.sub( - r'{%\s*hm\s*%}', - '', + r"{%\s*hm\s*%}", + "", xml, # Patched xml. flags=re.DOTALL, ) else: # There're no ``gridSpan``, add one. xml = re.sub( - r'(].*?)(.*?)(?:{%\s*hm\s*%})(.*?)()', + r"(].*?)(.*?)(?:{%\s*hm\s*%})(.*?)()", without_gridspan, xml_to_patch, flags=re.DOTALL, @@ -212,24 +274,31 @@ def without_gridspan(m2): # Discard every other cell generated in loop. return "{% if loop.first %}" + xml + "{% endif %}" - src_xml = re.sub(r'](?:(?!]).)*?{%\s*hm\s*%}.*?]', - h_merge_tc, src_xml, flags=re.DOTALL) + src_xml = re.sub( + r"](?:(?!]).)*?{%\s*hm\s*%}.*?]", + h_merge_tc, + src_xml, + flags=re.DOTALL, + ) def clean_tags(m): - return (m.group(0) - .replace(r"‘", "'") - .replace('<', '<') - .replace('>', '>') - .replace(u'“', u'"') - .replace(u'”', u'"') - .replace(u"‘", u"'") - .replace(u"’", u"'")) - src_xml = re.sub(r'(?<=\{[\{%])(.*?)(?=[\}%]})', clean_tags, src_xml) + return ( + m.group(0) + .replace(r"‘", "'") + .replace("<", "<") + .replace(">", ">") + .replace("“", '"') + .replace("”", '"') + .replace("‘", "'") + .replace("’", "'") + ) + + src_xml = re.sub(r"(?<=\{[\{%])(.*?)(?=[\}%]})", clean_tags, src_xml) return src_xml def render_xml_part(self, src_xml, part, context, jinja_env=None): - src_xml = re.sub(r'])', r'\n])", r"\n]+>', '', x), - src_xml.splitlines()[line_number:(line_number + 7)]) + exc.docx_context = map( + lambda x: re.sub(r"<[^>]+>", "", x), + src_xml.splitlines()[line_number : (line_number + 7)], + ) raise exc - dst_xml = re.sub(r'\n])', r'])", r" None: + def render_properties( + self, context: Dict[str, Any], jinja_env: Optional[Environment] = None + ) -> None: # List of string attributes of docx.opc.coreprops.CoreProperties which are strings. # It seems that some attributes cannot be written as strings. Those are commented out. properties = [ - 'author', + "author", # 'category', - 'comments', + "comments", # 'content_status', - 'identifier', + "identifier", # 'keywords', - 'language', + "language", # 'last_modified_by', - 'subject', - 'title', + "subject", + "title", # 'version', ] if jinja_env is None: @@ -280,32 +354,53 @@ def render_properties(self, context: Dict[str, Any], jinja_env: Optional[Environ def resolve_listing(self, xml): def resolve_text(run_properties, paragraph_properties, m): - xml = m.group(0).replace('\t', '' - '%s' - '%s' % (run_properties, run_properties)) - xml = xml.replace('\a', '' - '%s%s' % (paragraph_properties, run_properties)) - xml = xml.replace('\n', '') - xml = xml.replace('\f', '' - '' - '%s%s' % (paragraph_properties, run_properties)) + xml = m.group(0).replace( + "\t", + "" + "%s" + '%s' % (run_properties, run_properties), + ) + xml = xml.replace( + "\a", + "" + '%s%s' + % (paragraph_properties, run_properties), + ) + xml = xml.replace("\n", '') + xml = xml.replace( + "\f", + "" + '' + '%s%s' + % (paragraph_properties, run_properties), + ) return xml def resolve_run(paragraph_properties, m): - run_properties = re.search(r'.*?', m.group(0)) - run_properties = run_properties.group(0) if run_properties else '' - return re.sub(r']*)?>.*?', - lambda x: resolve_text(run_properties, paragraph_properties, x), m.group(0), - flags=re.DOTALL) + run_properties = re.search(r".*?", m.group(0)) + run_properties = run_properties.group(0) if run_properties else "" + return re.sub( + r"]*)?>.*?", + lambda x: resolve_text(run_properties, paragraph_properties, x), + m.group(0), + flags=re.DOTALL, + ) def resolve_paragraph(m): - paragraph_properties = re.search(r'.*?', m.group(0)) - paragraph_properties = paragraph_properties.group(0) if paragraph_properties else '' - return re.sub(r']*)?>.*?', - lambda x: resolve_run(paragraph_properties, x), - m.group(0), flags=re.DOTALL) + paragraph_properties = re.search(r".*?", m.group(0)) + paragraph_properties = ( + paragraph_properties.group(0) if paragraph_properties else "" + ) + return re.sub( + r"]*)?>.*?", + lambda x: resolve_run(paragraph_properties, x), + m.group(0), + flags=re.DOTALL, + ) - xml = re.sub(r']*)?>.*?', resolve_paragraph, xml, flags=re.DOTALL) + xml = re.sub( + r"]*)?>.*?", resolve_paragraph, xml, flags=re.DOTALL + ) return xml @@ -332,7 +427,7 @@ def get_headers_footers_encoding(self, xml): m = re.match(r'<\?xml[^\?]+\bencoding="([^"]+)"', xml, re.I) if m: return m.group(1) - return 'utf-8' + return "utf-8" def build_headers_footers_xml(self, context, uri, jinja_env=None): for relKey, part in self.get_headers_footers(uri): @@ -353,7 +448,7 @@ def render( self, context: Dict[str, Any], jinja_env: Optional[Environment] = None, - autoescape: bool = False + autoescape: bool = False, ) -> None: # init template working attributes self.render_init() @@ -377,14 +472,12 @@ def render( self.map_tree(tree) # Headers - headers = self.build_headers_footers_xml(context, self.HEADER_URI, - jinja_env) + headers = self.build_headers_footers_xml(context, self.HEADER_URI, jinja_env) for relKey, xml in headers: self.map_headers_footers_xml(relKey, xml) # Footers - footers = self.build_headers_footers_xml(context, self.FOOTER_URI, - jinja_env) + footers = self.build_headers_footers_xml(context, self.FOOTER_URI, jinja_env) for relKey, xml in footers: self.map_headers_footers_xml(relKey, xml) @@ -399,15 +492,15 @@ def fix_tables(self, xml): parser = etree.XMLParser(recover=True) tree = etree.fromstring(xml, parser=parser) # get namespace - ns = '{' + tree.nsmap['w'] + '}' + ns = "{" + tree.nsmap["w"] + "}" # walk trough xml and find table - for t in tree.iter(ns+'tbl'): - tblGrid = t.find(ns+'tblGrid') - columns = tblGrid.findall(ns+'gridCol') + for t in tree.iter(ns + "tbl"): + tblGrid = t.find(ns + "tblGrid") + columns = tblGrid.findall(ns + "gridCol") to_add = 0 # walk trough all rows and try to find if there is higher cell count - for r in t.iter(ns+'tr'): - cells = r.findall(ns+'tc') + for r in t.iter(ns + "tr"): + cells = r.findall(ns + "tc") if (len(columns) + to_add) < len(cells): to_add = len(cells) - len(columns) # is necessary to add columns? @@ -417,39 +510,44 @@ def fix_tables(self, xml): width = 0.0 new_average = None for c in columns: - if not c.get(ns+'w') is None: - width += float(c.get(ns+'w')) + if not c.get(ns + "w") is None: + width += float(c.get(ns + "w")) # try to keep proportion of table if width > 0: old_average = width / len(columns) new_average = width / (len(columns) + to_add) # scale the old columns for c in columns: - c.set(ns+'w', str(int(float(c.get(ns+'w')) * - new_average/old_average))) + c.set( + ns + "w", + str( + int(float(c.get(ns + "w")) * new_average / old_average) + ), + ) # add new columns for i in range(to_add): - etree.SubElement(tblGrid, ns+'gridCol', - {ns+'w': str(int(new_average))}) + etree.SubElement( + tblGrid, ns + "gridCol", {ns + "w": str(int(new_average))} + ) # Refetch columns after columns addition. - columns = tblGrid.findall(ns + 'gridCol') + columns = tblGrid.findall(ns + "gridCol") columns_len = len(columns) cells_len_max = 0 def get_cell_len(total, cell): - tc_pr = cell.find(ns + 'tcPr') - grid_span = None if tc_pr is None else tc_pr.find(ns + 'gridSpan') + tc_pr = cell.find(ns + "tcPr") + grid_span = None if tc_pr is None else tc_pr.find(ns + "gridSpan") if grid_span is not None: - return total + int(grid_span.get(ns + 'val')) + return total + int(grid_span.get(ns + "val")) return total + 1 # Calculate max of table cells to compare with `gridCol`. - for r in t.iter(ns + 'tr'): - cells = r.findall(ns + 'tc') + for r in t.iter(ns + "tr"): + cells = r.findall(ns + "tc") cells_len = functools.reduce(get_cell_len, cells, 0) cells_len_max = max(cells_len_max, cells_len) @@ -463,11 +561,11 @@ def get_cell_len(total, cell): removed_width = 0.0 for c in columns[-to_remove:]: - removed_width += float(c.get(ns + 'w')) + removed_width += float(c.get(ns + "w")) tblGrid.remove(c) - columns_left = tblGrid.findall(ns + 'gridCol') + columns_left = tblGrid.findall(ns + "gridCol") # Distribute `removed_width` across all columns that has # left after extras removal. @@ -477,15 +575,15 @@ def get_cell_len(total, cell): extra_space = int(extra_space) for c in columns_left: - c.set(ns+'w', str(int(float(c.get(ns+'w')) + extra_space))) + c.set(ns + "w", str(int(float(c.get(ns + "w")) + extra_space))) return tree def fix_docpr_ids(self, tree): # some Ids may have some collisions : so renumbering all of them : - for elt in tree.xpath('//wp:docPr', namespaces=docx.oxml.ns.nsmap): + for elt in tree.xpath("//wp:docPr", namespaces=docx.oxml.ns.nsmap): self.docx_ids_index += 1 - elt.attrib['id'] = str(self.docx_ids_index) + elt.attrib["id"] = str(self.docx_ids_index) def new_subdoc(self, docpath=None): self.init_docx() @@ -493,13 +591,13 @@ def new_subdoc(self, docpath=None): @staticmethod def get_file_crc(file_obj): - if hasattr(file_obj, 'read'): + if hasattr(file_obj, "read"): buf = file_obj.read() else: - with open(file_obj, 'rb') as fh: + with open(file_obj, "rb") as fh: buf = fh.read() - crc = (binascii.crc32(buf) & 0xFFFFFFFF) + crc = binascii.crc32(buf) & 0xFFFFFFFF return crc def replace_media(self, src_file, dst_file): @@ -522,10 +620,10 @@ def replace_media(self, src_file, dst_file): """ crc = self.get_file_crc(src_file) - if hasattr(dst_file, 'read'): + if hasattr(dst_file, "read"): self.crc_to_new_media[crc] = dst_file.read() else: - with open(dst_file, 'rb') as fh: + with open(dst_file, "rb") as fh: self.crc_to_new_media[crc] = fh.read() def replace_pic(self, embedded_file, dst_file): @@ -543,11 +641,11 @@ def replace_pic(self, embedded_file, dst_file): for replace_embedded and replace_media) """ - if hasattr(dst_file, 'read'): + if hasattr(dst_file, "read"): # NOTE: file extension not checked self.pics_to_replace[embedded_file] = dst_file.read() else: - with open(dst_file, 'rb') as fh: + with open(dst_file, "rb") as fh: self.pics_to_replace[embedded_file] = fh.read() def replace_embedded(self, src_file, dst_file): @@ -563,7 +661,7 @@ def replace_embedded(self, src_file, dst_file): Note2 : it is important to have the source file as it is required to calculate its CRC to find them in the docx """ - with open(dst_file, 'rb') as fh: + with open(dst_file, "rb") as fh: crc = self.get_file_crc(src_file) self.crc_to_new_embedded[crc] = fh.read() @@ -594,7 +692,7 @@ def replace_zipname(self, zipname, dst_file): "word/embeddings/". Note that the file is renamed by MSWord, so you have to guess a little bit... """ - with open(dst_file, 'rb') as fh: + with open(dst_file, "rb") as fh: self.zipname_to_replace[zipname] = fh.read() def reset_replacements(self): @@ -619,11 +717,9 @@ def reset_replacements(self): self.pics_to_replace = {} def post_processing(self, docx_file): - if (self.crc_to_new_media or - self.crc_to_new_embedded or - self.zipname_to_replace): + if self.crc_to_new_media or self.crc_to_new_embedded or self.zipname_to_replace: - if hasattr(docx_file, 'read'): + if hasattr(docx_file, "read"): tmp_file = io.BytesIO() DocxTemplate(docx_file).save(tmp_file) tmp_file.seek(0) @@ -632,27 +728,31 @@ def post_processing(self, docx_file): docx_file.seek(0) else: - tmp_file = '%s_docxtpl_before_replace_medias' % docx_file + tmp_file = "%s_docxtpl_before_replace_medias" % docx_file os.rename(docx_file, tmp_file) with zipfile.ZipFile(tmp_file) as zin: - with zipfile.ZipFile(docx_file, 'w') as zout: + with zipfile.ZipFile(docx_file, "w") as zout: for item in zin.infolist(): buf = zin.read(item.filename) if item.filename in self.zipname_to_replace: zout.writestr(item, self.zipname_to_replace[item.filename]) - elif (item.filename.startswith('word/media/') and - item.CRC in self.crc_to_new_media): + elif ( + item.filename.startswith("word/media/") + and item.CRC in self.crc_to_new_media + ): zout.writestr(item, self.crc_to_new_media[item.CRC]) - elif (item.filename.startswith('word/embeddings/') and - item.CRC in self.crc_to_new_embedded): + elif ( + item.filename.startswith("word/embeddings/") + and item.CRC in self.crc_to_new_embedded + ): zout.writestr(item, self.crc_to_new_embedded[item.CRC]) else: zout.writestr(item, buf) - if not hasattr(tmp_file, 'read'): + if not hasattr(tmp_file, "read"): os.remove(tmp_file) - if hasattr(docx_file, 'read'): + if hasattr(docx_file, "read"): docx_file.seek(0) def pre_processing(self): @@ -677,9 +777,7 @@ def _replace_pics(self): # make sure all template images defined by user were replaced for img_id, replaced in replaced_pics.items(): if not replaced: - raise ValueError( - "Picture %s not found in the docx template" % img_id - ) + raise ValueError("Picture %s not found in the docx template" % img_id) def get_pic_map(self): return self.pic_map @@ -690,16 +788,17 @@ def _replace_docx_part_pics(self, doc_part, replaced_pics): part_map = {} - gds = et.xpath('//a:graphic/a:graphicData', namespaces=docx.oxml.ns.nsmap) + gds = et.xpath("//a:graphic/a:graphicData", namespaces=docx.oxml.ns.nsmap) for gd in gds: rel = None # Either IMAGE, CHART, SMART_ART, ... try: - if gd.attrib['uri'] == docx.oxml.ns.nsmap['pic']: + if gd.attrib["uri"] == docx.oxml.ns.nsmap["pic"]: # Either PICTURE or LINKED_PICTURE image - blip = gd.xpath('pic:pic/pic:blipFill/a:blip', - namespaces=docx.oxml.ns.nsmap)[0] - dest = blip.xpath('@r:embed', namespaces=docx.oxml.ns.nsmap) + blip = gd.xpath( + "pic:pic/pic:blipFill/a:blip", namespaces=docx.oxml.ns.nsmap + )[0] + dest = blip.xpath("@r:embed", namespaces=docx.oxml.ns.nsmap) if len(dest) > 0: rel = dest[0] else: @@ -707,24 +806,29 @@ def _replace_docx_part_pics(self, doc_part, replaced_pics): else: continue - non_visual_properties = 'pic:pic/pic:nvPicPr/pic:cNvPr/' - filename = gd.xpath('%s@name' % non_visual_properties, - namespaces=docx.oxml.ns.nsmap)[0] - titles = gd.xpath('%s@title' % non_visual_properties, - namespaces=docx.oxml.ns.nsmap) + non_visual_properties = "pic:pic/pic:nvPicPr/pic:cNvPr/" + filename = gd.xpath( + "%s@name" % non_visual_properties, namespaces=docx.oxml.ns.nsmap + )[0] + titles = gd.xpath( + "%s@title" % non_visual_properties, namespaces=docx.oxml.ns.nsmap + ) if titles: title = titles[0] else: title = "" - descriptions = gd.xpath('%s@descr' % non_visual_properties, - namespaces=docx.oxml.ns.nsmap) + descriptions = gd.xpath( + "%s@descr" % non_visual_properties, namespaces=docx.oxml.ns.nsmap + ) if descriptions: description = descriptions[0] else: description = "" - part_map[filename] = (doc_part.rels[rel].target_ref, - doc_part.rels[rel].target_part) + part_map[filename] = ( + doc_part.rels[rel].target_ref, + doc_part.rels[rel].target_part, + ) # replace data for img_id, img_data in six.iteritems(self.pics_to_replace): @@ -741,8 +845,7 @@ def _replace_docx_part_pics(self, doc_part, replaced_pics): def build_url_id(self, url): self.init_docx() - return self.docx._part.relate_to(url, REL_TYPE.HYPERLINK, - is_external=True) + return self.docx._part.relate_to(url, REL_TYPE.HYPERLINK, is_external=True) def save(self, filename: Union[IO[bytes], str, PathLike], *args, **kwargs) -> None: # case where save() is called without doing rendering @@ -754,7 +857,9 @@ def save(self, filename: Union[IO[bytes], str, PathLike], *args, **kwargs) -> No self.post_processing(filename) self.is_saved = True - def get_undeclared_template_variables(self, jinja_env: Optional[Environment] = None) -> Set[str]: + def get_undeclared_template_variables( + self, jinja_env: Optional[Environment] = None + ) -> Set[str]: self.init_docx(reload=False) xml = self.get_xml() xml = self.patch_xml(xml) diff --git a/poetry.lock b/poetry.lock index a6f6e73..7cb1d13 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,6 +14,75 @@ files = [ [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] +[[package]] +name = "black" +version = "24.4.2" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, + {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, + {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, + {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, + {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, + {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, + {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, + {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, + {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, + {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, + {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, + {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, + {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, + {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, + {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, + {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, + {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, + {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, + {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, + {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, + {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, + {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + [[package]] name = "docxcompose" version = "1.4.0" @@ -306,6 +375,55 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.2.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] + [[package]] name = "pycodestyle" version = "2.12.0" @@ -384,4 +502,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "0a19499992b7770bc844b87288ec61c29b194487b3e99437a9004e66d7965ca8" +content-hash = "43818448bde523eafcedcdaeb6541d8205a5d52eef5cb4d0e1a0563a7134a579" diff --git a/pyproject.toml b/pyproject.toml index 692505d..cf402f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ six = "^1.16.0" python-docx = "^1.1.2" docxcompose = "^1.4.0" jinja2 = "^3.1.4" +black = "^24.4.2" [tool.poetry.group.dev.dependencies] diff --git a/tests/cellbg.py b/tests/cellbg.py index 79938fb..e8f4487 100644 --- a/tests/cellbg.py +++ b/tests/cellbg.py @@ -1,42 +1,42 @@ # -*- coding: utf-8 -*- -''' +""" Created : 2015-03-12 @author: Eric Lapouyade -''' +""" from docxtpl import DocxTemplate, RichText -tpl = DocxTemplate('templates/cellbg_tpl.docx') +tpl = DocxTemplate("templates/cellbg_tpl.docx") context = { - 'alerts': [ + "alerts": [ { - 'date': '2015-03-10', - 'desc': RichText('Very critical alert', color='FF0000', bold=True), - 'type': 'CRITICAL', - 'bg': 'FF0000', + "date": "2015-03-10", + "desc": RichText("Very critical alert", color="FF0000", bold=True), + "type": "CRITICAL", + "bg": "FF0000", }, { - 'date': '2015-03-11', - 'desc': RichText('Just a warning'), - 'type': 'WARNING', - 'bg': 'FFDD00', + "date": "2015-03-11", + "desc": RichText("Just a warning"), + "type": "WARNING", + "bg": "FFDD00", }, { - 'date': '2015-03-12', - 'desc': RichText('Information'), - 'type': 'INFO', - 'bg': '8888FF', + "date": "2015-03-12", + "desc": RichText("Information"), + "type": "INFO", + "bg": "8888FF", }, { - 'date': '2015-03-13', - 'desc': RichText('Debug trace'), - 'type': 'DEBUG', - 'bg': 'FF00FF', + "date": "2015-03-13", + "desc": RichText("Debug trace"), + "type": "DEBUG", + "bg": "FF00FF", }, ], } tpl.render(context) -tpl.save('output/cellbg.docx') +tpl.save("output/cellbg.docx") diff --git a/tests/comments.py b/tests/comments.py index 5a214ca..b0d31e4 100644 --- a/tests/comments.py +++ b/tests/comments.py @@ -1,6 +1,6 @@ from docxtpl import DocxTemplate -tpl = DocxTemplate('templates/comments_tpl.docx') +tpl = DocxTemplate("templates/comments_tpl.docx") tpl.render({}) -tpl.save('output/comments.docx') +tpl.save("output/comments.docx") diff --git a/tests/custom_jinja_filters.py b/tests/custom_jinja_filters.py index 580986b..5d89570 100644 --- a/tests/custom_jinja_filters.py +++ b/tests/custom_jinja_filters.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -''' +""" Created : 2015-03-12 @author: sandeeprah, Eric Lapouyade -''' +""" from docxtpl import DocxTemplate import jinja2 @@ -14,7 +14,7 @@ # to create new filters, first create functions that accept the value to filter # as first argument, and filter parameters as next arguments def my_filterA(value, my_string_arg): - return_value = value + ' ' + my_string_arg + return_value = value + " " + my_string_arg return return_value @@ -24,12 +24,12 @@ def my_filterB(value, my_float_arg): # Then, declare them to jinja like this : -jinja_env.filters['my_filterA'] = my_filterA -jinja_env.filters['my_filterB'] = my_filterB +jinja_env.filters["my_filterA"] = my_filterA +jinja_env.filters["my_filterB"] = my_filterB -context = {'base_value_string': ' Hello', 'base_value_float': 1.5} +context = {"base_value_string": " Hello", "base_value_float": 1.5} -tpl = DocxTemplate('templates/custom_jinja_filters_tpl.docx') +tpl = DocxTemplate("templates/custom_jinja_filters_tpl.docx") tpl.render(context, jinja_env) -tpl.save('output/custom_jinja_filters.docx') +tpl.save("output/custom_jinja_filters.docx") diff --git a/tests/doc_properties.py b/tests/doc_properties.py index e60ac19..fe48bf8 100644 --- a/tests/doc_properties.py +++ b/tests/doc_properties.py @@ -1,12 +1,10 @@ from docxtpl import DocxTemplate -doctemplate = r'templates/doc_properties_tpl.docx' +doctemplate = r"templates/doc_properties_tpl.docx" tpl = DocxTemplate(doctemplate) -context = { - 'test': 'HelloWorld' -} +context = {"test": "HelloWorld"} tpl.render(context) tpl.save("output/doc_properties.docx") diff --git a/tests/dynamic_table.py b/tests/dynamic_table.py index f4446d3..2eb6b48 100644 --- a/tests/dynamic_table.py +++ b/tests/dynamic_table.py @@ -1,15 +1,15 @@ from docxtpl import DocxTemplate -tpl = DocxTemplate('templates/dynamic_table_tpl.docx') +tpl = DocxTemplate("templates/dynamic_table_tpl.docx") context = { - 'col_labels': ['fruit', 'vegetable', 'stone', 'thing'], - 'tbl_contents': [ - {'label': 'yellow', 'cols': ['banana', 'capsicum', 'pyrite', 'taxi']}, - {'label': 'red', 'cols': ['apple', 'tomato', 'cinnabar', 'doubledecker']}, - {'label': 'green', 'cols': ['guava', 'cucumber', 'aventurine', 'card']}, + "col_labels": ["fruit", "vegetable", "stone", "thing"], + "tbl_contents": [ + {"label": "yellow", "cols": ["banana", "capsicum", "pyrite", "taxi"]}, + {"label": "red", "cols": ["apple", "tomato", "cinnabar", "doubledecker"]}, + {"label": "green", "cols": ["guava", "cucumber", "aventurine", "card"]}, ], } tpl.render(context) -tpl.save('output/dynamic_table.docx') +tpl.save("output/dynamic_table.docx") diff --git a/tests/embedded.py b/tests/embedded.py index 842798c..a9fff5f 100644 --- a/tests/embedded.py +++ b/tests/embedded.py @@ -1,45 +1,45 @@ # -*- coding: utf-8 -*- -''' +""" Created : 2017-09-09 @author: Eric Lapouyade -''' +""" from docxtpl import DocxTemplate # rendering the "dynamic embedded docx": -embedded_docx_tpl = DocxTemplate('templates/embedded_embedded_docx_tpl.docx') +embedded_docx_tpl = DocxTemplate("templates/embedded_embedded_docx_tpl.docx") context = { - 'name': 'John Doe', + "name": "John Doe", } embedded_docx_tpl.render(context) -embedded_docx_tpl.save('output/embedded_embedded_docx.docx') +embedded_docx_tpl.save("output/embedded_embedded_docx.docx") # rendering the main document : -tpl = DocxTemplate('templates/embedded_main_tpl.docx') +tpl = DocxTemplate("templates/embedded_main_tpl.docx") context = { - 'name': 'John Doe', + "name": "John Doe", } tpl.replace_embedded( - 'templates/embedded_dummy.docx', 'templates/embedded_static_docx.docx' + "templates/embedded_dummy.docx", "templates/embedded_static_docx.docx" ) tpl.replace_embedded( - 'templates/embedded_dummy2.docx', 'output/embedded_embedded_docx.docx' + "templates/embedded_dummy2.docx", "output/embedded_embedded_docx.docx" ) # The zipname is the one you can find when you open docx with WinZip, 7zip (Windows) # or unzip -l (Linux). The zipname starts with "word/embeddings/". # Note that the file is renamed by MSWord, so you have to guess a little bit... tpl.replace_zipname( - 'word/embeddings/Feuille_Microsoft_Office_Excel3.xlsx', 'templates/real_Excel.xlsx' + "word/embeddings/Feuille_Microsoft_Office_Excel3.xlsx", "templates/real_Excel.xlsx" ) tpl.replace_zipname( - 'word/embeddings/Pr_sentation_Microsoft_Office_PowerPoint4.pptx', - 'templates/real_PowerPoint.pptx', + "word/embeddings/Pr_sentation_Microsoft_Office_PowerPoint4.pptx", + "templates/real_PowerPoint.pptx", ) tpl.render(context) -tpl.save('output/embedded.docx') +tpl.save("output/embedded.docx") diff --git a/tests/escape.py b/tests/escape.py index de14345..77375ab 100644 --- a/tests/escape.py +++ b/tests/escape.py @@ -1,19 +1,19 @@ from docxtpl import DocxTemplate, R, Listing -tpl = DocxTemplate('templates/escape_tpl.docx') +tpl = DocxTemplate("templates/escape_tpl.docx") context = { - 'myvar': R( + "myvar": R( '"less than" must be escaped : <, this can be done with RichText() or R()' ), - 'myescvar': 'It can be escaped with a "|e" jinja filter in the template too : < ', - 'nlnp': R('Here is a multiple\nlines\nstring\aand some\aother\aparagraphs', - color='#ff00ff'), - 'mylisting': Listing( - 'the listing\nwith\nsome\nlines\nand special chars : <>& ...' + "myescvar": 'It can be escaped with a "|e" jinja filter in the template too : < ', + "nlnp": R( + "Here is a multiple\nlines\nstring\aand some\aother\aparagraphs", + color="#ff00ff", ), - 'page_break': R('\f'), - 'new_listing': """ + "mylisting": Listing("the listing\nwith\nsome\nlines\nand special chars : <>& ..."), + "page_break": R("\f"), + "new_listing": """ This is a new listing Now, does not require Listing() Object Here is a \t tab\a @@ -21,34 +21,34 @@ Here is a page break : \f That's it """, - 'some_html': ( - 'HTTP/1.1 200 OK\n' - 'Server: Apache-Coyote/1.1\n' - 'Cache-Control: no-store\n' - 'Expires: Thu, 01 Jan 1970 00:00:00 GMT\n' - 'Pragma: no-cache\n' - 'Content-Type: text/html;charset=UTF-8\n' - 'Content-Language: zh-CN\n' - 'Date: Thu, 22 Oct 2020 10:59:40 GMT\n' - 'Content-Length: 9866\n' - '\n' - '\n' - '\n' - ' Struts Problem Report\n' - ' \n' - '\n' - '\n' - '...\n' - '\n' - '' + "some_html": ( + "HTTP/1.1 200 OK\n" + "Server: Apache-Coyote/1.1\n" + "Cache-Control: no-store\n" + "Expires: Thu, 01 Jan 1970 00:00:00 GMT\n" + "Pragma: no-cache\n" + "Content-Type: text/html;charset=UTF-8\n" + "Content-Language: zh-CN\n" + "Date: Thu, 22 Oct 2020 10:59:40 GMT\n" + "Content-Length: 9866\n" + "\n" + "\n" + "\n" + " Struts Problem Report\n" + " \n" + "\n" + "\n" + "...\n" + "\n" + "" ), } tpl.render(context) -tpl.save('output/escape.docx') +tpl.save("output/escape.docx") diff --git a/tests/escape_auto.py b/tests/escape_auto.py index e0def1d..bd4d676 100644 --- a/tests/escape_auto.py +++ b/tests/escape_auto.py @@ -12,18 +12,18 @@ XML_RESERVED = """<"&'>""" -tpl = DocxTemplate('templates/escape_tpl_auto.docx') +tpl = DocxTemplate("templates/escape_tpl_auto.docx") context = { - 'nested_dict': {name(text_type(c)): c for c in XML_RESERVED}, - 'autoescape': 'Escaped "str & ing"!', - 'autoescape_unicode': u'This is an escaped example \u4f60 & \u6211', - 'iteritems': iteritems, + "nested_dict": {name(text_type(c)): c for c in XML_RESERVED}, + "autoescape": 'Escaped "str & ing"!', + "autoescape_unicode": "This is an escaped example \u4f60 & \u6211", + "iteritems": iteritems, } tpl.render(context, autoescape=True) -OUTPUT = 'output' +OUTPUT = "output" if not os.path.exists(OUTPUT): os.makedirs(OUTPUT) -tpl.save(OUTPUT + '/escape_auto.docx') +tpl.save(OUTPUT + "/escape_auto.docx") diff --git a/tests/header_footer.py b/tests/header_footer.py index d60cc74..53362b6 100644 --- a/tests/header_footer.py +++ b/tests/header_footer.py @@ -1,25 +1,25 @@ # -*- coding: utf-8 -*- -''' +""" Created : 2015-03-12 @author: Eric Lapouyade -''' +""" from docxtpl import DocxTemplate -tpl = DocxTemplate('templates/header_footer_tpl.docx') +tpl = DocxTemplate("templates/header_footer_tpl.docx") sd = tpl.new_subdoc() p = sd.add_paragraph( - 'This is a sub-document to check it does not break header and footer' + "This is a sub-document to check it does not break header and footer" ) context = { - 'title': 'Header and footer test', - 'company_name': 'The World Wide company', - 'date': '2016-03-17', - 'mysubdoc': sd, + "title": "Header and footer test", + "company_name": "The World Wide company", + "date": "2016-03-17", + "mysubdoc": sd, } tpl.render(context) -tpl.save('output/header_footer.docx') +tpl.save("output/header_footer.docx") diff --git a/tests/header_footer_entities.py b/tests/header_footer_entities.py index 320b5ef..ddaa3d2 100644 --- a/tests/header_footer_entities.py +++ b/tests/header_footer_entities.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- -''' +""" Created : 2015-03-12 @author: Eric Lapouyade -''' +""" from docxtpl import DocxTemplate -tpl = DocxTemplate('templates/header_footer_entities_tpl.docx') +tpl = DocxTemplate("templates/header_footer_entities_tpl.docx") context = { - 'title': 'Header and footer test', + "title": "Header and footer test", } tpl.render(context) -tpl.save('output/header_footer_entities.docx') +tpl.save("output/header_footer_entities.docx") diff --git a/tests/header_footer_image.py b/tests/header_footer_image.py index 6ccaf55..24ca020 100644 --- a/tests/header_footer_image.py +++ b/tests/header_footer_image.py @@ -1,19 +1,19 @@ # -*- coding: utf-8 -*- -''' +""" Created : 2017-09-03 @author: Eric Lapouyade -''' +""" from docxtpl import DocxTemplate -DEST_FILE = 'output/header_footer_image.docx' +DEST_FILE = "output/header_footer_image.docx" -tpl = DocxTemplate('templates/header_footer_image_tpl.docx') +tpl = DocxTemplate("templates/header_footer_image_tpl.docx") context = { - 'mycompany': 'The World Wide company', + "mycompany": "The World Wide company", } -tpl.replace_media('templates/dummy_pic_for_header.png', 'templates/python.png') +tpl.replace_media("templates/dummy_pic_for_header.png", "templates/python.png") tpl.render(context) tpl.save(DEST_FILE) diff --git a/tests/header_footer_image_file_obj.py b/tests/header_footer_image_file_obj.py index a1bfcc9..0959748 100644 --- a/tests/header_footer_image_file_obj.py +++ b/tests/header_footer_image_file_obj.py @@ -1,29 +1,29 @@ # -*- coding: utf-8 -*- -''' +""" Created : 2019-05-22 @author: Eric Dufresne -''' +""" from docxtpl import DocxTemplate import io -DEST_FILE = 'output/header_footer_image_file_obj.docx' -DEST_FILE2 = 'output/header_footer_image_file_obj2.docx' +DEST_FILE = "output/header_footer_image_file_obj.docx" +DEST_FILE2 = "output/header_footer_image_file_obj2.docx" -tpl = DocxTemplate('templates/header_footer_image_tpl.docx') +tpl = DocxTemplate("templates/header_footer_image_tpl.docx") context = { - 'mycompany': 'The World Wide company', + "mycompany": "The World Wide company", } -dummy_pic = io.BytesIO(open('templates/dummy_pic_for_header.png', 'rb').read()) -new_image = io.BytesIO(open('templates/python.png', 'rb').read()) +dummy_pic = io.BytesIO(open("templates/dummy_pic_for_header.png", "rb").read()) +new_image = io.BytesIO(open("templates/python.png", "rb").read()) tpl.replace_media(dummy_pic, new_image) tpl.render(context) tpl.save(DEST_FILE) -tpl = DocxTemplate('templates/header_footer_image_tpl.docx') +tpl = DocxTemplate("templates/header_footer_image_tpl.docx") dummy_pic.seek(0) new_image.seek(0) tpl.replace_media(dummy_pic, new_image) @@ -32,5 +32,5 @@ file_obj = io.BytesIO() tpl.save(file_obj) file_obj.seek(0) -with open(DEST_FILE2, 'wb') as f: +with open(DEST_FILE2, "wb") as f: f.write(file_obj.read()) diff --git a/tests/header_footer_inline_image.py b/tests/header_footer_inline_image.py index 81fa0eb..26a3122 100644 --- a/tests/header_footer_inline_image.py +++ b/tests/header_footer_inline_image.py @@ -1,24 +1,24 @@ # -*- coding: utf-8 -*- -''' +""" Created : 2021-04-06 @author: Eric Lapouyade -''' +""" from docxtpl import DocxTemplate, InlineImage # for height and width you have to use millimeters (Mm), inches or points(Pt) class : from docx.shared import Mm -tpl = DocxTemplate('templates/header_footer_inline_image_tpl.docx') +tpl = DocxTemplate("templates/header_footer_inline_image_tpl.docx") context = { - 'inline_image': InlineImage(tpl, 'templates/django.png', height=Mm(10)), - 'images': [ - InlineImage(tpl, 'templates/python.png', height=Mm(10)), - InlineImage(tpl, 'templates/python.png', height=Mm(10)), - InlineImage(tpl, 'templates/python.png', height=Mm(10)) - ] + "inline_image": InlineImage(tpl, "templates/django.png", height=Mm(10)), + "images": [ + InlineImage(tpl, "templates/python.png", height=Mm(10)), + InlineImage(tpl, "templates/python.png", height=Mm(10)), + InlineImage(tpl, "templates/python.png", height=Mm(10)), + ], } tpl.render(context) -tpl.save('output/header_footer_inline_image.docx') +tpl.save("output/header_footer_inline_image.docx") diff --git a/tests/header_footer_utf8.py b/tests/header_footer_utf8.py index 9d2c16b..a167074 100644 --- a/tests/header_footer_utf8.py +++ b/tests/header_footer_utf8.py @@ -1,28 +1,28 @@ # -*- coding: utf-8 -*- -''' +""" Created : 2016-07-19 @author: AhnSeongHyun Edited : 2016-07-19 by Eric Lapouyade -''' +""" from docxtpl import DocxTemplate -tpl = DocxTemplate('templates/header_footer_tpl_utf8.docx') +tpl = DocxTemplate("templates/header_footer_tpl_utf8.docx") sd = tpl.new_subdoc() p = sd.add_paragraph( - u'This is a sub-document to check it does not break header and footer with utf-8 ' - u'characters inside the template .docx' + "This is a sub-document to check it does not break header and footer with utf-8 " + "characters inside the template .docx" ) context = { - 'title': u'헤더와 푸터', - 'company_name': u'세계적 회사', - 'date': u'2016-03-17', - 'mysubdoc': sd, + "title": "헤더와 푸터", + "company_name": "세계적 회사", + "date": "2016-03-17", + "mysubdoc": sd, } tpl.render(context) -tpl.save('output/header_footer_utf8.docx') +tpl.save("output/header_footer_utf8.docx") diff --git a/tests/horizontal_merge.py b/tests/horizontal_merge.py index 7c8393c..889eb77 100644 --- a/tests/horizontal_merge.py +++ b/tests/horizontal_merge.py @@ -2,6 +2,6 @@ from docxtpl import DocxTemplate -tpl = DocxTemplate('templates/horizontal_merge_tpl.docx') +tpl = DocxTemplate("templates/horizontal_merge_tpl.docx") tpl.render({}) -tpl.save('output/horizontal_merge.docx') +tpl.save("output/horizontal_merge.docx") diff --git a/tests/inline_image.py b/tests/inline_image.py index 5134d64..c07bf72 100644 --- a/tests/inline_image.py +++ b/tests/inline_image.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -''' +""" Created : 2017-01-14 @author: Eric Lapouyade -''' +""" from docxtpl import DocxTemplate, InlineImage @@ -11,37 +11,37 @@ from docx.shared import Mm import jinja2 -tpl = DocxTemplate('templates/inline_image_tpl.docx') +tpl = DocxTemplate("templates/inline_image_tpl.docx") context = { - 'myimage': InlineImage(tpl, 'templates/python_logo.png', width=Mm(20)), - 'myimageratio': InlineImage( - tpl, 'templates/python_jpeg.jpg', width=Mm(30), height=Mm(60) + "myimage": InlineImage(tpl, "templates/python_logo.png", width=Mm(20)), + "myimageratio": InlineImage( + tpl, "templates/python_jpeg.jpg", width=Mm(30), height=Mm(60) ), - 'frameworks': [ + "frameworks": [ { - 'image': InlineImage(tpl, 'templates/django.png', height=Mm(10)), - 'desc': 'The web framework for perfectionists with deadlines', + "image": InlineImage(tpl, "templates/django.png", height=Mm(10)), + "desc": "The web framework for perfectionists with deadlines", }, { - 'image': InlineImage(tpl, 'templates/zope.png', height=Mm(10)), - 'desc': 'Zope is a leading Open Source Application Server and Content Management Framework', + "image": InlineImage(tpl, "templates/zope.png", height=Mm(10)), + "desc": "Zope is a leading Open Source Application Server and Content Management Framework", }, { - 'image': InlineImage(tpl, 'templates/pyramid.png', height=Mm(10)), - 'desc': 'Pyramid is a lightweight Python web framework aimed at taking small web apps into big web apps.', + "image": InlineImage(tpl, "templates/pyramid.png", height=Mm(10)), + "desc": "Pyramid is a lightweight Python web framework aimed at taking small web apps into big web apps.", }, { - 'image': InlineImage(tpl, 'templates/bottle.png', height=Mm(10)), - 'desc': 'Bottle is a fast, simple and lightweight WSGI micro web-framework for Python', + "image": InlineImage(tpl, "templates/bottle.png", height=Mm(10)), + "desc": "Bottle is a fast, simple and lightweight WSGI micro web-framework for Python", }, { - 'image': InlineImage(tpl, 'templates/tornado.png', height=Mm(10)), - 'desc': 'Tornado is a Python web framework and asynchronous networking library.', + "image": InlineImage(tpl, "templates/tornado.png", height=Mm(10)), + "desc": "Tornado is a Python web framework and asynchronous networking library.", }, ], } # testing that it works also when autoescape has been forced to True jinja_env = jinja2.Environment(autoescape=True) tpl.render(context, jinja_env) -tpl.save('output/inline_image.docx') +tpl.save("output/inline_image.docx") diff --git a/tests/less_cells_after_loop.py b/tests/less_cells_after_loop.py index 4e0cd5a..ca725d4 100644 --- a/tests/less_cells_after_loop.py +++ b/tests/less_cells_after_loop.py @@ -1,5 +1,5 @@ from docxtpl import DocxTemplate -tpl = DocxTemplate('templates/less_cells_after_loop_tpl.docx') +tpl = DocxTemplate("templates/less_cells_after_loop_tpl.docx") tpl.render({}) -tpl.save('output/less_cells_after_loop.docx') +tpl.save("output/less_cells_after_loop.docx") diff --git a/tests/merge_docx.py b/tests/merge_docx.py index 9c99d11..28bbfd5 100644 --- a/tests/merge_docx.py +++ b/tests/merge_docx.py @@ -1,19 +1,19 @@ # -*- coding: utf-8 -*- -''' +""" Created : 2021-07-30 @author: Eric Lapouyade -''' +""" from docxtpl import DocxTemplate -tpl = DocxTemplate('templates/merge_docx_master_tpl.docx') -sd = tpl.new_subdoc('templates/merge_docx_subdoc.docx') +tpl = DocxTemplate("templates/merge_docx_master_tpl.docx") +sd = tpl.new_subdoc("templates/merge_docx_subdoc.docx") context = { - 'mysubdoc': sd, + "mysubdoc": sd, } tpl.render(context) -tpl.save('output/merge_docx.docx') +tpl.save("output/merge_docx.docx") diff --git a/tests/merge_paragraph.py b/tests/merge_paragraph.py index 6ff5cf0..39cdcef 100644 --- a/tests/merge_paragraph.py +++ b/tests/merge_paragraph.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- -''' +""" Created : 2015-03-12 @author: Eric Lapouyade -''' +""" from docxtpl import DocxTemplate -tpl = DocxTemplate('templates/merge_paragraph_tpl.docx') +tpl = DocxTemplate("templates/merge_paragraph_tpl.docx") context = { - 'living_in_town': True, + "living_in_town": True, } tpl.render(context) -tpl.save('output/merge_paragraph.docx') +tpl.save("output/merge_paragraph.docx") diff --git a/tests/module_execute.py b/tests/module_execute.py index e2ef28f..86c4ab9 100644 --- a/tests/module_execute.py +++ b/tests/module_execute.py @@ -1,19 +1,25 @@ import os -TEMPLATE_PATH = 'templates/module_execute_tpl.docx' -JSON_PATH = 'templates/module_execute.json' -OUTPUT_FILENAME = 'output/module_execute.docx' -OVERWRITE = '-o' -QUIET = '-q' +TEMPLATE_PATH = "templates/module_execute_tpl.docx" +JSON_PATH = "templates/module_execute.json" +OUTPUT_FILENAME = "output/module_execute.docx" +OVERWRITE = "-o" +QUIET = "-q" if os.path.exists(OUTPUT_FILENAME): os.unlink(OUTPUT_FILENAME) os.chdir(os.path.dirname(__file__)) -cmd = 'python -m docxtpl %s %s %s %s %s' % (TEMPLATE_PATH, JSON_PATH, OUTPUT_FILENAME, OVERWRITE, QUIET) +cmd = "python -m docxtpl %s %s %s %s %s" % ( + TEMPLATE_PATH, + JSON_PATH, + OUTPUT_FILENAME, + OVERWRITE, + QUIET, +) print('Executing "%s" ...' % cmd) os.system(cmd) if os.path.exists(OUTPUT_FILENAME): - print(' --> File %s has been generated.' % OUTPUT_FILENAME) + print(" --> File %s has been generated." % OUTPUT_FILENAME) diff --git a/tests/multi_rendering.py b/tests/multi_rendering.py index 6822a6f..f9a934f 100644 --- a/tests/multi_rendering.py +++ b/tests/multi_rendering.py @@ -1,40 +1,40 @@ # -*- coding: utf-8 -*- -''' +""" Created : 2021-12-20 @author: Eric Lapouyade -''' +""" from docxtpl import DocxTemplate -tpl = DocxTemplate('templates/multi_rendering_tpl.docx') +tpl = DocxTemplate("templates/multi_rendering_tpl.docx") documents_data = [ { - 'dest_file': 'multi_render1.docx', - 'context': { - 'title': 'Title ONE', - 'body': 'This is the body for first document' - } + "dest_file": "multi_render1.docx", + "context": { + "title": "Title ONE", + "body": "This is the body for first document", + }, }, { - 'dest_file': 'multi_render2.docx', - 'context': { - 'title': 'Title TWO', - 'body': 'This is the body for second document' - } + "dest_file": "multi_render2.docx", + "context": { + "title": "Title TWO", + "body": "This is the body for second document", + }, }, { - 'dest_file': 'multi_render3.docx', - 'context': { - 'title': 'Title THREE', - 'body': 'This is the body for third document' - } + "dest_file": "multi_render3.docx", + "context": { + "title": "Title THREE", + "body": "This is the body for third document", + }, }, ] for document_data in documents_data: - dest_file = document_data['dest_file'] - context = document_data['context'] + dest_file = document_data["dest_file"] + context = document_data["context"] tpl.render(context) - tpl.save('output/%s' % dest_file) + tpl.save("output/%s" % dest_file) diff --git a/tests/nested_for.py b/tests/nested_for.py index 839becc..fc67eea 100644 --- a/tests/nested_for.py +++ b/tests/nested_for.py @@ -1,45 +1,45 @@ # -*- coding: utf-8 -*- -''' +""" Created : 2016-03-26 @author: Eric Lapouyade -''' +""" from docxtpl import DocxTemplate -tpl = DocxTemplate('templates/nested_for_tpl.docx') +tpl = DocxTemplate("templates/nested_for_tpl.docx") context = { - 'dishes': [ - {'name': 'Pizza', 'ingredients': ['bread', 'tomato', 'ham', 'cheese']}, + "dishes": [ + {"name": "Pizza", "ingredients": ["bread", "tomato", "ham", "cheese"]}, { - 'name': 'Hamburger', - 'ingredients': ['bread', 'chopped steak', 'cheese', 'sauce'], + "name": "Hamburger", + "ingredients": ["bread", "chopped steak", "cheese", "sauce"], }, { - 'name': 'Apple pie', - 'ingredients': ['flour', 'apples', 'suggar', 'quince jelly'], + "name": "Apple pie", + "ingredients": ["flour", "apples", "suggar", "quince jelly"], }, ], - 'authors': [ + "authors": [ { - 'name': 'Saint-Exupery', - 'books': [ - {'title': 'Le petit prince'}, - {'title': "L'aviateur"}, - {'title': 'Vol de nuit'}, + "name": "Saint-Exupery", + "books": [ + {"title": "Le petit prince"}, + {"title": "L'aviateur"}, + {"title": "Vol de nuit"}, ], }, { - 'name': 'Barjavel', - 'books': [ - {'title': 'Ravage'}, - {'title': "La nuit des temps"}, - {'title': 'Le grand secret'}, + "name": "Barjavel", + "books": [ + {"title": "Ravage"}, + {"title": "La nuit des temps"}, + {"title": "Le grand secret"}, ], }, ], } tpl.render(context) -tpl.save('output/nested_for.docx') +tpl.save("output/nested_for.docx") diff --git a/tests/order.py b/tests/order.py index a4f1f57..416da89 100644 --- a/tests/order.py +++ b/tests/order.py @@ -1,26 +1,26 @@ # -*- coding: utf-8 -*- -''' +""" Created : 2015-03-12 @author: Eric Lapouyade -''' +""" from docxtpl import DocxTemplate -tpl = DocxTemplate('templates/order_tpl.docx') +tpl = DocxTemplate("templates/order_tpl.docx") context = { - 'customer_name': 'Eric', - 'items': [ - {'desc': 'Python interpreters', 'qty': 2, 'price': 'FREE'}, - {'desc': 'Django projects', 'qty': 5403, 'price': 'FREE'}, - {'desc': 'Guido', 'qty': 1, 'price': '100,000,000.00'}, + "customer_name": "Eric", + "items": [ + {"desc": "Python interpreters", "qty": 2, "price": "FREE"}, + {"desc": "Django projects", "qty": 5403, "price": "FREE"}, + {"desc": "Guido", "qty": 1, "price": "100,000,000.00"}, ], - 'in_europe': True, - 'is_paid': False, - 'company_name': 'The World Wide company', - 'total_price': '100,000,000.00', + "in_europe": True, + "is_paid": False, + "company_name": "The World Wide company", + "total_price": "100,000,000.00", } tpl.render(context) -tpl.save('output/order.docx') +tpl.save("output/order.docx") diff --git a/tests/preserve_spaces.py b/tests/preserve_spaces.py index 8ff9e82..03d9af0 100644 --- a/tests/preserve_spaces.py +++ b/tests/preserve_spaces.py @@ -3,12 +3,12 @@ # With old docxtpl version, "... for spicy ..." was replaced by "... forspicy..." # This test is for checking that is some cases the spaces are not lost anymore -tpl = DocxTemplate('templates/preserve_spaces_tpl.docx') +tpl = DocxTemplate("templates/preserve_spaces_tpl.docx") -tags = ['tag_1', 'tag_2'] -replacement = ['looking', 'too'] +tags = ["tag_1", "tag_2"] +replacement = ["looking", "too"] context = dict(zip(tags, replacement)) tpl.render(context) -tpl.save('output/preserve_spaces.docx') +tpl.save("output/preserve_spaces.docx") diff --git a/tests/replace_picture.py b/tests/replace_picture.py index 45ccd7d..c30f2ce 100644 --- a/tests/replace_picture.py +++ b/tests/replace_picture.py @@ -1,18 +1,18 @@ # -*- coding: utf-8 -*- -''' +""" Created : 2017-09-03 @author: Eric Lapouyade -''' +""" from docxtpl import DocxTemplate -DEST_FILE = 'output/replace_picture.docx' +DEST_FILE = "output/replace_picture.docx" -tpl = DocxTemplate('templates/replace_picture_tpl.docx') +tpl = DocxTemplate("templates/replace_picture_tpl.docx") context = {} -tpl.replace_pic('python_logo.png', 'templates/python.png') +tpl.replace_pic("python_logo.png", "templates/python.png") tpl.render(context) tpl.save(DEST_FILE) diff --git a/tests/richtext.py b/tests/richtext.py index 7d40ae5..c836ecf 100644 --- a/tests/richtext.py +++ b/tests/richtext.py @@ -1,55 +1,64 @@ # -*- coding: utf-8 -*- -''' +""" Created : 2015-03-26 @author: Eric Lapouyade -''' +""" from docxtpl import DocxTemplate, RichText -tpl = DocxTemplate('templates/richtext_tpl.docx') +tpl = DocxTemplate("templates/richtext_tpl.docx") rt = RichText() -rt.add('a rich text', style='myrichtextstyle') -rt.add(' with ') -rt.add('some italic', italic=True) -rt.add(' and ') -rt.add('some violet', color='#ff00ff') -rt.add(' and ') -rt.add('some striked', strike=True) -rt.add(' and ') -rt.add('some Highlighted', highlight='#ffff00') -rt.add(' and ') -rt.add('some small', size=14) -rt.add(' or ') -rt.add('big', size=60) -rt.add(' text.') -rt.add('\nYou can add an hyperlink, here to ') -rt.add('google', url_id=tpl.build_url_id('http://google.com')) -rt.add('\nEt voilà ! ') -rt.add('\n1st line') -rt.add('\n2nd line') -rt.add('\n3rd line') -rt.add('\aA new paragraph : \a') -rt.add('--- A page break here (see next page) ---\f') - -for ul in ['single', 'double', 'thick', 'dotted', 'dash', 'dotDash', 'dotDotDash', 'wave']: - rt.add('\nUnderline : ' + ul + ' \n', underline=ul) -rt.add('\nFonts :\n', underline=True) -rt.add('Arial\n', font='Arial') -rt.add('Courier New\n', font='Courier New') -rt.add('Times New Roman\n', font='Times New Roman') -rt.add('\n\nHere some') -rt.add('superscript', superscript=True) -rt.add(' and some') -rt.add('subscript', subscript=True) - -rt_embedded = RichText('an example of ') +rt.add("a rich text", style="myrichtextstyle") +rt.add(" with ") +rt.add("some italic", italic=True) +rt.add(" and ") +rt.add("some violet", color="#ff00ff") +rt.add(" and ") +rt.add("some striked", strike=True) +rt.add(" and ") +rt.add("some Highlighted", highlight="#ffff00") +rt.add(" and ") +rt.add("some small", size=14) +rt.add(" or ") +rt.add("big", size=60) +rt.add(" text.") +rt.add("\nYou can add an hyperlink, here to ") +rt.add("google", url_id=tpl.build_url_id("http://google.com")) +rt.add("\nEt voilà ! ") +rt.add("\n1st line") +rt.add("\n2nd line") +rt.add("\n3rd line") +rt.add("\aA new paragraph : \a") +rt.add("--- A page break here (see next page) ---\f") + +for ul in [ + "single", + "double", + "thick", + "dotted", + "dash", + "dotDash", + "dotDotDash", + "wave", +]: + rt.add("\nUnderline : " + ul + " \n", underline=ul) +rt.add("\nFonts :\n", underline=True) +rt.add("Arial\n", font="Arial") +rt.add("Courier New\n", font="Courier New") +rt.add("Times New Roman\n", font="Times New Roman") +rt.add("\n\nHere some") +rt.add("superscript", superscript=True) +rt.add(" and some") +rt.add("subscript", subscript=True) + +rt_embedded = RichText("an example of ") rt_embedded.add(rt) context = { - 'example': rt_embedded, + "example": rt_embedded, } tpl.render(context) -tpl.save('output/richtext.docx') +tpl.save("output/richtext.docx") diff --git a/tests/richtext_and_if.py b/tests/richtext_and_if.py index 34ed64d..031eb62 100644 --- a/tests/richtext_and_if.py +++ b/tests/richtext_and_if.py @@ -1,16 +1,16 @@ # -*- coding: utf-8 -*- -''' +""" Created : 2015-03-26 @author: Eric Lapouyade -''' +""" from docxtpl import DocxTemplate, RichText -tpl = DocxTemplate('templates/richtext_and_if_tpl.docx') +tpl = DocxTemplate("templates/richtext_and_if_tpl.docx") -context = {'foobar': RichText('Foobar!', color='ff0000')} +context = {"foobar": RichText("Foobar!", color="ff0000")} tpl.render(context) -tpl.save('output/richtext_and_if.docx') +tpl.save("output/richtext_and_if.docx") diff --git a/tests/richtext_eastAsia.py b/tests/richtext_eastAsia.py index 1dd2a4f..23177bc 100644 --- a/tests/richtext_eastAsia.py +++ b/tests/richtext_eastAsia.py @@ -6,15 +6,16 @@ from docxtpl import DocxTemplate, RichText -tpl = DocxTemplate('templates/richtext_eastAsia_tpl.docx') -rt = RichText('测试TEST', font='eastAsia:Microsoft YaHei') -ch = RichText('测试TEST', font='eastAsia:微软雅黑') -sun = RichText('测试TEST', font='eastAsia:SimSun') + +tpl = DocxTemplate("templates/richtext_eastAsia_tpl.docx") +rt = RichText("测试TEST", font="eastAsia:Microsoft YaHei") +ch = RichText("测试TEST", font="eastAsia:微软雅黑") +sun = RichText("测试TEST", font="eastAsia:SimSun") context = { - 'example': rt, - 'Chinese': ch, - 'simsun': sun, + "example": rt, + "Chinese": ch, + "simsun": sun, } tpl.render(context) -tpl.save('output/richtext_eastAsia.docx') +tpl.save("output/richtext_eastAsia.docx") diff --git a/tests/runtests.py b/tests/runtests.py index c956864..083ae0c 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -3,16 +3,16 @@ import six import os -tests = sorted(glob.glob('[A-Za-z]*.py')) -excludes = ['runtests.py'] +tests = sorted(glob.glob("[A-Za-z]*.py")) +excludes = ["runtests.py"] -output_dir = os.path.join(os.path.dirname(__file__), 'output') +output_dir = os.path.join(os.path.dirname(__file__), "output") if not os.path.exists(output_dir): os.mkdir(output_dir) for test in tests: if test not in excludes: - six.print_('%s ...' % test) - subprocess.call(['python', './%s' % test]) + six.print_("%s ..." % test) + subprocess.call(["python", "./%s" % test]) -six.print_('Done.') +six.print_("Done.") diff --git a/tests/subdoc.py b/tests/subdoc.py index b118e4b..10f5abd 100644 --- a/tests/subdoc.py +++ b/tests/subdoc.py @@ -1,36 +1,36 @@ # -*- coding: utf-8 -*- -''' +""" Created : 2015-03-12 @author: Eric Lapouyade -''' +""" from docxtpl import DocxTemplate from docx.shared import Inches -tpl = DocxTemplate('templates/subdoc_tpl.docx') +tpl = DocxTemplate("templates/subdoc_tpl.docx") sd = tpl.new_subdoc() -p = sd.add_paragraph('This is a sub-document inserted into a bigger one') -p = sd.add_paragraph('It has been ') -p.add_run('dynamically').style = 'dynamic' -p.add_run(' generated with python by using ') -p.add_run('python-docx').italic = True -p.add_run(' library') +p = sd.add_paragraph("This is a sub-document inserted into a bigger one") +p = sd.add_paragraph("It has been ") +p.add_run("dynamically").style = "dynamic" +p.add_run(" generated with python by using ") +p.add_run("python-docx").italic = True +p.add_run(" library") -sd.add_heading('Heading, level 1', level=1) -sd.add_paragraph('This is an Intense quote', style='IntenseQuote') +sd.add_heading("Heading, level 1", level=1) +sd.add_paragraph("This is an Intense quote", style="IntenseQuote") -sd.add_paragraph('A picture :') -sd.add_picture('templates/python_logo.png', width=Inches(1.25)) +sd.add_paragraph("A picture :") +sd.add_picture("templates/python_logo.png", width=Inches(1.25)) -sd.add_paragraph('A Table :') +sd.add_paragraph("A Table :") table = sd.add_table(rows=1, cols=3) hdr_cells = table.rows[0].cells -hdr_cells[0].text = 'Qty' -hdr_cells[1].text = 'Id' -hdr_cells[2].text = 'Desc' -recordset = ((1, 101, 'Spam'), (2, 42, 'Eggs'), (3, 631, 'Spam,spam, eggs, and ham')) +hdr_cells[0].text = "Qty" +hdr_cells[1].text = "Id" +hdr_cells[2].text = "Desc" +recordset = ((1, 101, "Spam"), (2, 42, "Eggs"), (3, 631, "Spam,spam, eggs, and ham")) for item in recordset: row_cells = table.add_row().cells row_cells[0].text = str(item[0]) @@ -38,8 +38,8 @@ row_cells[2].text = item[2] context = { - 'mysubdoc': sd, + "mysubdoc": sd, } tpl.render(context) -tpl.save('output/subdoc.docx') +tpl.save("output/subdoc.docx") diff --git a/tests/template_error.py b/tests/template_error.py index 2938fdc..4a7d909 100644 --- a/tests/template_error.py +++ b/tests/template_error.py @@ -2,19 +2,19 @@ from jinja2.exceptions import TemplateError import six -six.print_('=' * 80) +six.print_("=" * 80) six.print_("Generating template error for testing (so it is safe to ignore) :") -six.print_('.' * 80) +six.print_("." * 80) try: - tpl = DocxTemplate('templates/template_error_tpl.docx') - tpl.render({'test_variable': 'test variable value'}) + tpl = DocxTemplate("templates/template_error_tpl.docx") + tpl.render({"test_variable": "test variable value"}) except TemplateError as the_error: six.print_(six.text_type(the_error)) - if hasattr(the_error, 'docx_context'): + if hasattr(the_error, "docx_context"): six.print_("Context:") for line in the_error.docx_context: six.print_(line) -tpl.save('output/template_error.docx') -six.print_('.' * 80) +tpl.save("output/template_error.docx") +six.print_("." * 80) six.print_(" End of TemplateError Test ") -six.print_('=' * 80) +six.print_("=" * 80) diff --git a/tests/vertical_merge.py b/tests/vertical_merge.py index 60b6288..c0dc68f 100644 --- a/tests/vertical_merge.py +++ b/tests/vertical_merge.py @@ -1,23 +1,23 @@ # -*- coding: utf-8 -*- -''' +""" Created : 2017-10-15 @author: Arthaslixin -''' +""" from docxtpl import DocxTemplate -tpl = DocxTemplate('templates/vertical_merge_tpl.docx') +tpl = DocxTemplate("templates/vertical_merge_tpl.docx") context = { - 'items': [ - {'desc': 'Python interpreters', 'qty': 2, 'price': 'FREE'}, - {'desc': 'Django projects', 'qty': 5403, 'price': 'FREE'}, - {'desc': 'Guido', 'qty': 1, 'price': '100,000,000.00'}, + "items": [ + {"desc": "Python interpreters", "qty": 2, "price": "FREE"}, + {"desc": "Django projects", "qty": 5403, "price": "FREE"}, + {"desc": "Guido", "qty": 1, "price": "100,000,000.00"}, ], - 'total_price': '100,000,000.00', - 'category': 'Book', + "total_price": "100,000,000.00", + "category": "Book", } tpl.render(context) -tpl.save('output/vertical_merge.docx') +tpl.save("output/vertical_merge.docx") diff --git a/tests/vertical_merge_nested.py b/tests/vertical_merge_nested.py index 3ae7bcf..bcd912a 100644 --- a/tests/vertical_merge_nested.py +++ b/tests/vertical_merge_nested.py @@ -1,5 +1,5 @@ from docxtpl import DocxTemplate -tpl = DocxTemplate('templates/vertical_merge_nested_tpl.docx') +tpl = DocxTemplate("templates/vertical_merge_nested_tpl.docx") tpl.render({}) -tpl.save('output/vertical_merge_nested.docx') +tpl.save("output/vertical_merge_nested.docx") diff --git a/tests/word2016.py b/tests/word2016.py index eb2f77d..81f2333 100644 --- a/tests/word2016.py +++ b/tests/word2016.py @@ -1,12 +1,12 @@ from docxtpl import DocxTemplate, RichText -tpl = DocxTemplate('templates/word2016_tpl.docx') +tpl = DocxTemplate("templates/word2016_tpl.docx") tpl.render( { - 'test_space': ' ', - 'test_tabs': 5 * '\t', - 'test_space_r': RichText(' '), - 'test_tabs_r': RichText(5 * '\t'), + "test_space": " ", + "test_tabs": 5 * "\t", + "test_space_r": RichText(" "), + "test_tabs_r": RichText(5 * "\t"), } ) -tpl.save('output/word2016.docx') +tpl.save("output/word2016.docx")