Python doctest with abstractmethod

huangapple go评论99阅读模式
英文:

Python doctest with abstractmethod

问题

# main.py
import abc


class A(abc.ABC):

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

    @classmethod
    @property
    def bar(cls) -> int:
        """
        >>> B.bar
        1
        >>> C.bar
        2
        """
        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 main.py
# use_method.py
import abc


class A(abc.ABC):

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

    @classmethod
    def bar(cls) -> int:
        """
        >>> B.bar()
        1
        >>> C.bar()
        2
        """
        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
英文:

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.

  • _foo: an abstact class method
  • bar: a class property which behaves differently based on foo inside.
# 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.

答案1

得分: 0

您正在使用@classmethod来包装@property。这个功能事实证明是一个设计错误,导致了很多奇怪的问题,包括这个问题。由于引起的问题,这个功能在3.11版本中被移除了。

您应该重新设计您的类,停止使用@classmethod@property。这不仅仅是一个测试问题 - 如果您继续使用@classmethod@property,您将无法支持Python 3.11。

如果您真的想保持相同的API,您可以使用一个自定义的@classproperty装饰器:

class classproperty:
    def __init__(self, func):
        self.func = func
    def __get__(self, instance, owner):
        return self.func(owner)
    # 不支持设置器 - 描述符设置器在类上不起作用
英文:

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

huangapple
  • 本文由 发表于 2023年5月21日 11:17:41
  • 转载请务必保留本文链接:https://go.coder-hub.com/76298158.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定