Skip to content

Commit 8e4f120

Browse files
committed
TST: Add tests covering the inspect.unwrap() problem (#463)
1 parent f8704ec commit 8e4f120

File tree

4 files changed

+132
-0
lines changed

4 files changed

+132
-0
lines changed

pdoc/test/__init__.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,47 @@ def test__pdoc__dict(self):
659659
self.assertEqual(cm, [])
660660
self.assertNotIn('downloaded_modules', mod.doc)
661661

662+
# flake8: noqa: E501 line too long
663+
def test_class_wrappers(self):
664+
"""
665+
Check that decorated classes are unwrapped properly.
666+
Details: https://github.com/pdoc3/pdoc/issues/463
667+
"""
668+
669+
module_name = f'{EXAMPLE_MODULE}._test_classwrap'
670+
671+
root_module = pdoc.Module(module_name, context=pdoc.Context())
672+
root_wrapped_cls_parent = root_module.doc['DecoratedClassParent']
673+
root_wrapped_cls_child = root_module.doc['DecoratedClassChild']
674+
675+
module_classdef = root_module.doc['class_definition']
676+
module_classdef_cls_parent = module_classdef.doc['DecoratedClassParent']
677+
module_classdef_cls_child = module_classdef.doc['DecoratedClassChild']
678+
679+
module_util = root_module.doc['util']
680+
module_util_decorator = module_util.doc['decorate_class']
681+
682+
self.assertEqual(root_module.qualname, module_name)
683+
self.assertEqual(root_wrapped_cls_parent.qualname, 'DecoratedClassParent')
684+
self.assertEqual(root_wrapped_cls_parent.docstring,
685+
"""This is `DecoratedClassParent` class.""")
686+
self.assertEqual(root_wrapped_cls_child.qualname, 'DecoratedClassChild')
687+
self.assertEqual(root_wrapped_cls_child.docstring,
688+
"""This is an `DecoratedClassParent`'s implementation that always returns 1.""")
689+
690+
self.assertEqual(module_classdef.qualname, f'{module_name}.class_definition')
691+
self.assertEqual(module_classdef_cls_parent.qualname, 'DecoratedClassParent')
692+
self.assertEqual(module_classdef_cls_parent.docstring,
693+
"""This is `DecoratedClassParent` class.""")
694+
self.assertEqual(module_classdef_cls_child.qualname, 'DecoratedClassChild')
695+
self.assertEqual(module_classdef_cls_child.docstring,
696+
"""This is an `DecoratedClassParent`'s implementation that always returns 1.""")
697+
698+
self.assertEqual(module_util.qualname, f'{module_name}.util')
699+
self.assertEqual(module_util_decorator.qualname, 'decorate_class')
700+
701+
pdoc.link_inheritance(root_module._context)
702+
662703
@ignore_warnings
663704
def test_dont_touch__pdoc__blacklisted(self):
664705
class Bomb:
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""
2+
This is the root module.
3+
4+
This re-exports `DecoratedClassParent` and `DecoratedClassChild`.
5+
See `pdoc.test.example_pkg._test_classwrap.class_definition.DecoratedClassParent`
6+
and `pdoc.test.example_pkg._test_classwrap.class_definition.DecoratedClassChild` for more details.
7+
"""
8+
9+
10+
from .class_definition import DecoratedClassParent, DecoratedClassChild
11+
12+
13+
__all__ = [
14+
'DecoratedClassParent',
15+
'DecoratedClassChild',
16+
]
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""
2+
This module exports the following classes:
3+
4+
* `DecoratedClassParent`
5+
* `DecoratedClassChild`
6+
"""
7+
8+
from .util import decorate_class
9+
from abc import ABC, abstractmethod
10+
11+
12+
@decorate_class
13+
class DecoratedClassParent(ABC):
14+
""" This is `DecoratedClassParent` class. """
15+
16+
@abstractmethod
17+
def __value__(self) -> int:
18+
""" An `DecoratedClassParent`'s value implementation, abstract method. """
19+
raise NotImplementedError
20+
21+
@property
22+
def value(self) -> int:
23+
""" This is `DecoratedClassParent`'s property. """
24+
return self.__value__()
25+
26+
27+
@decorate_class
28+
class DecoratedClassChild(DecoratedClassParent):
29+
""" This is an `DecoratedClassParent`'s implementation that always returns 1. """
30+
31+
def __value__(self) -> int:
32+
return 1
33+
34+
35+
__all__ = [
36+
'DecoratedClassParent',
37+
'DecoratedClassChild',
38+
]
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import functools
2+
import types
3+
from typing import Type, TypeVar, cast
4+
5+
6+
C = TypeVar('C')
7+
8+
9+
def wrap_first(cls: Type[C]) -> Type[C]:
10+
wrapped = types.new_class(cls.__name__, (cls, ), {})
11+
wrapped = functools.update_wrapper(wrapped, cls, updated=())
12+
wrapped = cast(Type[C], wrapped)
13+
14+
return wrapped
15+
16+
17+
def wrap_second(cls: Type[C]) -> Type[C]:
18+
wrapped = type(cls.__name__, cls.__mro__, dict(cls.__dict__))
19+
wrapped = functools.update_wrapper(wrapped, cls, updated=())
20+
wrapped = cast(Type[C], wrapped)
21+
22+
return wrapped
23+
24+
25+
def decorate_class(cls: Type[C]) -> Type[C]:
26+
""" Creates a two-step class decoration. """
27+
28+
wrapped = wrap_first(cls)
29+
wrapped_again = wrap_second(wrapped)
30+
wrapped_again.__decorated__ = True
31+
32+
return wrapped_again
33+
34+
35+
__all__ = [
36+
'decorate_class',
37+
]

0 commit comments

Comments
 (0)