Skip to content

Commit 28ab5f2

Browse files
committed
Close #8588: autodoc: autodoc_type_aliases supports dotted name
It allows users to define an alias for a class with module name like `foo.bar.BazClass`.
1 parent b237e78 commit 28ab5f2

File tree

5 files changed

+147
-26
lines changed

5 files changed

+147
-26
lines changed

CHANGES

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ Features added
1616
* #8107: autodoc: Add ``class-doc-from`` option to :rst:dir:`autoclass`
1717
directive to control the content of the specific class like
1818
:confval:`autoclass_content`
19+
* #8588: autodoc: :confval:`autodoc_type_aliases` now supports dotted name. It
20+
allows you to define an alias for a class with module name like
21+
``foo.bar.BazClass``
1922
* #9129: html search: Show search summaries when html_copy_source = False
2023
* #9120: html theme: Eliminate prompt characters of code-block from copyable
2124
text

sphinx/util/inspect.py

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
import typing
1919
import warnings
2020
from functools import partial, partialmethod
21+
from importlib import import_module
2122
from inspect import Parameter, isclass, ismethod, ismethoddescriptor, ismodule # NOQA
2223
from io import StringIO
24+
from types import ModuleType
2325
from typing import Any, Callable, Dict, Mapping, Optional, Sequence, Tuple, Type, cast
2426

2527
from sphinx.deprecation import RemovedInSphinx50Warning
@@ -501,6 +503,78 @@ def __repr__(self) -> str:
501503
return self.value
502504

503505

506+
class TypeAliasForwardRef:
507+
"""Pseudo typing class for autodoc_type_aliases.
508+
509+
This avoids the error on evaluating the type inside `get_type_hints()`.
510+
"""
511+
def __init__(self, name: str) -> None:
512+
self.name = name
513+
514+
def __call__(self) -> None:
515+
# Dummy method to imitate special typing classes
516+
pass
517+
518+
def __eq__(self, other: Any) -> bool:
519+
return self.name == other
520+
521+
522+
class TypeAliasModule:
523+
"""Pseudo module class for autodoc_type_aliases."""
524+
525+
def __init__(self, modname: str, mapping: Dict[str, str]) -> None:
526+
self.__modname = modname
527+
self.__mapping = mapping
528+
529+
self.__module: Optional[ModuleType] = None
530+
531+
def __getattr__(self, name: str) -> Any:
532+
fullname = '.'.join(filter(None, [self.__modname, name]))
533+
if fullname in self.__mapping:
534+
# exactly matched
535+
return TypeAliasForwardRef(self.__mapping[fullname])
536+
else:
537+
prefix = fullname + '.'
538+
nested = {k: v for k, v in self.__mapping.items() if k.startswith(prefix)}
539+
if nested:
540+
# sub modules or classes found
541+
return TypeAliasModule(fullname, nested)
542+
else:
543+
# no sub modules or classes found.
544+
try:
545+
# return the real submodule if exists
546+
return import_module(fullname)
547+
except ImportError:
548+
# return the real class
549+
if self.__module is None:
550+
self.__module = import_module(self.__modname)
551+
552+
return getattr(self.__module, name)
553+
554+
555+
class TypeAliasNamespace(Dict[str, Any]):
556+
"""Pseudo namespace class for autodoc_type_aliases.
557+
558+
This enables to look up nested modules and classes like `mod1.mod2.Class`.
559+
"""
560+
561+
def __init__(self, mapping: Dict[str, str]) -> None:
562+
self.__mapping = mapping
563+
564+
def __getitem__(self, key: str) -> Any:
565+
if key in self.__mapping:
566+
# exactly matched
567+
return TypeAliasForwardRef(self.__mapping[key])
568+
else:
569+
prefix = key + '.'
570+
nested = {k: v for k, v in self.__mapping.items() if k.startswith(prefix)}
571+
if nested:
572+
# sub modules or classes found
573+
return TypeAliasModule(key, nested)
574+
else:
575+
raise KeyError
576+
577+
504578
def _should_unwrap(subject: Callable) -> bool:
505579
"""Check the function should be unwrapped on getting signature."""
506580
__globals__ = getglobals(subject)
@@ -549,12 +623,19 @@ def signature(subject: Callable, bound_method: bool = False, follow_wrapped: boo
549623

550624
try:
551625
# Resolve annotations using ``get_type_hints()`` and type_aliases.
552-
annotations = typing.get_type_hints(subject, None, type_aliases)
626+
localns = TypeAliasNamespace(type_aliases)
627+
annotations = typing.get_type_hints(subject, None, localns)
553628
for i, param in enumerate(parameters):
554629
if param.name in annotations:
555-
parameters[i] = param.replace(annotation=annotations[param.name])
630+
annotation = annotations[param.name]
631+
if isinstance(annotation, TypeAliasForwardRef):
632+
annotation = annotation.name
633+
parameters[i] = param.replace(annotation=annotation)
556634
if 'return' in annotations:
557-
return_annotation = annotations['return']
635+
if isinstance(annotations['return'], TypeAliasForwardRef):
636+
return_annotation = annotations['return'].name
637+
else:
638+
return_annotation = annotations['return']
558639
except Exception:
559640
# ``get_type_hints()`` does not support some kind of objects like partial,
560641
# ForwardRef and so on.

tests/roots/test-ext-autodoc/target/annotations.py renamed to tests/roots/test-ext-autodoc/target/autodoc_type_aliases.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import io
34
from typing import overload
45

56
myint = int
@@ -11,6 +12,10 @@
1112
variable2 = None # type: myint
1213

1314

15+
def read(r: io.BytesIO) -> io.StringIO:
16+
"""docstring"""
17+
18+
1419
def sum(x: myint, y: myint) -> myint:
1520
"""docstring"""
1621
return x + y

tests/test_ext_autodoc_configs.py

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -792,54 +792,60 @@ def test_autodoc_typehints_description_for_invalid_node(app):
792792
def test_autodoc_type_aliases(app):
793793
# default
794794
options = {"members": None}
795-
actual = do_autodoc(app, 'module', 'target.annotations', options)
795+
actual = do_autodoc(app, 'module', 'target.autodoc_type_aliases', options)
796796
assert list(actual) == [
797797
'',
798-
'.. py:module:: target.annotations',
798+
'.. py:module:: target.autodoc_type_aliases',
799799
'',
800800
'',
801801
'.. py:class:: Foo()',
802-
' :module: target.annotations',
802+
' :module: target.autodoc_type_aliases',
803803
'',
804804
' docstring',
805805
'',
806806
'',
807807
' .. py:attribute:: Foo.attr1',
808-
' :module: target.annotations',
808+
' :module: target.autodoc_type_aliases',
809809
' :type: int',
810810
'',
811811
' docstring',
812812
'',
813813
'',
814814
' .. py:attribute:: Foo.attr2',
815-
' :module: target.annotations',
815+
' :module: target.autodoc_type_aliases',
816816
' :type: int',
817817
'',
818818
' docstring',
819819
'',
820820
'',
821821
'.. py:function:: mult(x: int, y: int) -> int',
822822
' mult(x: float, y: float) -> float',
823-
' :module: target.annotations',
823+
' :module: target.autodoc_type_aliases',
824+
'',
825+
' docstring',
826+
'',
827+
'',
828+
'.. py:function:: read(r: _io.BytesIO) -> _io.StringIO',
829+
' :module: target.autodoc_type_aliases',
824830
'',
825831
' docstring',
826832
'',
827833
'',
828834
'.. py:function:: sum(x: int, y: int) -> int',
829-
' :module: target.annotations',
835+
' :module: target.autodoc_type_aliases',
830836
'',
831837
' docstring',
832838
'',
833839
'',
834840
'.. py:data:: variable',
835-
' :module: target.annotations',
841+
' :module: target.autodoc_type_aliases',
836842
' :type: int',
837843
'',
838844
' docstring',
839845
'',
840846
'',
841847
'.. py:data:: variable2',
842-
' :module: target.annotations',
848+
' :module: target.autodoc_type_aliases',
843849
' :type: int',
844850
' :value: None',
845851
'',
@@ -848,55 +854,62 @@ def test_autodoc_type_aliases(app):
848854
]
849855

850856
# define aliases
851-
app.config.autodoc_type_aliases = {'myint': 'myint'}
852-
actual = do_autodoc(app, 'module', 'target.annotations', options)
857+
app.config.autodoc_type_aliases = {'myint': 'myint',
858+
'io.StringIO': 'my.module.StringIO'}
859+
actual = do_autodoc(app, 'module', 'target.autodoc_type_aliases', options)
853860
assert list(actual) == [
854861
'',
855-
'.. py:module:: target.annotations',
862+
'.. py:module:: target.autodoc_type_aliases',
856863
'',
857864
'',
858865
'.. py:class:: Foo()',
859-
' :module: target.annotations',
866+
' :module: target.autodoc_type_aliases',
860867
'',
861868
' docstring',
862869
'',
863870
'',
864871
' .. py:attribute:: Foo.attr1',
865-
' :module: target.annotations',
872+
' :module: target.autodoc_type_aliases',
866873
' :type: myint',
867874
'',
868875
' docstring',
869876
'',
870877
'',
871878
' .. py:attribute:: Foo.attr2',
872-
' :module: target.annotations',
879+
' :module: target.autodoc_type_aliases',
873880
' :type: myint',
874881
'',
875882
' docstring',
876883
'',
877884
'',
878885
'.. py:function:: mult(x: myint, y: myint) -> myint',
879886
' mult(x: float, y: float) -> float',
880-
' :module: target.annotations',
887+
' :module: target.autodoc_type_aliases',
888+
'',
889+
' docstring',
890+
'',
891+
'',
892+
'.. py:function:: read(r: _io.BytesIO) -> my.module.StringIO',
893+
' :module: target.autodoc_type_aliases',
881894
'',
882895
' docstring',
883896
'',
884897
'',
885898
'.. py:function:: sum(x: myint, y: myint) -> myint',
886-
' :module: target.annotations',
899+
' :module: target.autodoc_type_aliases',
887900
'',
888901
' docstring',
889902
'',
890903
'',
891904
'.. py:data:: variable',
892-
' :module: target.annotations',
905+
' :module: target.autodoc_type_aliases',
893906
' :type: myint',
894907
'',
895908
' docstring',
896909
'',
897910
'',
898911
'.. py:data:: variable2',
899-
' :module: target.annotations',
912+
' :module: target.autodoc_type_aliases',
900913
' :type: myint',
901914
' :value: None',
902915
'',
@@ -911,10 +924,10 @@ def test_autodoc_type_aliases(app):
911924
confoverrides={'autodoc_typehints': "description",
912925
'autodoc_type_aliases': {'myint': 'myint'}})
913926
def test_autodoc_typehints_description_and_type_aliases(app):
914-
(app.srcdir / 'annotations.rst').write_text('.. autofunction:: target.annotations.sum')
927+
(app.srcdir / 'autodoc_type_aliases.rst').write_text('.. autofunction:: target.autodoc_type_aliases.sum')
915928
app.build()
916-
context = (app.outdir / 'annotations.txt').read_text()
917-
assert ('target.annotations.sum(x, y)\n'
929+
context = (app.outdir / 'autodoc_type_aliases.txt').read_text()
930+
assert ('target.autodoc_type_aliases.sum(x, y)\n'
918931
'\n'
919932
' docstring\n'
920933
'\n'

tests/test_util_inspect.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,26 @@
1919
import pytest
2020

2121
from sphinx.util import inspect
22-
from sphinx.util.inspect import stringify_signature
22+
from sphinx.util.inspect import TypeAliasNamespace, stringify_signature
23+
24+
25+
def test_TypeAliasNamespace():
26+
import logging.config
27+
type_alias = TypeAliasNamespace({'logging.Filter': 'MyFilter',
28+
'logging.Handler': 'MyHandler',
29+
'logging.handlers.SyslogHandler': 'MySyslogHandler'})
30+
31+
assert type_alias['logging'].Filter == 'MyFilter'
32+
assert type_alias['logging'].Handler == 'MyHandler'
33+
assert type_alias['logging'].handlers.SyslogHandler == 'MySyslogHandler'
34+
assert type_alias['logging'].Logger == logging.Logger
35+
assert type_alias['logging'].config == logging.config
36+
37+
with pytest.raises(KeyError):
38+
assert type_alias['log']
39+
40+
with pytest.raises(KeyError):
41+
assert type_alias['unknown']
2342

2443

2544
def test_signature():

0 commit comments

Comments
 (0)