elapouya / python-docx-template

Use a docx as a jinja2 template
GNU Lesser General Public License v2.1
2k stars 386 forks source link

__radd__, __rmod__ and __rdiv__ don't seem to be used #399

Closed nonprofittechy closed 2 years ago

nonprofittechy commented 2 years ago

Describe the bug

I am experimenting with using a custom undefined handler that intercepts most errors caused by undefined variables. These three methods:

Do not seem to be accessed by python-docx-template. The left-handed equivalents work fine.

To Reproduce

Below is my custom undefined handler:

from jinja2 import ChainableUndefined

class DASkipUndefined(ChainableUndefined):
    """Undefined handler for Jinja2 exceptions that allows rendering most
    templates that have undefined variables. It will not fix all broken
    templates. For example, if the missing variable is used in a complex
    mathematical expression it may still break (but expressions with only two
    elements should render as ''). 
    """
    def __init__(self, *pargs, **kwargs):
        # Handle the way Docassemble DAEnvironment triggers attribute errors
        pass

    def __str__(self) -> str:
        return ''

    def __call__(self, *pargs, **kwargs)->"DASkipUndefined":
        return self

    __getitem__ = __getattr__ = __call__

    def __eq__(self, *pargs) -> bool:
        return False

    # need to return a bool type
    __bool__ = __ne__ = __le__ = __lt__ = __gt__ = __ge__ = __nonzero__ = __eq__

    # let undefined variables work in for loops
    def __iter__(self, *pargs)->"DASkipUndefined":
        return self

    def __next__(self, *pargs)->None:
        raise StopIteration        

    # need to return an int type
    def __int__(self, *pargs)->int:
        return 0

    __len__ = __int__

    # need to return a float type
    def __float__(self, *pargs)->float:
        return 0.0

    # need to return complex type
    def __complex__(self, *pargs)->complex:
        return 0j

    def __add__(self, *pargs, **kwargs)->str:
        return self.__str__()

    # type can be anything. we want it to work with `str()` function though
    # and we do not want to silently give wrong math results.
    # note that this means 1 + (undefined) or (undefined) + 1 will work but not 1 + (undefined) + 1
    __radd__ = __mul__ = __rmul__ = __div__ = __rdiv__ = \
        __truediv__ = __rtruediv__ = __floordiv__ = __rfloordiv__ = \
        __mod__ = __rmod__ = __pos__ = __neg__ = __pow__ = __rpow__ = \
        __sub__ = __rsub__= __hash__ = __add__ 

Expected behavior

Trying to render a template with {{ 1 + undefined_variable }} should produce the same results as {{ undefined_variable + 1 }}.

Instead, one returns '' and the other raises a type error.

Attaching a template that can demonstrate the problem.

test_da_skipundefined.docx

Note that the template should render fine if the __radd__ row is removed. I left the __rdiv__ and __rmod__ rows untouched.

elapouya commented 2 years ago

In your bug, I can see a docx template but no code that use it. Do you have at least a stack to give me ?

nonprofittechy commented 2 years ago

Sorry about that--I am rendering the template inside Docassemble. I can try to put together a test cases that leaves that dependency out. Here is the stack trace:

Traceback (most recent call last):
  File "/usr/share/docassemble/local3.8/lib/python3.8/site-packages/flask/app.py", line 1950, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/share/docassemble/local3.8/lib/python3.8/site-packages/flask/app.py", line 1936, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/usr/share/docassemble/local3.8/lib/python3.8/site-packages/docassemble/webapp/server.py", line 7648, in index
    interview.assemble(user_dict, interview_status, old_user_dict, force_question=special_question)
  File "/usr/share/docassemble/local3.8/lib/python3.8/site-packages/docassemble/base/parse.py", line 8034, in assemble
    raise the_error
  File "/usr/share/docassemble/local3.8/lib/python3.8/site-packages/docassemble/base/parse.py", line 7778, in assemble
    interview_status.populate(question.ask(user_dict, old_user_dict, 'None', [], None, None))
  File "/usr/share/docassemble/local3.8/lib/python3.8/site-packages/docassemble/base/parse.py", line 5981, in ask
    attachment_text = self.processed_attachments(user_dict) # , the_x=the_x, iterators=iterators
  File "/usr/share/docassemble/local3.8/lib/python3.8/site-packages/docassemble/base/parse.py", line 6057, in processed_attachments
    result_list.append(self.finalize_attachment(item[0], item[1], the_user_dict))
  File "/usr/share/docassemble/local3.8/lib/python3.8/site-packages/docassemble/base/parse.py", line 6263, in finalize_attachment
    the_template.render(result['field_data'], jinja_env=custom_jinja_env(skip_undefined = attachment['options']['skip_undefined']))
  File "/usr/share/docassemble/local3.8/lib/python3.8/site-packages/docxtpl/__init__.py", line 316, in render
    xml_src = self.build_xml(context, jinja_env)
  File "/usr/share/docassemble/local3.8/lib/python3.8/site-packages/docxtpl/__init__.py", line 271, in build_xml
    xml = self.render_xml_part(xml, self.docx._part, context, jinja_env)
  File "/usr/share/docassemble/local3.8/lib/python3.8/site-packages/docxtpl/__init__.py", line 220, in render_xml_part
    dst_xml = template.render(context)
  File "/usr/share/docassemble/local3.8/lib/python3.8/site-packages/jinja2/environment.py", line 1289, in render
    self.environment.handle_exception()
  File "/usr/share/docassemble/local3.8/lib/python3.8/site-packages/jinja2/environment.py", line 924, in handle_exception
    raise rewrite_traceback_stack(source=source)
  File "<template>", line 1, in top-level template code
TypeError: unsupported operand type(s) for +: 'int' and 'str'

test_da_skipundefined.docx

elapouya commented 2 years ago

This is a Jinja2 and/or pure python issue, not a python-docx-template one. you are adding a string and an int. I guess you gave an string in the context instead of an int.

nonprofittechy commented 2 years ago

It may be a jinja2 error upstream. But because I am defining __radd__ it should be possible to add a string and an integer. It also shouldn't work with __add__ and fail with __radd__. The only reason for that would be something bypassing the call to __radd__.

elapouya commented 2 years ago

May be Jinja2 does not handle the '+' the way you think... I suggest to contact Jinja2 creator.