pythondoctest

ValueError: wrapper loop when unwrapping


Python3 test cases (doctests) are failing with my sample code. But the same is working fine in Python2.

test.py:

class Test(object):
    def __init__(self, a=0):
        self.a = a

    def __getattr__(self, attr):
        return Test(a=str(self.a) + attr)

tst.py:

from test import Test

t = Test()

Run test cases: python3 -m doctest -v tst.py

Error:

Traceback (most recent call last):
  File "/usr/lib/python3.6/runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "/usr/lib/python3.6/runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "/usr/lib/python3.6/doctest.py", line 2787, in <module>
    sys.exit(_test())
  File "/usr/lib/python3.6/doctest.py", line 2777, in _test
    failures, _ = testmod(m, verbose=verbose, optionflags=options)
  File "/usr/lib/python3.6/doctest.py", line 1950, in testmod
    for test in finder.find(m, name, globs=globs, extraglobs=extraglobs):
  File "/usr/lib/python3.6/doctest.py", line 933, in find
    self._find(tests, obj, name, module, source_lines, globs, {})
  File "/usr/lib/python3.6/doctest.py", line 992, in _find
    if ((inspect.isroutine(inspect.unwrap(val))
  File "/usr/lib/python3.6/inspect.py", line 513, in unwrap
    raise ValueError('wrapper loop when unwrapping {!r}'.format(f))
ValueError: wrapper loop when unwrapping <test.Test object at 0x7f6e80028550>

Solution

  • This is arguably a bug in doctest. What's happening is that doctest is searching for functions/methods/callables with a docstring, and while doing so it's unwrapping any decorators it finds. Why it does this, I have no idea. But anyway, doctest ends up calling inspect.unwrap(t) (where t is a Test instance), which is essentially equivalent to doing this:

    while True:
       try:
           t = t.__wrapped__
       except AttributeError:
           break
    

    Because t is a Test instance, accessing t.__wrapped__ calls __getattr__ and returns a new Test instance. This would go on forever, but inspect.unwrap is smart enough to notice that it's not getting anywhere, and throws an exception instead of entering an infinite loop.


    As a workaround, you can rewrite your __getattr__ method to throw an AttributeError when __wrapped__ is accessed. Even better, throw an AttributeError when any dunder-attribute is accessed:

    def __getattr__(self, attr):
        if attr.startswith('__') and attr.endswith('__'):
            raise AttributeError
        return Test(a=str(self.a) + attr)