Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ Release 9.1.1 (in development)
Bugs fixed
----------

* #14354: autodoc: execute PEP 561 ``.pyi`` stub files with deferred
annotation semantics, as if they began with ``from __future__ import
annotations``, so that forward references in stub annotations no longer
raise ``NameError`` at import time.

* #14465: LaTeX: PDF build crash since LaTeX June 2026 release if tables are
styled with ``'colorrows'`` (which is the default).
Patch by Jean-François B.
Expand Down
30 changes: 28 additions & 2 deletions sphinx/ext/autodoc/_dynamic/_importer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Importer utilities for autodoc"""

from __future__ import annotations
import __future__

import contextlib
import importlib
Expand Down Expand Up @@ -220,11 +221,36 @@ def _import_module(modname: str, try_reload: bool = False) -> Any:
if skip_pyi or pyi_path is None:
module = importlib.import_module(modname)
else:
if spec.loader is None:
loader = spec.loader
if loader is None:
msg = 'missing loader'
raise ImportError(msg, name=spec.name) # NoQA: TRY301
sys.modules[modname] = module = module_from_spec(spec)
spec.loader.exec_module(module)
if isinstance(loader, _StubFileLoader):
# PEP 484 requires stub files to be treated as if all type
# annotations were lazily evaluated, i.e. as if each file began
# with ``from __future__ import annotations``. Executing the stub
# as a plain module would otherwise fail on forward references
# (https://github.com/sphinx-doc/sphinx/issues/14354). Python 3.14
# defers annotations by default, so the flag is only applied when
# the running Python provides it.
feature = getattr(__future__, 'CO_FUTURE_ANNOTATIONS', None)
flags = (
int(getattr(feature, 'compiler_flag', feature))
if feature is not None
else 0
)
code = compile(
loader.get_source(modname),
str(pyi_path),
'exec',
flags=flags,
dont_inherit=True,
)
# Executing the stub is the whole purpose of this loader.
exec(code, module.__dict__) # noqa: S102
else:
loader.exec_module(module)
except ImportError:
raise
except BaseException as exc:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class Fish:
def __init__(self, other: Fish) -> None: ...
11 changes: 10 additions & 1 deletion tests/test_ext_autodoc/test_ext_autodoc_autofunction.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

from __future__ import annotations

import re
import time

import pytest

from tests.test_ext_autodoc.autodoc_util import do_autodoc
Expand Down Expand Up @@ -122,10 +125,16 @@ def test_singledispatch() -> None:


def test_cfunction() -> None:
# CPython has renamed the argument in time.asctime()'s signature
# between releases ('tuple' vs 'time_tuple'); autodoc renders whatever
# the running interpreter's docstring says, so derive it at runtime.
match = re.search(r'asctime\(\[(\w+)\]\)', time.asctime.__doc__ or '')
assert match is not None
asctime_arg = match.group(1)
actual = do_autodoc('function', 'time.asctime')
assert actual == [
'',
'.. py:function:: asctime([tuple]) -> string',
f'.. py:function:: asctime([{asctime_arg}]) -> string',
' :module: time',
'',
" Convert a time tuple to a string, e.g. 'Sat Jun 06 16:26:11 1998'.",
Expand Down
17 changes: 17 additions & 0 deletions tests/test_ext_autodoc/test_ext_autodoc_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,20 @@ def test_import_native_module_stubs(rootdir: Path) -> None:
halibut_path = Path(halibut.__file__).resolve()
assert halibut_path.is_file()
assert halibut_path == fish_licence_root / 'fish_licence' / 'halibut.pyi'


def test_import_native_module_stubs_defer_annotations(rootdir: Path) -> None:
# PEP 484: stub files are compiled as if they began with
# ``from __future__ import annotations``, so forward references in
# annotations must not fail at import time (issue #14354).
fish_licence_root = rootdir / 'test-ext-apidoc-duplicates'

sys_path = list(sys.path)
sys.path.insert(0, str(fish_licence_root))
try:
halibut = _import_module('fish_licence.halibut')
finally:
sys.path[:] = sys_path

annotations = halibut.Fish.__init__.__annotations__
assert annotations['other'] == 'Fish'
Loading