From 8248be31dbd6f235b9393f39ff16fa9e67dec67c Mon Sep 17 00:00:00 2001 From: Matt Wozniski Date: Tue, 29 Aug 2023 15:05:41 -0400 Subject: [PATCH] autodoc: Reset ``sys.modules`` on partial import failure (#11645) If importing with ``TYPE_CHECKING is True`` fails, reset the state of ``sys.modules`` so that the attempt with ``TYPE_CHECKING is False`` may succeed. Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com> --- CHANGES | 5 +++++ sphinx/ext/autodoc/importer.py | 8 +++++++- .../test-ext-autodoc/circular_import/__init__.py | 1 + tests/roots/test-ext-autodoc/circular_import/a.py | 1 + tests/roots/test-ext-autodoc/circular_import/b.py | 4 ++++ tests/roots/test-ext-autodoc/circular_import/c.py | 6 ++++++ tests/test_ext_autodoc.py | 13 +++++++++++++ 7 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/roots/test-ext-autodoc/circular_import/__init__.py create mode 100644 tests/roots/test-ext-autodoc/circular_import/a.py create mode 100644 tests/roots/test-ext-autodoc/circular_import/b.py create mode 100644 tests/roots/test-ext-autodoc/circular_import/c.py diff --git a/CHANGES b/CHANGES index 3edbbd67587..464cd71fec0 100644 --- a/CHANGES +++ b/CHANGES @@ -16,6 +16,11 @@ Features added Bugs fixed ---------- +* #11645: Fix a regression preventing autodoc from importing modules within + packages that make use of ``if typing.TYPE_CHECKING:`` to guard circular + imports needed by type checkers. + Patch by Matt Wozniski. + Testing ------- diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py index 2082a714036..0bbde6b57d5 100644 --- a/sphinx/ext/autodoc/importer.py +++ b/sphinx/ext/autodoc/importer.py @@ -3,6 +3,7 @@ from __future__ import annotations import importlib +import sys import traceback import typing from typing import TYPE_CHECKING, Any, Callable, NamedTuple @@ -82,13 +83,18 @@ def import_object(modname: str, objpath: list[str], objtype: str = '', objpath = list(objpath) while module is None: try: + orig_modules = frozenset(sys.modules) try: # try importing with ``typing.TYPE_CHECKING == True`` typing.TYPE_CHECKING = True module = import_module(modname, warningiserror=warningiserror) except ImportError: # if that fails (e.g. circular import), retry with - # ``typing.TYPE_CHECKING == False`` + # ``typing.TYPE_CHECKING == False`` after reverting + # changes made to ``sys.modules`` by the failed try + for m in [m for m in sys.modules if m not in orig_modules]: + sys.modules.pop(m) + typing.TYPE_CHECKING = False module = import_module(modname, warningiserror=warningiserror) finally: diff --git a/tests/roots/test-ext-autodoc/circular_import/__init__.py b/tests/roots/test-ext-autodoc/circular_import/__init__.py new file mode 100644 index 00000000000..402678d6706 --- /dev/null +++ b/tests/roots/test-ext-autodoc/circular_import/__init__.py @@ -0,0 +1 @@ +from circular_import.c import SomeClass diff --git a/tests/roots/test-ext-autodoc/circular_import/a.py b/tests/roots/test-ext-autodoc/circular_import/a.py new file mode 100644 index 00000000000..97ad9d8ab1b --- /dev/null +++ b/tests/roots/test-ext-autodoc/circular_import/a.py @@ -0,0 +1 @@ +X = 42 diff --git a/tests/roots/test-ext-autodoc/circular_import/b.py b/tests/roots/test-ext-autodoc/circular_import/b.py new file mode 100644 index 00000000000..c9b8ad54249 --- /dev/null +++ b/tests/roots/test-ext-autodoc/circular_import/b.py @@ -0,0 +1,4 @@ +import typing + +if typing.TYPE_CHECKING: + from circular_import import SomeClass diff --git a/tests/roots/test-ext-autodoc/circular_import/c.py b/tests/roots/test-ext-autodoc/circular_import/c.py new file mode 100644 index 00000000000..0a8829e378c --- /dev/null +++ b/tests/roots/test-ext-autodoc/circular_import/c.py @@ -0,0 +1,6 @@ +import circular_import.a +import circular_import.b + + +class SomeClass: + X = circular_import.a.X diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index c2f0d0bfc44..02869b8bee2 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -2024,6 +2024,19 @@ def test_autodoc_TYPE_CHECKING(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_TYPE_CHECKING_circular_import(app): + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'module', 'circular_import', options) + assert list(actual) == [ + '', + '.. py:module:: circular_import', + '', + ] + assert sys.modules["circular_import"].a is sys.modules["circular_import.a"] + + @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_singledispatch(app): options = {"members": None}