From cd2bf0a7e516caecf84b4c24aad43a987e4ac8fb Mon Sep 17 00:00:00 2001 From: j-carson <44308120+j-carson@users.noreply.github.com> Date: Sat, 3 Feb 2024 01:46:13 -0800 Subject: [PATCH] #11917 - Fix rendering of inherited members for Python 3.9 (#11919) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: j-carson Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- CHANGES.rst | 2 + sphinx/util/inspect.py | 8 +++- .../target/inherited_annotations.py | 17 +++++++ .../test_ext_autodoc_autoclass.py | 46 +++++++++++++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 tests/roots/test-ext-autodoc/target/inherited_annotations.py diff --git a/CHANGES.rst b/CHANGES.rst index a10cb8e31..2652542d4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -72,6 +72,8 @@ Bugs fixed Patch by Colin Marquardt. * #11598: Do not use query components in URLs for assets in EPUB rendering. Patch by David Runge. +* #11917: Fix rendering of annotated inherited members for Python 3.9. + Patch by Janet Carson. * #11925: Blacklist the ``sphinxprettysearchresults`` extension; the functionality it provides was merged into Sphinx v2.0.0. Patch by James Addison. diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 855e5d537..8c63656fb 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -87,7 +87,13 @@ def getall(obj: Any) -> Sequence[str] | None: def getannotations(obj: Any) -> Mapping[str, Any]: """Get __annotations__ from given *obj* safely.""" - __annotations__ = safe_getattr(obj, '__annotations__', None) + if sys.version_info >= (3, 10, 0) or not isinstance(obj, type): + __annotations__ = safe_getattr(obj, '__annotations__', None) + else: + # Workaround for bugfix not available until python 3.10 as recommended by docs + # https://docs.python.org/3.10/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older + __dict__ = safe_getattr(obj, '__dict__', {}) + __annotations__ = __dict__.get('__annotations__', None) if isinstance(__annotations__, Mapping): return __annotations__ else: diff --git a/tests/roots/test-ext-autodoc/target/inherited_annotations.py b/tests/roots/test-ext-autodoc/target/inherited_annotations.py new file mode 100644 index 000000000..3ae58a852 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/inherited_annotations.py @@ -0,0 +1,17 @@ +""" + Test case for #11387 corner case involving inherited + members with type annotations on python 3.9 and earlier +""" + +class HasTypeAnnotatedMember: + inherit_me: int + """Inherited""" + +class NoTypeAnnotation(HasTypeAnnotatedMember): + a = 1 + """Local""" + +class NoTypeAnnotation2(HasTypeAnnotatedMember): + a = 1 + """Local""" + diff --git a/tests/test_extensions/test_ext_autodoc_autoclass.py b/tests/test_extensions/test_ext_autodoc_autoclass.py index 107d12ab4..dc65785d3 100644 --- a/tests/test_extensions/test_ext_autodoc_autoclass.py +++ b/tests/test_extensions/test_ext_autodoc_autoclass.py @@ -515,3 +515,49 @@ def test_autoattribute_TypeVar_module_level(app): " alias of TypeVar('T1')", '', ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_inherited_instance_variable_with_annotations(app): + options = {'members': None, + 'inherited-members': None} + actual = do_autodoc(app, 'class', 'target.inherited_annotations.NoTypeAnnotation', options) + assert list(actual) == [ + '', + '.. py:class:: NoTypeAnnotation()', + ' :module: target.inherited_annotations', + '', + '', + ' .. py:attribute:: NoTypeAnnotation.a', + ' :module: target.inherited_annotations', + ' :value: 1', + '', + ' Local', + '', + '', + ' .. py:attribute:: NoTypeAnnotation.inherit_me', + ' :module: target.inherited_annotations', + ' :type: int', + '', + ' Inherited', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_no_inherited_instance_variable_with_annotations(app): + options = {'members': None} + actual = do_autodoc(app, 'class', 'target.inherited_annotations.NoTypeAnnotation2', options) + assert list(actual) == [ + '', + '.. py:class:: NoTypeAnnotation2()', + ' :module: target.inherited_annotations', + '', + '', + ' .. py:attribute:: NoTypeAnnotation2.a', + ' :module: target.inherited_annotations', + ' :value: 1', + '', + ' Local', + '', + ]