Skip to content

Commit 742c7dd

Browse files
ryssonkernc
authored andcommitted
FIX: Handle ro-value-descrptiors (#76) (#77)
* FIX: Handle ro-value-descrptiors (#76) * FIX: Handle read-only value descriptors. * 'FIX: ro-value-descrptiors as instance var(#76)' * FIX: unittests for ro-value-descrptiors (#76) * FIX: separate section for ro-value-descrptiors (#76) * FIX: unittest for ro-value-descrptiors (#76) * update, simplify by moving Variable creation outside PEP 224 docstrings collection function. * silence mypy
1 parent 43bd6ff commit 742c7dd

File tree

3 files changed

+85
-60
lines changed

3 files changed

+85
-60
lines changed

pdoc/__init__.py

Lines changed: 51 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -199,56 +199,46 @@ def _pairwise(iterable):
199199
return zip(a, b)
200200

201201

202-
def _var_docstrings(doc_obj: Union['Module', 'Class'], *,
203-
_init_tree: ast.FunctionDef = None) -> Dict[str, 'Variable']:
202+
def _pep224_docstrings(doc_obj: Union['Module', 'Class'], *,
203+
_init_tree=None) -> Tuple[Dict[str, str],
204+
Dict[str, str]]:
204205
"""
205-
Extracts docstrings for variables of `doc_obj`
206+
Extracts PEP-224 docstrings for variables of `doc_obj`
206207
(either a `pdoc.Module` or `pdoc.Class`).
207208
208-
Returns a dict mapping variable names to `pdoc.Variable` objects.
209-
210-
For `pdoc.Class` objects, the dict contains class' instance
211-
variables (defined as `self.something` in class' `__init__`),
212-
recognized by `Variable.instance_var == True`.
209+
Returns a tuple of two dicts mapping variable names to their docstrings.
210+
The second dict contains instance variables and is non-empty only in case
211+
`doc_obj` is a `pdoc.Class` which has `__init__` method.
213212
"""
213+
# No variables in namespace packages
214+
if isinstance(doc_obj, Module) and doc_obj.is_namespace:
215+
return {}, {}
216+
217+
vars = {} # type: Dict[str, str]
218+
instance_vars = {} # type: Dict[str, str]
219+
214220
if _init_tree:
215-
tree = _init_tree # type: Union[ast.Module, ast.FunctionDef]
221+
tree = _init_tree
216222
else:
217-
# No variables in namespace packages
218-
if isinstance(doc_obj, Module) and doc_obj.is_namespace:
219-
return {}
220223
try:
221224
tree = ast.parse(inspect.getsource(doc_obj.obj))
222225
except (OSError, TypeError, SyntaxError):
223226
warn("Couldn't get/parse source of '{!r}'".format(doc_obj))
224-
return {}
225-
if isinstance(doc_obj, Class):
226-
tree = tree.body[0] # type: ignore # ast.parse creates a dummy ast.Module wrapper
227-
228-
vs = {} # type: Dict[str, Variable]
229-
230-
cls = None
231-
module = doc_obj
232-
module_all = set(getattr(module.obj, '__all__', ()))
233-
member_obj = dict(inspect.getmembers(doc_obj.obj)).get
227+
return {}, {}
234228

235-
if isinstance(doc_obj, Class):
236-
cls = doc_obj
237-
module = doc_obj.module
229+
if isinstance(doc_obj, Class):
230+
tree = tree.body[0] # ast.parse creates a dummy ast.Module wrapper
238231

239-
# For classes, first add instance variables defined in __init__
240-
if not _init_tree:
241-
# Recursive call with just the __init__ tree
232+
# For classes, maybe add instance variables defined in __init__
242233
for node in tree.body:
243234
if isinstance(node, ast.FunctionDef) and node.name == '__init__':
244-
vs.update(_var_docstrings(doc_obj, _init_tree=node))
235+
instance_vars, _ = _pep224_docstrings(doc_obj, _init_tree=node)
245236
break
246237

247238
try:
248239
ast_AnnAssign = ast.AnnAssign # type: Type
249240
except AttributeError: # Python < 3.6
250241
ast_AnnAssign = type(None)
251-
252242
ast_Assignments = (ast.Assign, ast_AnnAssign)
253243

254244
for assign_node, str_node in _pairwise(ast.iter_child_nodes(tree)):
@@ -275,20 +265,13 @@ def _var_docstrings(doc_obj: Union['Module', 'Class'], *,
275265
else:
276266
continue
277267

278-
if not _is_public(name):
279-
continue
280-
281-
if module_all and name not in module_all:
282-
continue
283-
284268
docstring = inspect.cleandoc(str_node.value.s).strip()
285269
if not docstring:
286270
continue
287271

288-
vs[name] = Variable(name, module, docstring,
289-
obj=member_obj(name),
290-
cls=cls, instance_var=bool(_init_tree))
291-
return vs
272+
vars[name] = docstring
273+
274+
return vars, instance_vars
292275

293276

294277
def _is_public(ident_name):
@@ -299,6 +282,10 @@ def _is_public(ident_name):
299282
return not ident_name.startswith("_")
300283

301284

285+
def _is_function(obj):
286+
return inspect.isroutine(obj) and callable(obj)
287+
288+
302289
def _filter_type(type: Type[T],
303290
values: Union[Iterable['Doc'], Dict[str, 'Doc']]) -> List[T]:
304291
"""
@@ -541,6 +528,8 @@ def __init__(self, module: Union[ModuleType, str], *, docfilter: Callable[[Doc],
541528
self._is_inheritance_linked = False
542529
"""Re-entry guard for `pdoc.Module._link_inheritance()`."""
543530

531+
var_docstrings, _ = _pep224_docstrings(self)
532+
544533
# Populate self.doc with this module's public members
545534
if hasattr(self.obj, '__all__'):
546535
public_objs = []
@@ -558,16 +547,17 @@ def is_from_this_module(obj):
558547
public_objs = [(name, inspect.unwrap(obj))
559548
for name, obj in inspect.getmembers(self.obj)
560549
if (_is_public(name) and
561-
is_from_this_module(obj))]
550+
(is_from_this_module(obj) or name in var_docstrings))]
562551
index = list(self.obj.__dict__).index
563552
public_objs.sort(key=lambda i: index(i[0]))
553+
564554
for name, obj in public_objs:
565-
if inspect.isroutine(obj):
555+
if _is_function(obj):
566556
self.doc[name] = Function(name, self, obj)
567557
elif inspect.isclass(obj):
568558
self.doc[name] = Class(name, self, obj)
569-
570-
self.doc.update(_var_docstrings(self))
559+
elif name in var_docstrings:
560+
self.doc[name] = Variable(name, self, var_docstrings[name], obj=obj)
571561

572562
# If the module is a package, scan the directory for submodules
573563
if self.is_package:
@@ -804,8 +794,6 @@ def __init__(self, name, module, obj, *, docstring=None):
804794
self.doc = {}
805795
"""A mapping from identifier name to a `pdoc.Doc` objects."""
806796

807-
self.doc.update(_var_docstrings(self))
808-
809797
public_objs = [(name, inspect.unwrap(obj))
810798
for name, obj in inspect.getmembers(self.obj)
811799
# Filter only *own* members. The rest are inherited
@@ -814,27 +802,30 @@ def __init__(self, name, module, obj, *, docstring=None):
814802
index = list(self.obj.__dict__).index
815803
public_objs.sort(key=lambda i: index(i[0]))
816804

805+
var_docstrings, instance_var_docstrings = _pep224_docstrings(self)
806+
817807
# Convert the public Python objects to documentation objects.
818808
for name, obj in public_objs:
819-
if name in self.doc and self.doc[name].docstring:
820-
continue
821-
if inspect.isroutine(obj):
809+
if _is_function(obj):
822810
self.doc[name] = Function(
823811
name, self.module, obj, cls=self,
824812
method=not self._method_type(self.obj, name))
825-
elif (inspect.isdatadescriptor(obj) or
826-
inspect.isgetsetdescriptor(obj) or
827-
inspect.ismemberdescriptor(obj)):
828-
self.doc[name] = Variable(
829-
name, self.module, inspect.getdoc(obj),
830-
obj=getattr(obj, 'fget', obj),
831-
cls=self, instance_var=True)
832813
else:
833814
self.doc[name] = Variable(
834815
name, self.module,
835-
docstring=isinstance(obj, type) and inspect.getdoc(obj) or "",
836-
cls=self,
837-
instance_var=name in getattr(self.obj, "__slots__", ()))
816+
docstring=var_docstrings.get(name) or inspect.getdoc(obj), cls=self,
817+
obj=getattr(obj, 'fget', getattr(obj, '__get__', obj)),
818+
instance_var=(inspect.isdatadescriptor(obj) or
819+
inspect.ismethoddescriptor(obj) or
820+
inspect.isgetsetdescriptor(obj) or
821+
inspect.ismemberdescriptor(obj) or
822+
name in getattr(self.obj, '__slots__', ())))
823+
824+
for name, docstring in instance_var_docstrings.items():
825+
self.doc[name] = Variable(
826+
name, self.module, docstring, cls=self,
827+
obj=getattr(self.obj, name, None),
828+
instance_var=True)
838829

839830
@staticmethod
840831
def _method_type(cls: type, name: str):
@@ -1024,7 +1015,7 @@ def __init__(self, name, module, obj, *, cls: Class = None, method=False):
10241015
`method` should be `True` when the function is a method. In
10251016
all other cases, it should be `False`.
10261017
"""
1027-
assert callable(obj)
1018+
assert callable(obj), (name, module, obj)
10281019
super().__init__(name, module, obj)
10291020

10301021
self.cls = cls

pdoc/test/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ def test_html(self):
166166
'<object ',
167167
' class="ident">_private',
168168
' class="ident">_Private',
169+
'non_callable_routine',
169170
]
170171
package_files = {
171172
'': self.PUBLIC_FILES,
@@ -301,6 +302,7 @@ def test_text(self):
301302
'_Private',
302303
'subprocess',
303304
'Hidden',
305+
'non_callable_routine',
304306
]
305307

306308
with self.subTest(package=EXAMPLE_MODULE):
@@ -418,6 +420,21 @@ def test_instance_var(self):
418420
var = mod.doc['B'].doc['instance_var']
419421
self.assertTrue(var.instance_var)
420422

423+
def test_readonly_value_descriptors(self):
424+
pdoc.reset()
425+
mod = pdoc.Module(pdoc.import_module(EXAMPLE_MODULE))
426+
var = mod.doc['B'].doc['ro_value_descriptor']
427+
self.assertIsInstance(var, pdoc.Variable)
428+
self.assertTrue(var.instance_var)
429+
self.assertEqual(var.docstring, """ro_value_descriptor docstring""")
430+
self.assertTrue(var.source)
431+
432+
var = mod.doc['B'].doc['ro_value_descriptor_no_doc']
433+
self.assertIsInstance(var, pdoc.Variable)
434+
self.assertTrue(var.instance_var)
435+
self.assertEqual(var.docstring, """Read-only value descriptor""")
436+
self.assertTrue(var.source)
437+
421438
def test_builtin_methoddescriptors(self):
422439
import parser
423440
with self.assertWarns(UserWarning):

pdoc/test/example_pkg/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ def inherited(self): # Inherited in B
3636
"""A.inherited docstring"""
3737

3838

39+
non_callable_routine = staticmethod(lambda x: 2) # Not interpreted as Function; skipped
40+
41+
42+
class ReadOnlyValueDescriptor:
43+
"""Read-only value descriptor"""
44+
45+
def __get__(self, instance, instance_type=None):
46+
if instance is not None:
47+
return instance.var ** 2
48+
return self
49+
50+
3951
class B(A, int):
4052
"""
4153
B docstring
@@ -49,6 +61,11 @@ class B(A, int):
4961
var = 3
5062
"""B.var docstring"""
5163

64+
ro_value_descriptor = ReadOnlyValueDescriptor()
65+
"""ro_value_descriptor docstring"""
66+
67+
ro_value_descriptor_no_doc = ReadOnlyValueDescriptor() # no doc-string
68+
5269
def __init__(self, x, y, z, w):
5370
"""`__init__` docstring"""
5471
self.instance_var = None

0 commit comments

Comments
 (0)