Search code examples
pythonpython-3.xdoctest

@cached_property doctest is not detected


I have a code. a.py

from functools import cached_property, cache
import doctest


class C1:

    def test_1(self):
        """
        >>> C1().test_1()
        'f'
        """
        return "f"

    @property
    def test_2(self):
        """
        >>> C1().test_2
        'p'
        """
        return "p"

    @cached_property
    def test_3(self):
        """
        >>> C1().test_3
        'cp'
        """
        return "cp"

    @cache
    def test_4(self):
        """
        >>> C1().test_4()
        'c'
        """
        return "c"


doctest.testmod()

test_3 is a function decorated by @cached_property. It has a doctest. but that was not executed.

$ python3 a.py -v
Trying:
    C1().test_1()
Expecting:
    'f'
ok
Trying:
    C1().test_2
Expecting:
    'p'
ok
Trying:
    C1().test_4()
Expecting:
    'c'
ok
2 items had no tests:
    __main__
    __main__.C1
3 items passed all tests:
   1 tests in __main__.C1.test_1
   1 tests in __main__.C1.test_2
   1 tests in __main__.C1.test_4
3 tests in 5 items.
3 passed and 0 failed.
Test passed.

How can I run test_3 doctest?

Environment

$ python3 --version
Python 3.9.6

$ uname
Darwin

Solution

  • The problem is likely caused by a difference in the implementation of cache and cached_property. Namely, cache sets __module__ whereas cached_property does not:

    >>> from functools import cache, cached_property
    >>> @cache
    ... def f(): pass
    ...
    >>> @cached_property
    ... def g(): pass
    ...
    >>> f.__module__
    '__main__'
    >>> g.__module__
    'functools'
    

    Functions that are not defined in the current __module__ are ignored by doctesting. This is intentional since otherwise all the doctests of the methods that you import at the top of the file would run. However, in this case, this seems like a bug to me.

    One can explicitly add a method (or class) to the doctests for a module by adding it to __test__, so in your case this should do the trick:

    __test__= { "C1.test_3": C1.test_3 }
    

    To fix this in Python, one should probably add self.__module__ = func.__module__ to this initializer and maybe all the others that update_wrapper() sets for @cache. But maybe this has unintended side effects and that's why this was not set in the first place.