diff --git a/doc/usage/extensions/autosummary.rst b/doc/usage/extensions/autosummary.rst index 0a25d8dbd21..2a0dee02080 100644 --- a/doc/usage/extensions/autosummary.rst +++ b/doc/usage/extensions/autosummary.rst @@ -293,16 +293,106 @@ The following variables are available in the templates: .. data:: members - List containing names of all members of the module or class. Only available - for modules and classes. + List containing names of all members of the module or class including private + ones. Only available for modules and classes. .. data:: inherited_members - List containing names of all inherited members of class. Only available for - classes. + List containing names of all inherited members of class including private ones. + Only available for classes. .. versionadded:: 1.8.0 +.. data:: inherited_qualnames + + List containing the fully qualified names of each inherited member including + private ones. Will return just the closest parent from which this class was + inherited. Only available for classes. + + The following example assumes that this code block has been written as + part of a module ``mypackage.test`` + + .. code-block:: python + + class foo(): + foo_attr = "I'm an attribute" + def __init__(self): + print("object initialised") + + def do_something(self): + print("Foo something") + + def _i_am_private(self): + print("I'm a private method") + + class bar(foo): + def do_something_else(self): + print("Bar something") + + Some available parameters for the autosummary of class ``bar`` in the above + example are: + + * ``name`` returns ``'bar'`` + * ``objname`` returns ``'bar'`` + * ``fullname`` returns ``'mypackage.test.bar'`` + * ``methods`` returns ``['__init__', 'do_something', 'do_something_else']`` + * ``attributes`` returns ``['foo_attr']`` + * ``members`` returns a list with many built in methods/attributes and + ``['__init__', '_i_am_private', 'do_something', 'do_something_else', + 'foo_attr']`` + * ``inherited_members`` returns a list with many built in methods/attributes + and ``['__init__', '_i_am_private','do_something', 'foo_attr']`` + * ``inherited_qualnames`` returns a list with many built in methods/ + attributes and ``['mypackage.test.foo.__init__', + 'mypackage.test.foo._i_am_private', 'mypackage.test.foo.do_something', + 'mypackage.test.foo.foo_attr']`` + * ``inherited_methods`` returns ``['mypackage.test.foo.__init__', + 'mypackage.test.foo.do_something']`` + * ``inherited_attributes`` returns ``['mypackage.test.foo.foo_attr']`` + + These parameters could then be used in a Sphinx template ``class.rst`` like + the following + + .. code-block:: RST + :dedent: 0 + + .. currentmodule:: {{ module }} + + .. autoclass:: {{ objname }} + :show-inheritance: + + .. rubric:: Methods + .. autosummary:: + :toctree: + :nosignatures: + {% for item in methods %} + {% if item not in inherited_members %} + {{objname}}.{{ item }} + {%- endif %} + {%- endfor %} + + .. rubric:: Inherited Methods + .. autosummary:: + {% for item in inherited_methods %} + {{item}} + {%- endfor %} + + .. versionadded:: 7.3.0 + +.. data:: inherited_methods + + List containing fully qualified names of "public" inherited methods only. + ``__init__`` is still included. Only available for classes. + + .. versionadded:: 7.3.0 + +.. data:: inherited_attributes + + List containing qualified names of "public" inherited attributes only. + Only available for classes. + + .. versionadded:: 7.3.0 + .. data:: functions List containing names of "public" functions in the module. Here, "public" diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index 5fbc51118bf..7d69037d67f 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -346,8 +346,14 @@ def generate_autosummary_content( ns['modules'] = imported_modules + modules ns['all_modules'] = all_imported_modules + all_modules elif doc.objtype == 'class': - ns['members'] = dir(obj) - ns['inherited_members'] = set(dir(obj)) - set(obj.__dict__.keys()) + ns['members'] = list(_get_class_members(obj)) + obj_classinfo = _get_class_members(obj, False) + obj_local = [j for j, k in obj_classinfo.items() if k == obj] + + ns['inherited_members'] = [ + x for x in dict.fromkeys(ns['members']) if x not in obj_local + ] + ns['methods'], ns['all_methods'] = _get_members( doc, app, obj, {'method'}, include_public={'__init__'} ) @@ -355,6 +361,29 @@ def generate_autosummary_content( doc, app, obj, {'attribute', 'property'} ) + method_string = [m.split('.')[-1] for m in ns['methods']] + attr_string = [m.split('.')[-1] for m in ns['attributes']] + + # Find the first link for this attribute in higher classes + ns['inherited_qualnames'] = [] + ns['inherited_methods'] = [] + ns['inherited_attributes'] = [] + + inherited_set = set(ns['inherited_members']) + for cl in obj.__mro__: + classinfo = _get_class_members(cl, False) + local = [j for j, k in classinfo.items() if k == cl] + + for i in local: + if i in inherited_set: + cl_str = f'{cl.__module__}.{cl.__name__}.{i}' + ns['inherited_qualnames'].append(cl_str) + if i in method_string: + ns['inherited_methods'].append(cl_str) + elif i in attr_string: + ns['inherited_attributes'].append(cl_str) + inherited_set.remove(i) + if modname is None or qualname is None: modname, qualname = _split_full_qualified_name(name) @@ -398,9 +427,12 @@ def _skip_member(app: Sphinx, obj: Any, name: str, objtype: str) -> bool: return False -def _get_class_members(obj: Any) -> dict[str, Any]: +def _get_class_members(obj: Any, return_object: bool = True) -> dict[str, Any]: members = sphinx.ext.autodoc.importer.get_class_members(obj, None, safe_getattr) - return {name: member.object for name, member in members.items()} + if return_object: + return {name: member.object for name, member in members.items()} + else: + return {name: member.class_ for name, member in members.items()} def _get_module_members(app: Sphinx, obj: Any) -> dict[str, Any]: diff --git a/tests/roots/test-ext-autosummary/autosummary_dummy_complex_inheritance_module.py b/tests/roots/test-ext-autosummary/autosummary_dummy_complex_inheritance_module.py new file mode 100644 index 00000000000..192038a2c21 --- /dev/null +++ b/tests/roots/test-ext-autosummary/autosummary_dummy_complex_inheritance_module.py @@ -0,0 +1,112 @@ +import sys + +class Parent: + + relation = "Family" + + def __init__(self): + self.name = "Andrew" + self.age = 35 + + def get_name(self): + return self.name + + def get_age(self): + return f"Parent class - {self.age}" + + # Test mangled name behaviour + __get_age = get_age # private copy of original get_age method + __private_parent_attribute = "Private_parent" + + +class Child(Parent): + + def __init__(self): + self.name = "Bobby" + self.age = 15 + + def get_name(self): + return self.name + + def get_age(self): + return f"Child class - {self.age}" + + @staticmethod + def addition(a, b): + return a + b + + +class Baby(Child): + + # Test a private attribute + __private_baby_name = "Private1234" + + def __init__(self): + self.name = "Charlie" + self.age = 2 + + def get_age(self): + return f"Baby class - {self.age}" + + class BabyInnerClass(): + baby_inner_attribute = "An attribute of an inner class" + + +class Job: + + def __init__(self): + self.salary = 10 + + def get_salary(self): + return self.salary + + +class Architect(Job): + + def __init__(self): + self.salary = 20 + + def get_age(self): + return f"Architect age - {self.age}" + +# Test a class that inherits multiple classes +class Jerry(Architect, Child): + + def __init__(self): + self.name = "Jerry" + self.age = 25 + + +__all__ = ["Parent", "Child", "Baby", "Job", "Architect", "Jerry"] + +# The following is used to process the expected builtin members for different +# versions of Python. The base list is for v3.11 (the latest testing when added) +# and new builtin attributes/methods in later versions can be appended below. + +members_3_11 = ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', + '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', + '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', + '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', + '__sizeof__', '__str__', '__subclasshook__', '__weakref__'] + +attr_3_11 = ['__annotations__', '__dict__', '__doc__', '__module__', '__weakref__'] + + +def concat_and_sort(list1, list2): + return sorted(list1 + list2) + + +built_in_attr = attr_3_11 +built_in_members = members_3_11 +# The second test has an extra builtin member +built_in_members2 = ['__annotations__', *members_3_11] + +if sys.version_info[:2] >= (3, 13): + add_3_13 = ['__firstlineno__', '__static_attributes__'] + built_in_attr = concat_and_sort(built_in_attr, add_3_13) + built_in_members = concat_and_sort(built_in_members, add_3_13) + built_in_members2 = concat_and_sort(built_in_members2, add_3_13) + +if sys.version_info[:2] >= (3, 14): + built_in_attr = concat_and_sort(built_in_attr, ['__annotate__']) + built_in_members2 = concat_and_sort(built_in_members2, ['__annotate__']) \ No newline at end of file diff --git a/tests/test_extensions/test_ext_autosummary.py b/tests/test_extensions/test_ext_autosummary.py index 81b13860278..5a924aa6008 100644 --- a/tests/test_extensions/test_ext_autosummary.py +++ b/tests/test_extensions/test_ext_autosummary.py @@ -488,6 +488,376 @@ def test_autosummary_generate_content_for_module_imported_members_inherited_modu assert context['objtype'] == 'module' +@pytest.mark.sphinx(testroot='ext-autosummary') +def test_autosummary_generate_content_for_module_imported_members_inherited_class(app): + import autosummary_dummy_inherited_module + + template = Mock() + + generate_autosummary_content( + 'autosummary_dummy_inherited_module.InheritedAttrClass', + autosummary_dummy_inherited_module.InheritedAttrClass, + None, + template, + None, + True, + app, + False, + {}, + ) + assert template.render.call_args[0][0] == 'class' + + context = template.render.call_args[0][1] + + def assert_all_a_in_b(a, b): + assert all(x in b for x in a) + + assert_all_a_in_b( + [ + 'Bar', + 'CONSTANT3', + 'CONSTANT4', + '__init__', + 'bar', + 'baz', + 'subclassattr', + 'value', + ], + context['members'], + ) + assert_all_a_in_b( + ['Bar', 'CONSTANT3', 'CONSTANT4', 'bar', 'baz', 'value'], + context['inherited_members'], + ) + assert '__init__' not in context['inherited_members'] + assert 'subclassattr' not in context['inherited_members'] + + assert context['methods'] == ['__init__', 'bar'] + assert context['attributes'] == [ + 'CONSTANT3', + 'CONSTANT4', + 'baz', + 'subclassattr', + 'value', + ] + assert_all_a_in_b( + [ + 'autosummary_dummy_module.Foo.Bar', + 'autosummary_dummy_module.Foo.CONSTANT3', + 'autosummary_dummy_module.Foo.CONSTANT4', + 'autosummary_dummy_module.Foo.bar', + 'autosummary_dummy_module.Foo.baz', + 'autosummary_dummy_module.Foo.value', + ], + context['inherited_qualnames'], + ) + assert context['inherited_methods'] == ['autosummary_dummy_module.Foo.bar'] + assert context['inherited_attributes'] == [ + 'autosummary_dummy_module.Foo.CONSTANT3', + 'autosummary_dummy_module.Foo.CONSTANT4', + 'autosummary_dummy_module.Foo.baz', + 'autosummary_dummy_module.Foo.value', + ] + assert ( + context['fullname'] == 'autosummary_dummy_inherited_module.InheritedAttrClass' + ) + assert context['module'] == 'autosummary_dummy_inherited_module' + assert context['objname'] == 'InheritedAttrClass' + assert context['name'] == 'InheritedAttrClass' + assert context['objtype'] == 'class' + + +@pytest.mark.sphinx(testroot='ext-autosummary') +def test_autosummary_generate_content_for_module_imported_members_complex_inheritance( + app, +): + import autosummary_dummy_complex_inheritance_module + + built_in_attr = autosummary_dummy_complex_inheritance_module.built_in_attr + built_in_members = autosummary_dummy_complex_inheritance_module.built_in_members + built_in_members2 = autosummary_dummy_complex_inheritance_module.built_in_members2 + + template_jerry = Mock() + + generate_autosummary_content( + name='autosummary_dummy_complex_inheritance_module.Jerry', + obj=autosummary_dummy_complex_inheritance_module.Jerry, + parent=None, + template=template_jerry, + template_name=None, + imported_members=True, + app=app, + recursive=False, + context={}, + ) + assert template_jerry.render.call_args[0][0] == 'class' + + context = template_jerry.render.call_args[0][1] + + assert context['name'] == 'Jerry' + assert context['module'] == 'autosummary_dummy_complex_inheritance_module' + assert context['fullname'] == 'autosummary_dummy_complex_inheritance_module.Jerry' + + assert context['attributes'] == ['relation'] + assert context['methods'] == [ + '__init__', + 'addition', + 'get_age', + 'get_name', + 'get_salary', + ] + + assert context['all_attributes'] == [ + *built_in_attr, + 'relation', + ] + assert context['all_methods'] == [ + '__delattr__', + '__dir__', + '__eq__', + '__format__', + '__ge__', + '__getattribute__', + '__getstate__', + '__gt__', + '__hash__', + '__init__', + '__init_subclass__', + '__le__', + '__lt__', + '__ne__', + '__new__', + '__reduce__', + '__reduce_ex__', + '__repr__', + '__setattr__', + '__sizeof__', + '__str__', + '__subclasshook__', + 'addition', + 'get_age', + 'get_name', + 'get_salary', + ] + + assert context['inherited_attributes'] == [ + 'autosummary_dummy_complex_inheritance_module.Parent.relation' + ] + assert context['inherited_methods'] == [ + 'autosummary_dummy_complex_inheritance_module.Architect.get_age', + 'autosummary_dummy_complex_inheritance_module.Job.get_salary', + 'autosummary_dummy_complex_inheritance_module.Child.addition', + 'autosummary_dummy_complex_inheritance_module.Child.get_name', + ] + + assert context['inherited_qualnames'] == [ + 'autosummary_dummy_complex_inheritance_module.Architect.get_age', + 'autosummary_dummy_complex_inheritance_module.Job.__dict__', + 'autosummary_dummy_complex_inheritance_module.Job.__weakref__', + 'autosummary_dummy_complex_inheritance_module.Job.get_salary', + 'autosummary_dummy_complex_inheritance_module.Child.addition', + 'autosummary_dummy_complex_inheritance_module.Child.get_name', + 'autosummary_dummy_complex_inheritance_module.Parent.relation', + 'builtins.object.__class__', + 'builtins.object.__delattr__', + 'builtins.object.__dir__', + 'builtins.object.__eq__', + 'builtins.object.__format__', + 'builtins.object.__ge__', + 'builtins.object.__getattribute__', + 'builtins.object.__getstate__', + 'builtins.object.__gt__', + 'builtins.object.__hash__', + 'builtins.object.__init_subclass__', + 'builtins.object.__le__', + 'builtins.object.__lt__', + 'builtins.object.__ne__', + 'builtins.object.__new__', + 'builtins.object.__reduce__', + 'builtins.object.__reduce_ex__', + 'builtins.object.__repr__', + 'builtins.object.__setattr__', + 'builtins.object.__sizeof__', + 'builtins.object.__str__', + 'builtins.object.__subclasshook__', + ] + + assert context['inherited_members'] == [ + '__class__', + '__delattr__', + '__dict__', + '__dir__', + '__eq__', + '__format__', + '__ge__', + '__getattribute__', + '__getstate__', + '__gt__', + '__hash__', + '__init_subclass__', + '__le__', + '__lt__', + '__ne__', + '__new__', + '__reduce__', + '__reduce_ex__', + '__repr__', + '__setattr__', + '__sizeof__', + '__str__', + '__subclasshook__', + '__weakref__', + 'addition', + 'get_age', + 'get_name', + 'get_salary', + 'relation', + ] + + assert context['members'] == [ + *built_in_members, + 'addition', + 'get_age', + 'get_name', + 'get_salary', + 'relation', + ] + + template_baby = Mock() + + generate_autosummary_content( + name='autosummary_dummy_complex_inheritance_module.Baby', + obj=autosummary_dummy_complex_inheritance_module.Baby, + parent=None, + template=template_baby, + template_name=None, + imported_members=True, + app=app, + recursive=False, + context={}, + ) + + context2 = template_baby.render.call_args[0][1] + + assert context2['name'] == 'Baby' + assert context2['module'] == 'autosummary_dummy_complex_inheritance_module' + assert context2['fullname'] == 'autosummary_dummy_complex_inheritance_module.Baby' + + assert context2['attributes'] == ['relation'] + assert context2['methods'] == ['__init__', 'addition', 'get_age', 'get_name'] + + assert context2['all_attributes'] == [ + '__private_baby_name', + *built_in_attr, + 'relation', + ] + assert context2['all_methods'] == [ + '__delattr__', + '__dir__', + '__eq__', + '__format__', + '__ge__', + '__getattribute__', + '__getstate__', + '__gt__', + '__hash__', + '__init__', + '__init_subclass__', + '__le__', + '__lt__', + '__ne__', + '__new__', + '__reduce__', + '__reduce_ex__', + '__repr__', + '__setattr__', + '__sizeof__', + '__str__', + '__subclasshook__', + 'addition', + 'get_age', + 'get_name', + ] + + assert context2['inherited_attributes'] == [ + 'autosummary_dummy_complex_inheritance_module.Parent.relation' + ] + assert context2['inherited_methods'] == [ + 'autosummary_dummy_complex_inheritance_module.Child.addition', + 'autosummary_dummy_complex_inheritance_module.Child.get_name', + ] + + assert context2['inherited_qualnames'] == [ + 'autosummary_dummy_complex_inheritance_module.Child.addition', + 'autosummary_dummy_complex_inheritance_module.Child.get_name', + 'autosummary_dummy_complex_inheritance_module.Parent.__dict__', + 'autosummary_dummy_complex_inheritance_module.Parent.__weakref__', + 'autosummary_dummy_complex_inheritance_module.Parent.relation', + 'builtins.object.__class__', + 'builtins.object.__delattr__', + 'builtins.object.__dir__', + 'builtins.object.__eq__', + 'builtins.object.__format__', + 'builtins.object.__ge__', + 'builtins.object.__getattribute__', + 'builtins.object.__getstate__', + 'builtins.object.__gt__', + 'builtins.object.__hash__', + 'builtins.object.__init_subclass__', + 'builtins.object.__le__', + 'builtins.object.__lt__', + 'builtins.object.__ne__', + 'builtins.object.__new__', + 'builtins.object.__reduce__', + 'builtins.object.__reduce_ex__', + 'builtins.object.__repr__', + 'builtins.object.__setattr__', + 'builtins.object.__sizeof__', + 'builtins.object.__str__', + 'builtins.object.__subclasshook__', + ] + + assert context2['inherited_members'] == [ + '__class__', + '__delattr__', + '__dict__', + '__dir__', + '__eq__', + '__format__', + '__ge__', + '__getattribute__', + '__getstate__', + '__gt__', + '__hash__', + '__init_subclass__', + '__le__', + '__lt__', + '__ne__', + '__new__', + '__reduce__', + '__reduce_ex__', + '__repr__', + '__setattr__', + '__sizeof__', + '__str__', + '__subclasshook__', + '__weakref__', + 'addition', + 'get_name', + 'relation', + ] + + assert context2['members'] == [ + 'BabyInnerClass', + '__private_baby_name', + *built_in_members2, + 'addition', + 'get_age', + 'get_name', + 'relation', + ] + + @pytest.mark.sphinx('dummy', testroot='ext-autosummary') def test_autosummary_generate(app): app.build(force_all=True)