Skip to content

Commit 2c09437

Browse files
tbekolayAA-Turner
andauthored
Disallow module cycles in autosummary (#6792)
Consider the following piece of reST:: .. automodule:: sphinx.ext.autosummary :members: .. autosummary:: sphinx.ext.autosummary.Autosummary This inserts an autosummary after the module docstring, but before the members of the module. Without the change in this commit, this would fail because `import_by_name` would attempt to import:: sphinx.ext.autosummary.sphinx.ext.autosummary.Autosumary because the prefix (from the parent) is `sphinx.ext.autosummary`, and the name is `sphinx.ext.autosummary.Autosummary`, which is able to be imported from `sphinx.ext.autosummary`, but is not the way that anyone would want to refer to it. Co-authored-by: Adam Turner <[email protected]>
1 parent 78c8b4d commit 2c09437

File tree

9 files changed

+78
-0
lines changed

9 files changed

+78
-0
lines changed

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ Features added
8080
* #7896, #11989: Add a :rst:dir:`py:type` directiv for documenting type aliases,
8181
and a :rst:role:`py:type` role for linking to them.
8282
Patch by Ashley Whetter.
83+
* #6792: Prohibit module import cycles in :mod:`sphinx.ext.autosummary`.
84+
Patch by Trevor Bekolay.
8385

8486
Bugs fixed
8587
----------

doc/usage/configuration.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,6 +1383,7 @@ Options for warning control
13831383
* ``autodoc.import_object``
13841384
* ``autosectionlabel.<document name>``
13851385
* ``autosummary``
1386+
* ``autosummary.import_cycle``
13861387
* ``intersphinx.external``
13871388

13881389
You can choose from these types. You can also give only the first

sphinx/ext/autosummary/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,13 @@ def import_by_name(
639639
tried = []
640640
errors: list[ImportExceptionGroup] = []
641641
for prefix in prefixes:
642+
if prefix is not None and name.startswith(f'{prefix}.'):
643+
# Catch and avoid module cycles (e.g., sphinx.ext.sphinx.ext...)
644+
msg = __('Summarised items should not include the current module. '
645+
'Replace %r with %r.')
646+
logger.warning(msg, name, name.removeprefix(f'{prefix}.'),
647+
type='autosummary', subtype='import_cycle')
648+
continue
642649
try:
643650
if prefix:
644651
prefixed_name = f'{prefix}.{name}'
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import os
2+
import sys
3+
4+
sys.path.insert(0, os.path.abspath('.'))
5+
6+
extensions = ['sphinx.ext.autosummary']
7+
autosummary_generate = False
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.. automodule:: spam.eggs
2+
:members:
3+
4+
.. autosummary::
5+
6+
spam.eggs.Ham
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""``spam`` module docstring."""
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""``spam.eggs`` module docstring."""
2+
3+
import spam # Required for test.
4+
5+
6+
class Ham:
7+
"""``spam.eggs.Ham`` class docstring."""
8+
a = 1
9+
b = 2
10+
c = 3
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Test autosummary for import cycles."""
2+
3+
import pytest
4+
from docutils import nodes
5+
6+
from sphinx import addnodes
7+
from sphinx.ext.autosummary import autosummary_table
8+
from sphinx.testing.util import assert_node
9+
10+
11+
@pytest.mark.sphinx('dummy', testroot='ext-autosummary-import_cycle')
12+
@pytest.mark.usefixtures("rollback_sysmodules")
13+
def test_autosummary_import_cycle(app, warning):
14+
app.build()
15+
16+
doctree = app.env.get_doctree('index')
17+
app.env.apply_post_transforms(doctree, 'index')
18+
19+
assert len(list(doctree.findall(nodes.reference))) == 1
20+
21+
assert_node(doctree,
22+
(addnodes.index, # [0]
23+
nodes.target, # [1]
24+
nodes.paragraph, # [2]
25+
addnodes.tabular_col_spec, # [3]
26+
[autosummary_table, nodes.table, nodes.tgroup, (nodes.colspec, # [4][0][0][0]
27+
nodes.colspec, # [4][0][0][1]
28+
[nodes.tbody, nodes.row])], # [4][0][0][2][1]
29+
addnodes.index, # [5]
30+
addnodes.desc)) # [6]
31+
assert_node(doctree[4][0][0][2][0],
32+
([nodes.entry, nodes.paragraph, (nodes.reference, nodes.Text)], nodes.entry))
33+
assert_node(doctree[4][0][0][2][0][0][0][0], nodes.reference,
34+
refid='spam.eggs.Ham', reftitle='spam.eggs.Ham')
35+
36+
expected = (
37+
"Summarised items should not include the current module. "
38+
"Replace 'spam.eggs.Ham' with 'Ham'."
39+
)
40+
assert expected in app.warning.getvalue()

tests/test_extensions/test_ext_viewcode.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def check_viewcode_output(app, warning):
4242

4343
@pytest.mark.sphinx(testroot='ext-viewcode', freshenv=True,
4444
confoverrides={"viewcode_line_numbers": True})
45+
@pytest.mark.usefixtures("rollback_sysmodules")
4546
def test_viewcode_linenos(app, warning):
4647
shutil.rmtree(app.outdir / '_modules', ignore_errors=True)
4748
app.build(force_all=True)
@@ -52,6 +53,7 @@ def test_viewcode_linenos(app, warning):
5253

5354
@pytest.mark.sphinx(testroot='ext-viewcode', freshenv=True,
5455
confoverrides={"viewcode_line_numbers": False})
56+
@pytest.mark.usefixtures("rollback_sysmodules")
5557
def test_viewcode(app, warning):
5658
shutil.rmtree(app.outdir / '_modules', ignore_errors=True)
5759
app.build(force_all=True)
@@ -61,6 +63,7 @@ def test_viewcode(app, warning):
6163

6264

6365
@pytest.mark.sphinx('epub', testroot='ext-viewcode')
66+
@pytest.mark.usefixtures("rollback_sysmodules")
6467
def test_viewcode_epub_default(app, status, warning):
6568
shutil.rmtree(app.outdir)
6669
app.build(force_all=True)
@@ -73,6 +76,7 @@ def test_viewcode_epub_default(app, status, warning):
7376

7477
@pytest.mark.sphinx('epub', testroot='ext-viewcode',
7578
confoverrides={'viewcode_enable_epub': True})
79+
@pytest.mark.usefixtures("rollback_sysmodules")
7680
def test_viewcode_epub_enabled(app, status, warning):
7781
app.build(force_all=True)
7882

0 commit comments

Comments
 (0)