pythonabstract-classdoctest

Python doctest with abstractmethod


I want to implement a class property that behaves differently based on an abstract class method in Python and have its doctest. But running doctest gives an error that I want to avoid.

Here is a minimal example of what I want to accomplish.

# main.py
import abc


class A(abc.ABC):

    @classmethod
    @abc.abstractmethod
    def _foo(cls):
        pass

    @classmethod
    @property
    def bar(cls) -> int:
        return cls._foo()


class B(A):
    """
    >>> B.bar
    1
    """

    @classmethod
    def _foo(cls) -> int:
        return 1


class C(A):
    """
    >>> C.bar
    2
    """
    @classmethod
    def _foo(cls) -> int:
        return 2


if __name__ == '__main__':
    print(B.bar)
    print(C.bar)

Running the script by $python main.py works just fine. But running doctest throws an attribute error presumably occurred by A._foo().

$ python -m doctest main.py
Traceback (most recent call last):
  File ".../.pyenv/versions/3.9.6/lib/python3.9/runpy.py", line 197, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File ".../.pyenv/versions/3.9.6/lib/python3.9/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File ".../.pyenv/versions/3.9.6/lib/python3.9/doctest.py", line 2793, in <module>
    sys.exit(_test())
  File ".../.pyenv/versions/3.9.6/lib/python3.9/doctest.py", line 2783, in _test
    failures, _ = testmod(m, verbose=verbose, optionflags=options)
  File ".../.pyenv/versions/3.9.6/lib/python3.9/doctest.py", line 1955, in testmod
    for test in finder.find(m, name, globs=globs, extraglobs=extraglobs):
  File ".../.pyenv/versions/3.9.6/lib/python3.9/doctest.py", line 939, in find
    self._find(tests, obj, name, module, source_lines, globs, {})
  File ".../.pyenv/versions/3.9.6/lib/python3.9/doctest.py", line 1001, in _find
    self._find(tests, val, valname, module, source_lines,
  File ".../.pyenv/versions/3.9.6/lib/python3.9/doctest.py", line 1028, in _find
    val = getattr(obj, valname).__func__
AttributeError: 'NoneType' object has no attribute '__func__'

Are there ways to avoid this error?

Using method instead of property seems to work fine.

# use_method.py
import abc


class A(abc.ABC):

    @classmethod
    @abc.abstractmethod
    def _foo(cls):
        pass

    @classmethod
    def bar(cls) -> int:
        return cls._foo()


class B(A):
    """
    >>> B.bar()
    1
    """

    @classmethod
    def _foo(cls) -> int:
        return 1


class C(A):
    """
    >>> C.bar()
    2
    """
    @classmethod
    def _foo(cls) -> int:
        return 2


if __name__ == '__main__':
    print(B.bar())
    print(C.bar())
$ python -m doctest -v use_method.py
Trying:
    B.bar()
Expecting:
    1
ok
Trying:
    C.bar()
Expecting:
    2
ok
6 items had no tests:
    use_method
    use_method.A
    use_method.A._foo
    use_method.A.bar
    use_method.B._foo
    use_method.C._foo
2 items passed all tests:
   1 tests in use_method.B
   1 tests in use_method.C
2 tests in 8 items.
2 passed and 0 failed.
Test passed.

But it is not my preference to change API and force users to edit their code only because of test.


Solution

  • You're using @classmethod to wrap a @property. That functionality turned out to be a design mistake that caused a lot of weird problems, including this problem. The feature was removed in 3.11 due to all the problems it caused.

    You should redesign your class to stop using @classmethod with @property. This goes beyond a test issue - if you keep using @classmethod with @property, you will be unable to support Python 3.11.

    If you really want to keep the same API, you can use a custom @classproperty decorator:

    class classproperty:
        def __init__(self, func):
            self.func = func
        def __get__(self, instance, owner):
            return self.func(owner)
        # No setter support - descriptor setters don't work on the class