diff --git a/pdoc/__init__.py b/pdoc/__init__.py index 0f05b2c0..6dd73e8e 100644 --- a/pdoc/__init__.py +++ b/pdoc/__init__.py @@ -712,7 +712,15 @@ def is_from_this_module(obj): if _is_function(obj): self.doc[name] = Function(name, self, obj) elif inspect.isclass(obj): - self.doc[name] = Class(name, self, obj) + cl = Class(name, self, obj) + self.doc[name] = cl + # Also add all nested classes of the class just found to the + # module context, otherwise some classes will be recognized + # as "External" even though they were correctly recognized + # as "Class" during an earlier scanning process + # (=> Module.find_ident()). + for ncl in cl._nested_classes(): + self.doc[ncl.name] = ncl elif name in var_docstrings: self.doc[name] = Variable(name, self, var_docstrings[name], obj=obj) @@ -945,7 +953,7 @@ def variables(self, sort=True) -> List['Variable']: def classes(self, sort=True) -> List['Class']: """ - Returns all documented module-level classes in the module, + Returns all documented classes in the module, optionally sorted alphabetically, as a list of `pdoc.Class`. """ return self._filter_doc_objs(Class, sort) @@ -1005,9 +1013,10 @@ class Class(Doc): """ Representation of a class' documentation. """ - __slots__ = ('doc', '_super_members') + __slots__ = ('doc', 'cls', '_super_members') - def __init__(self, name: str, module: Module, obj, *, docstring: str = None): + def __init__(self, name: str, module: Module, obj, *, docstring: str = None, + cls: 'Class' = None): assert inspect.isclass(obj) if docstring is None: @@ -1018,7 +1027,13 @@ def __init__(self, name: str, module: Module, obj, *, docstring: str = None): super().__init__(name, module, obj, docstring=docstring) - self.doc: Dict[str, Union[Function, Variable]] = {} + self.cls = cls + """ + The `pdoc.Class` object if this class is defined in a class. If not, + this is None. + """ + + self.doc: Dict[str, Union[Function, Variable, Class]] = {} """A mapping from identifier name to a `pdoc.Doc` objects.""" # Annotations for filtering. @@ -1063,6 +1078,13 @@ def definition_order_index( if _is_function(obj): self.doc[name] = Function( name, self.module, obj, cls=self) + elif inspect.isclass(obj): + self.doc[name] = Class( + self.name + "." + name, + self.module, + obj, + cls=self + ) else: self.doc[name] = Variable( name, self.module, @@ -1158,6 +1180,14 @@ def _filter_doc_objs(self, type: Type[T], include_inherited=True, if (include_inherited or not obj.inherits) and filter_func(obj)] return sorted(result) if sort else result + def classes(self, include_inherited=True, sort=True): + """Returns the classes immediately nested in this class.""" + return self._filter_doc_objs( + Class, + include_inherited=include_inherited, + sort=sort + ) + def class_variables(self, include_inherited=True, sort=True) -> List['Variable']: """ Returns an optionally-sorted list of `pdoc.Variable` objects that @@ -1195,6 +1225,19 @@ def functions(self, include_inherited=True, sort=True) -> List['Function']: Function, include_inherited, lambda dobj: not dobj.is_method, sort) + def _nested_classes(self, include_inherited=True, sort=True) -> List['Class']: + """ + Returns an optionally-sorted list of `pdoc.Class` objects that + represent this class' nested classes. + """ + stack = self.classes(sort)[::-1] + results = [] + while stack: + c = stack.pop() + results.append(c) + stack.extend(c.classes(sort=sort)[::-1]) + return results + def inherited_members(self) -> List[Tuple['Class', List[Doc]]]: """ Returns all inherited members as a list of tuples @@ -1204,7 +1247,7 @@ def inherited_members(self) -> List[Tuple['Class', List[Doc]]]: return sorted(((cast(Class, k), sorted(g)) for k, g in groupby((i.inherits for i in self.doc.values() if i.inherits), - key=lambda i: i.cls)), # type: ignore + key=lambda i: i.cls)), key=lambda x, _mro_index=self.mro().index: _mro_index(x[0])) # type: ignore def _fill_inheritance(self): diff --git a/pdoc/test/__init__.py b/pdoc/test/__init__.py index c98d0c8e..1c2f818f 100644 --- a/pdoc/test/__init__.py +++ b/pdoc/test/__init__.py @@ -3,6 +3,7 @@ """ import doctest import enum +import importlib import inspect import os import shutil @@ -721,7 +722,7 @@ def test__all__(self): mod = pdoc.Module(module) with self.assertWarns(UserWarning): # Only B is used but __pdoc__ contains others pdoc.link_inheritance() - self.assertEqual(list(mod.doc.keys()), ['B']) + self.assertEqual(list(mod.doc.keys()), ['B', 'B.C']) def test_find_ident(self): mod = pdoc.Module(EXAMPLE_MODULE) @@ -1026,6 +1027,63 @@ class G(F[int]): self.assertEqual(pdoc.Class('F', mod, F).docstring, """baz\n\nbar""") self.assertEqual(pdoc.Class('G', mod, G).docstring, """foo\n\nbar""") + def test_nested_classes(self): + m_name = 'M' + m_spec = importlib.util.spec_from_loader(m_name, loader=None) + m_module = importlib.util.module_from_spec(m_spec) + code = ''' +class A: + """Class A documentation""" + x: str = '' + class B: + """ Class A.B documentation""" + class C: + """ Class A.B.C documentation""" + pass + +class D(A): + pass + +class E(A.B): + pass +''' + exec(code, m_module.__dict__) + + sys.modules[m_name] = m_module + + try: + mod = pdoc.Module('M') + pdoc.link_inheritance() + + pdoc_A = mod.find_ident('A') + pdoc_B = mod.find_ident('A.B') + pdoc_C = mod.find_ident('A.B.C') + pdoc_D = mod.find_ident('D') + pdoc_E = mod.find_ident('E') + + self.assertEqual([c.qualname for c in mod.classes()], ['A', 'A.B', 'A.B.C', 'D', 'E']) + self.assertEqual([c.qualname for c in pdoc_A.classes()], ['A.B']) + self.assertEqual([c.qualname for c in pdoc_B.classes()], ['A.B.C']) + self.assertEqual([c.qualname for c in pdoc_C.classes()], []) + + self.assertEqual(pdoc_A.docstring, "Class A documentation") + self.assertEqual(pdoc_B.docstring, "Class A.B documentation") + self.assertEqual(pdoc_C.docstring, "Class A.B.C documentation") + + # D inherits doc from A + self.assertEqual([c.qualname for c in pdoc_D.mro()], ['A']) + self.assertEqual(pdoc_D.docstring, "Class A documentation") + self.assertEqual([c.qualname for c in pdoc_D.classes(include_inherited=False)], []) + self.assertEqual([c.qualname for c in pdoc_D.classes()], ['A.B']) + + # E inherits doc from A.B + self.assertEqual([c.qualname for c in pdoc_E.mro()], ['A.B']) + self.assertEqual(pdoc_E.docstring, "Class A.B documentation") + self.assertEqual([c.qualname for c in pdoc_E.classes(include_inherited=False)], []) + self.assertEqual([c.qualname for c in pdoc_E.classes()], ['A.B.C']) + finally: + del sys.modules[m_name] + @ignore_warnings def test_Class_params(self): class C: