Repository: sphinx-doc/sphinx

Issue:
Sphinx accesses inherited annotations incorrectly in python 3.9 and earlier
### Describe the bug

In python 3.10, a bug was fixed where an attempt to read the annotations on a class object could accidentally return the annotations on a superclass instead.  For python 3.9 and earlier, the is the suggested workaround:

https://docs.python.org/3.10/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older

It appears that this needs to be implemented in sphinx/util/inspect.py line 88

In my test case, class A1 is NOT using inherited-members and class A2 is using inherited-members.
The inherited attribute `inherit_me` shows up in both classes when sphinx is run in a python 3.9 environment but only on class A2 if you build and install in python 3.10

### How to Reproduce

index.rst 
```rst
My Docs
=======

.. currentmodule:: my_module

.. autoclass:: A1
   :members:

.. autoclass:: A2
   :members:
   :inherited-members:
```

source code
```python
class B:
    inherit_me: int
    """Inherit me from baseclass B"""

class A1(B):
    """inherit test #1  - members but not inherited-memebers"""

    my_a1 = 1
    """my_a1"""


class A2(B):
    """inherit test #2. -- members and inherited-members"""

    my_a2 = 2
    """my_a2"""
```


### Environment Information

Python 3.9 environment:

```text
Platform:              darwin; (macOS-14.2.1-arm64-arm-64bit)
Python version:        3.9.18 (main, Aug 24 2023, 18:16:58)
[Clang 15.0.0 (clang-1500.1.0.2.5)])
Python implementation: CPython
Sphinx version:        7.2.6
Docutils version:      0.20.1
Jinja2 version:        3.1.3
Pygments version:      2.17.2
```

Python 3.10 environment:
```text
Platform:              darwin; (macOS-14.2.1-arm64-arm-64bit)
Python version:        3.10.13 (main, Aug 24 2023, 12:59:26) [Clang 15.0.0 (clang-1500.1.0.2.5)])
Python implementation: CPython
Sphinx version:        7.2.6
Docutils version:      0.20.1
Jinja2 version:        3.1.3
Pygments version:      2.17.2
```


### Sphinx extensions

```python
extensions = [ "sphinx.ext.autodoc" ]
source_suffix = {
  '.rst': 'restructuredtext'
}
```


### Additional context

This is possibly a duplicate or related to a number of other issues referencing inherited annotations... but I've debugged this particular test case

Discussion:

Patch:
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:


Test patch:
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',
+        '',
+    ]