Skip to content

Commit 9bdb076

Browse files
committed
BUG: Avoid as much as touching __pdoc__-blacklisted objects
Fixes #285
1 parent 3f15cfa commit 9bdb076

File tree

2 files changed

+60
-7
lines changed

2 files changed

+60
-7
lines changed

pdoc/__init__.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,15 @@
6666
tpl_lookup.directories.insert(0, path.join(os.getenv("XDG_CONFIG_HOME", ''), "pdoc"))
6767

6868

69+
# A surrogate so that the check in Module._link_inheritance()
70+
# "__pdoc__-overriden key {!r} does not exist" can pick the object up
71+
# (and not warn).
72+
# If you know how to keep the warning, but skip the object creation
73+
# altogether, please make it happen!
74+
class _BLACKLISTED_DUMMY:
75+
pass
76+
77+
6978
class Context(dict):
7079
"""
7180
The context object that maps all documented identifiers
@@ -90,7 +99,9 @@ def reset():
9099
_global_context.clear()
91100

92101
# Clear LRU caches
93-
for func in (_get_type_hints,):
102+
for func in (_get_type_hints,
103+
_is_blacklisted,
104+
_is_whitelisted):
94105
func.cache_clear()
95106
for cls in (Doc, Module, Class, Function, Variable, External):
96107
for _, method in inspect.getmembers(cls):
@@ -318,6 +329,7 @@ def _pep224_docstrings(doc_obj: Union['Module', 'Class'], *,
318329
return vars, instance_vars
319330

320331

332+
@lru_cache()
321333
def _is_whitelisted(name: str, doc_obj: Union['Module', 'Class']):
322334
"""
323335
Returns `True` if `name` (relative or absolute refname) is
@@ -333,6 +345,7 @@ def _is_whitelisted(name: str, doc_obj: Union['Module', 'Class']):
333345
return False
334346

335347

348+
@lru_cache()
336349
def _is_blacklisted(name: str, doc_obj: Union['Module', 'Class']):
337350
"""
338351
Returns `True` if `name` (relative or absolute refname) is
@@ -624,24 +637,32 @@ def __init__(self, module: Union[ModuleType, str], *, docfilter: Callable[[Doc],
624637
public_objs = []
625638
for name in self.obj.__all__:
626639
try:
627-
public_objs.append((name, getattr(self.obj, name)))
640+
obj = getattr(self.obj, name)
628641
except AttributeError:
629642
warn("Module {!r} doesn't contain identifier `{}` "
630643
"exported in `__all__`".format(self.module, name))
644+
if not _is_blacklisted(name, self):
645+
obj = inspect.unwrap(obj)
646+
public_objs.append((name, obj))
631647
else:
632648
def is_from_this_module(obj):
633649
mod = inspect.getmodule(inspect.unwrap(obj))
634650
return mod is None or mod.__name__ == self.obj.__name__
635651

636-
public_objs = [(name, inspect.unwrap(obj))
652+
public_objs = [(name, (_BLACKLISTED_DUMMY
653+
if _is_blacklisted(name, self) else
654+
inspect.unwrap(obj)))
637655
for name, obj in inspect.getmembers(self.obj)
638656
if ((_is_public(name) or _is_whitelisted(name, self)) and
639-
(is_from_this_module(obj) or name in var_docstrings))]
657+
(_is_blacklisted(name, self) or # skips unwrapping that follows
658+
is_from_this_module(obj) or name in var_docstrings))]
640659
index = list(self.obj.__dict__).index
641660
public_objs.sort(key=lambda i: index(i[0]))
642661

643662
for name, obj in public_objs:
644-
if _is_function(obj):
663+
if obj is _BLACKLISTED_DUMMY:
664+
self.doc[name] = Variable(name, self, 'dummy', obj=obj)
665+
elif _is_function(obj):
645666
self.doc[name] = Function(name, self, obj)
646667
elif inspect.isclass(obj):
647668
self.doc[name] = Class(name, self, obj)
@@ -955,7 +976,9 @@ def __init__(self, name, module, obj, *, docstring=None):
955976
# Use only own, non-inherited annotations (the rest will be inherited)
956977
annotations = getattr(self.obj, '__annotations__', {})
957978

958-
public_objs = [(_name, inspect.unwrap(obj))
979+
public_objs = [(_name, (_BLACKLISTED_DUMMY
980+
if _is_blacklisted(_name, self) else
981+
inspect.unwrap(obj)))
959982
for _name, obj in _getmembers_all(self.obj)
960983
# Filter only *own* members. The rest are inherited
961984
# in Class._fill_inheritance()
@@ -981,7 +1004,9 @@ def definition_order_index(
9811004

9821005
# Convert the public Python objects to documentation objects.
9831006
for name, obj in public_objs:
984-
if _is_function(obj):
1007+
if obj is _BLACKLISTED_DUMMY:
1008+
self.doc[name] = Variable(name, self.module, 'dummy', obj=obj, cls=self)
1009+
elif _is_function(obj):
9851010
self.doc[name] = Function(
9861011
name, self.module, obj, cls=self)
9871012
else:

pdoc/test/__init__.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from random import randint
2020
from tempfile import TemporaryDirectory
2121
from time import sleep
22+
from types import ModuleType
2223
from unittest.mock import patch
2324
from urllib.error import HTTPError
2425
from urllib.request import Request, urlopen
@@ -35,6 +36,9 @@
3536
TESTS_BASEDIR = os.path.abspath(os.path.dirname(__file__) or '.')
3637
EXAMPLE_MODULE = 'example_pkg'
3738

39+
EMPTY_MODULE = ModuleType('empty')
40+
EMPTY_MODULE.__pdoc__ = {}
41+
3842
sys.path.insert(0, TESTS_BASEDIR)
3943

4044
T = typing.TypeVar("T")
@@ -608,6 +612,30 @@ def test__pdoc__dict(self):
608612
self.assertEqual(cm, [])
609613
self.assertNotIn('downloaded_modules', mod.doc)
610614

615+
@ignore_warnings
616+
def test_dont_touch__pdoc__blacklisted(self):
617+
class Bomb:
618+
def __getattribute__(self, item):
619+
raise RuntimeError
620+
621+
class D:
622+
x = Bomb()
623+
"""doc"""
624+
__qualname__ = 'D'
625+
626+
module = EMPTY_MODULE
627+
D.__module__ = module.__name__ # Need to match is_from_this_module check
628+
with patch.object(module, 'x', Bomb(), create=True), \
629+
patch.object(module, '__pdoc__', {'x': False}):
630+
mod = pdoc.Module(module)
631+
pdoc.link_inheritance()
632+
self.assertNotIn('x', mod.doc)
633+
with patch.object(module, 'D', D, create=True), \
634+
patch.object(module, '__pdoc__', {'D.x': False}):
635+
mod = pdoc.Module(module)
636+
pdoc.link_inheritance()
637+
self.assertNotIn('x', mod.doc['D'].doc)
638+
611639
def test__pdoc__invalid_value(self):
612640
module = pdoc.import_module(EXAMPLE_MODULE)
613641
with patch.object(module, '__pdoc__', {'B': 1}), \

0 commit comments

Comments
 (0)