Skip to content

Commit 5bbb1ae

Browse files
authored
sphinx-autodoc-pytest-fixtures(feat[type-alias]): Resolve TypeAlias return types (#9)
Fixture tables previously showed the expanded union or generic type when a fixture's return annotation was a TypeAlias (e.g. `-> MyAlias` rendered as `Options | Mapping[str, Any]` instead of `MyAlias`). The alias name is now preserved and emits a cross-reference to the alias's documentation entry. - **TYPE_CHECKING aliases**: `_qualify_forward_ref` now matches `AnnAssign` nodes with a `TypeAlias` annotation inside `TYPE_CHECKING` guards, in addition to `ImportFrom` nodes - **Module-level aliases**: same resolution applied to `AnnAssign` nodes at module body level (aliases defined outside `TYPE_CHECKING`) - **Fast-path guard**: runtime fast path now checks `obj.__qualname__ == name` to prevent TypeAlias values (whose `__qualname__` is `"Union"`) from stealing the cross-reference - **get_type_hints discriminator**: uses `isinstance(ret, type)` to distinguish real classes (resolve normally) from union/generic expansions (preserve the raw alias name string)
2 parents 3a3e8fb + 6e5b5fe commit 5bbb1ae

File tree

34 files changed

+833
-164
lines changed

34 files changed

+833
-164
lines changed

CHANGES

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ $ uv add gp-sphinx --prerelease allow
2424
- `merge_sphinx_config()` API for building complete Sphinx config from shared defaults
2525
- Shared extension list, theme options, MyST config, font config
2626
- Bundled workarounds (tabs.js removal, spa-nav.js injection)
27+
- `sphinx-autodoc-pytest-fixtures`: Fixture tables now resolve `TypeAlias`
28+
return annotations — alias names are preserved and linked rather than
29+
expanding to the underlying union or generic type (#9)
2730

2831
### Workspace packages
2932

docs/_ext/argparse_neo_demo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def build_parser() -> argparse.ArgumentParser:
3939
4040
Machine-readable output examples:
4141
gp-demo sync --format json packages/sphinx-fonts
42-
"""
42+
""",
4343
),
4444
formatter_class=argparse.RawDescriptionHelpFormatter,
4545
)

docs/_ext/package_reference.py

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,12 @@
6363
import sys
6464
import typing as t
6565

66-
from docutils import nodes
6766
from docutils.parsers.rst import roles
6867
from sphinx.util.docutils import SphinxDirective
6968

69+
if t.TYPE_CHECKING:
70+
from docutils import nodes
71+
7072
if sys.version_info >= (3, 11):
7173
import tomllib
7274
else:
@@ -104,8 +106,8 @@ def workspace_root() -> pathlib.Path:
104106
105107
Examples
106108
--------
107-
>>> workspace_root().name
108-
'gp-sphinx'
109+
>>> workspace_root().is_dir()
110+
True
109111
"""
110112
return pathlib.Path(__file__).resolve().parents[2]
111113

@@ -137,9 +139,9 @@ def workspace_packages() -> list[dict[str, str]]:
137139
"version": str(project["version"]),
138140
"repository": str(project.get("urls", {}).get("Repository", "")),
139141
"maturity": maturity_from_classifiers(
140-
t.cast(list[str], project.get("classifiers", []))
142+
t.cast("list[str]", project.get("classifiers", [])),
141143
),
142-
}
144+
},
143145
)
144146
return packages
145147

@@ -193,7 +195,8 @@ def extension_modules(module_name: str) -> list[str]:
193195
submodule = importlib.import_module(module_info.name)
194196
except ImportError:
195197
logger.warning(
196-
"package-reference: could not import submodule %r", module_info.name
198+
"package-reference: could not import submodule %r",
199+
module_info.name,
197200
)
198201
continue
199202
if callable(getattr(submodule, "setup", None)):
@@ -248,7 +251,7 @@ def render_types(types: object, default: object) -> str:
248251
if isinstance(types, (list, tuple, set, frozenset)) and types:
249252
names = sorted(
250253
getattr(item, "__name__", str(item))
251-
for item in t.cast(t.Iterable[object], types)
254+
for item in t.cast("t.Iterable[object]", types)
252255
)
253256
return f"`{' | '.join(names)}`"
254257
if default is None:
@@ -323,9 +326,9 @@ def _record_local(name: str, role: object) -> None:
323326
# Limitation: this mutates process-global state and is not safe for
324327
# parallel Sphinx builds (sphinx -j N); single-threaded builds only.
325328
try:
326-
roles.register_local_role = t.cast(t.Any, _record_local)
327-
roles.register_canonical_role = t.cast(t.Any, _record_local)
328-
setup = t.cast(t.Callable[[object], object], getattr(module, "setup"))
329+
roles.register_local_role = t.cast("t.Any", _record_local)
330+
roles.register_canonical_role = t.cast("t.Any", _record_local)
331+
setup = t.cast("t.Callable[[object], object]", getattr(module, "setup"))
329332
setup(app)
330333
finally:
331334
roles.register_local_role = original_local
@@ -351,7 +354,7 @@ def _record_local(name: str, role: object) -> None:
351354
"default": render_value(default),
352355
"rebuild": f"`{rebuild}`" if rebuild else "",
353356
"types": render_types(types, default),
354-
}
357+
},
355358
)
356359
elif name == "add_directive":
357360
directive_name = str(args[0])
@@ -363,7 +366,7 @@ def _record_local(name: str, role: object) -> None:
363366
"callable": object_path(directive_cls),
364367
"summary": summarize(getattr(directive_cls, "__doc__", None)),
365368
"options": directive_options_markdown(directive_cls),
366-
}
369+
},
367370
)
368371
elif name == "add_directive_to_domain":
369372
domain = str(args[0])
@@ -376,7 +379,7 @@ def _record_local(name: str, role: object) -> None:
376379
"callable": object_path(directive_cls),
377380
"summary": summarize(getattr(directive_cls, "__doc__", None)),
378381
"options": directive_options_markdown(directive_cls),
379-
}
382+
},
380383
)
381384
elif name == "add_crossref_type":
382385
directive_name = str(args[0])
@@ -388,15 +391,15 @@ def _record_local(name: str, role: object) -> None:
388391
"callable": "{py:meth}`~sphinx.application.Sphinx.add_crossref_type`",
389392
"summary": "Registers a standard-domain cross-reference target.",
390393
"options": "",
391-
}
394+
},
392395
)
393396
role_items.append(
394397
{
395398
"name": f"std:{role_name}",
396399
"kind": "cross-reference role",
397400
"callable": "{py:meth}`~sphinx.application.Sphinx.add_crossref_type`",
398401
"summary": "Registers a standard-domain cross-reference role.",
399-
}
402+
},
400403
)
401404
elif name == "add_role":
402405
role_name = str(args[0])
@@ -407,7 +410,7 @@ def _record_local(name: str, role: object) -> None:
407410
"kind": "role",
408411
"callable": object_path(role_fn),
409412
"summary": summarize(getattr(role_fn, "__doc__", None)),
410-
}
413+
},
411414
)
412415
elif name == "add_role_to_domain":
413416
domain = str(args[0])
@@ -419,21 +422,21 @@ def _record_local(name: str, role: object) -> None:
419422
"kind": "domain role",
420423
"callable": object_path(role_fn),
421424
"summary": summarize(getattr(role_fn, "__doc__", None)),
422-
}
425+
},
423426
)
424427
elif name == "add_lexer":
425428
lexers.append(
426429
{
427430
"name": str(args[0]),
428431
"callable": object_path(args[1]),
429-
}
432+
},
430433
)
431434
elif name == "add_html_theme":
432435
themes.append(
433436
{
434437
"name": str(args[0]),
435438
"path": f"`{args[1]}`",
436-
}
439+
},
437440
)
438441

439442
for role_name, role_fn in registered_roles:
@@ -443,7 +446,7 @@ def _record_local(name: str, role: object) -> None:
443446
"kind": "docutils role",
444447
"callable": object_path(role_fn),
445448
"summary": summarize(getattr(role_fn, "__doc__", None)),
446-
}
449+
},
447450
)
448451

449452
return {
@@ -585,7 +588,7 @@ def package_reference_markdown(package_name: str) -> str:
585588
f"- PyPI: [{package_name}]({pypi_url})",
586589
f"- Maturity: `{package['maturity']}`",
587590
"",
588-
]
591+
],
589592
)
590593

591594
if package_name == "gp-sphinx":
@@ -596,7 +599,7 @@ def package_reference_markdown(package_name: str) -> str:
596599
"This package is a coordinator rather than a Sphinx extension module.",
597600
"Its public runtime surface is documented in {doc}`/configuration` and {doc}`/api`.",
598601
"",
599-
]
602+
],
600603
)
601604
return "\n".join(lines)
602605

@@ -612,11 +615,11 @@ def package_reference_markdown(package_name: str) -> str:
612615
"",
613616
"| Name | Default | Rebuild | Types |",
614617
"| --- | --- | --- | --- |",
615-
]
618+
],
616619
)
617620
for row in config_rows:
618621
lines.append(
619-
f"| `{row['name']}` | {row['default']} | {row['rebuild']} | {row['types']} |"
622+
f"| `{row['name']}` | {row['default']} | {row['rebuild']} | {row['types']} |",
620623
)
621624
lines.append("")
622625

@@ -628,11 +631,11 @@ def package_reference_markdown(package_name: str) -> str:
628631
"",
629632
"| Name | Kind | Callable | Summary |",
630633
"| --- | --- | --- | --- |",
631-
]
634+
],
632635
)
633636
for row in directive_rows:
634637
lines.append(
635-
f"| `{row['name']}` | {row['kind']} | {row['callable']} | {row['summary']} |"
638+
f"| `{row['name']}` | {row['kind']} | {row['callable']} | {row['summary']} |",
636639
)
637640
lines.append("")
638641
for row in directive_rows:
@@ -642,7 +645,7 @@ def package_reference_markdown(package_name: str) -> str:
642645
f"##### {row['name']} options",
643646
row["options"],
644647
"",
645-
]
648+
],
646649
)
647650

648651
role_rows = block["roles"]
@@ -653,11 +656,11 @@ def package_reference_markdown(package_name: str) -> str:
653656
"",
654657
"| Name | Kind | Callable | Summary |",
655658
"| --- | --- | --- | --- |",
656-
]
659+
],
657660
)
658661
for row in role_rows:
659662
lines.append(
660-
f"| `{row['name']}` | {row['kind']} | {row['callable']} | {row['summary']} |"
663+
f"| `{row['name']}` | {row['kind']} | {row['callable']} | {row['summary']} |",
661664
)
662665
lines.append("")
663666

@@ -669,7 +672,7 @@ def package_reference_markdown(package_name: str) -> str:
669672
"",
670673
"| Name | Callable |",
671674
"| --- | --- |",
672-
]
675+
],
673676
)
674677
for row in lexer_rows:
675678
lines.append(f"| `{row['name']}` | {row['callable']} |")
@@ -683,7 +686,7 @@ def package_reference_markdown(package_name: str) -> str:
683686
"",
684687
"| Theme | Path |",
685688
"| --- | --- |",
686-
]
689+
],
687690
)
688691
for row in theme_rows:
689692
lines.append(f"| `{row['name']}` | {row['path']} |")
@@ -697,7 +700,7 @@ def package_reference_markdown(package_name: str) -> str:
697700
"",
698701
"| Option |",
699702
"| --- |",
700-
]
703+
],
701704
)
702705
for option in options:
703706
lines.append(f"| `{option}` |")
@@ -749,7 +752,7 @@ def workspace_package_grid_markdown() -> str:
749752
maturity_badge(package["maturity"]),
750753
":::",
751754
"",
752-
]
755+
],
753756
)
754757
lines.append("::::")
755758
return "\n".join(lines)
@@ -810,8 +813,8 @@ def _capture(
810813
_roles.append((role_name, role_fn))
811814

812815
try:
813-
roles.register_local_role = t.cast(t.Any, _capture)
814-
roles.register_canonical_role = t.cast(t.Any, _capture)
816+
roles.register_local_role = t.cast("t.Any", _capture)
817+
roles.register_canonical_role = t.cast("t.Any", _capture)
815818
setup_fn(recorder)
816819
except Exception:
817820
continue
@@ -828,18 +831,18 @@ def _capture(
828831
elif call_name == "add_role" and len(args) >= 2:
829832
obj = args[1]
830833
raw_objs.append(
831-
(obj, "function" if not inspect.isclass(obj) else "class")
834+
(obj, "function" if not inspect.isclass(obj) else "class"),
832835
)
833836
elif call_name == "add_role_to_domain" and len(args) >= 3:
834837
obj = args[2]
835838
raw_objs.append(
836-
(obj, "function" if not inspect.isclass(obj) else "class")
839+
(obj, "function" if not inspect.isclass(obj) else "class"),
837840
)
838841
elif call_name == "add_lexer" and len(args) >= 2:
839842
raw_objs.append((args[1], "class"))
840843
for _role_name, role_fn in docutils_roles:
841844
raw_objs.append(
842-
(role_fn, "function" if not inspect.isclass(role_fn) else "class")
845+
(role_fn, "function" if not inspect.isclass(role_fn) else "class"),
843846
)
844847

845848
for obj, objtype in raw_objs:

docs/conf.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
sys.path.insert(0, str(project_root / "packages" / "sphinx-gptheme" / "src"))
1414
sys.path.insert(0, str(project_root / "packages" / "sphinx-argparse-neo" / "src"))
1515
sys.path.insert(
16-
0, str(project_root / "packages" / "sphinx-autodoc-pytest-fixtures" / "src")
16+
0,
17+
str(project_root / "packages" / "sphinx-autodoc-pytest-fixtures" / "src"),
1718
)
1819
sys.path.insert(0, str(project_root / "packages" / "sphinx-autodoc-docutils" / "src"))
1920
sys.path.insert(0, str(project_root / "packages" / "sphinx-autodoc-sphinx" / "src"))

packages/gp-sphinx/src/gp_sphinx/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import logging
3232
import os.path
3333
import pathlib
34-
import types
3534
import typing as t
3635

3736
from gp_sphinx.defaults import (
@@ -65,6 +64,7 @@
6564
)
6665

6766
if t.TYPE_CHECKING:
67+
import types
6868
from collections.abc import Callable
6969

7070
from sphinx.application import Sphinx

packages/gp-sphinx/src/gp_sphinx/myst_lexer.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,15 @@ def _handle_eval_rst(
139139
(
140140
# group opening: backtick fence + directive + optional
141141
# info string (e.g. ```{eval-rst} some-arg)
142-
r"(?P<opening>^```\{eval-rst\}[^\n]*)"
143-
r"(?P<newline>\n)"
144-
# group body: RST content, non-greedy to stop at first
145-
# closing fence
146-
r"(?P<body>(?:.|\n)*?)"
147-
# group closing: bare ``` at start of line
148-
r"(?P<closing>^```[ \t]*$\n?)",
142+
(
143+
r"(?P<opening>^```\{eval-rst\}[^\n]*)"
144+
r"(?P<newline>\n)"
145+
# group body: RST content, non-greedy to stop at first
146+
# closing fence
147+
r"(?P<body>(?:.|\n)*?)"
148+
# group closing: bare ``` at start of line
149+
r"(?P<closing>^```[ \t]*$\n?)"
150+
),
149151
_handle_eval_rst,
150152
),
151153
# All MarkdownLexer root rules follow unchanged, providing

packages/sphinx-argparse-neo/src/sphinx_argparse_neo/compat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
import contextlib
1414
import importlib
1515
import sys
16-
import types
1716
import typing as t
1817

1918
if t.TYPE_CHECKING:
2019
import argparse
20+
import types
2121
from collections.abc import Iterator
2222

2323

packages/sphinx-argparse-neo/src/sphinx_argparse_neo/directive.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import typing as t
1010

11-
from docutils import nodes
1211
from docutils.parsers.rst import directives
1312
from sphinx.util.docutils import SphinxDirective
1413

@@ -20,6 +19,8 @@
2019
import argparse
2120
from collections.abc import Callable
2221

22+
from docutils import nodes
23+
2324

2425
class ArgparseDirective(SphinxDirective):
2526
"""Sphinx directive for documenting argparse-based CLI tools.

0 commit comments

Comments
 (0)