Skip to content

Commit bc71226

Browse files
committed
Render PEP-695 type aliases as TypeAlias assignments
Partially addresses #414
1 parent 007077a commit bc71226

File tree

11 files changed

+164
-36
lines changed

11 files changed

+164
-36
lines changed

autoapi/_astroid_utils.py

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -116,27 +116,39 @@ def get_full_basenames(node):
116116
yield _resolve_annotation(base)
117117

118118

119-
def _get_const_values(node):
120-
value = None
121-
122-
if isinstance(node, (astroid.nodes.List, astroid.nodes.Tuple)):
123-
new_value = []
124-
for element in node.elts:
125-
if isinstance(element, astroid.nodes.Const):
126-
new_value.append(element.value)
127-
elif isinstance(element, (astroid.nodes.List, astroid.nodes.Tuple)):
128-
new_value.append(_get_const_values(element))
129-
else:
130-
break
131-
else:
132-
value = new_value
119+
def _get_const_value(node):
120+
if isinstance(node, astroid.nodes.Const):
121+
if isinstance(node.value, str) and "\n" in node.value:
122+
return '"""{0}"""'.format(node.value)
123+
124+
class NotConstException(Exception):
125+
pass
126+
127+
def _inner(node):
128+
if isinstance(node, (astroid.nodes.List, astroid.nodes.Tuple)):
129+
new_value = []
130+
for element in node.elts:
131+
new_value.append(_inner(element))
132+
133+
if isinstance(node, astroid.nodes.Tuple):
134+
return tuple(new_value)
133135

134-
if isinstance(node, astroid.nodes.Tuple):
135-
value = tuple(new_value)
136-
elif isinstance(node, astroid.nodes.Const):
137-
value = node.value
136+
return new_value
137+
elif isinstance(node, astroid.nodes.Const):
138+
# Don't allow multi-line strings inside a data structure.
139+
if isinstance(node.value, str) and "\n" in node.value:
140+
raise NotConstException()
141+
142+
return node.value
143+
144+
raise NotConstException()
145+
146+
try:
147+
result = _inner(node)
148+
except NotConstException:
149+
return None
138150

139-
return value
151+
return repr(result)
140152

141153

142154
def get_assign_value(node):
@@ -149,8 +161,9 @@ def get_assign_value(node):
149161
to get the assignment value from.
150162
151163
Returns:
152-
tuple(str, object or None) or None: The name that is assigned
153-
to, and the value assigned to the name (if it can be converted).
164+
tuple(str, str or None) or None: The name that is assigned
165+
to, and the string representation of the value assigned to the name
166+
(if it can be converted).
154167
"""
155168
try:
156169
targets = node.targets
@@ -165,7 +178,7 @@ def get_assign_value(node):
165178
name = target.attrname
166179
else:
167180
return None
168-
return (name, _get_const_values(node.value))
181+
return (name, _get_const_value(node.value))
169182

170183
return None
171184

autoapi/_parser.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ def _parse_assign(self, node):
9191
value = assign_value[1]
9292

9393
annotation = _astroid_utils.get_assign_annotation(node)
94+
if annotation in ("TypeAlias", "typing.TypeAlias"):
95+
value = node.value.as_string()
9496

9597
data = {
9698
"type": type_,
@@ -274,6 +276,35 @@ def parse_module(self, node):
274276

275277
return data
276278

279+
def parse_typealias(self, node):
280+
doc = ""
281+
doc_node = node.next_sibling()
282+
if isinstance(doc_node, astroid.nodes.Expr) and isinstance(
283+
doc_node.value, astroid.nodes.Const
284+
):
285+
doc = doc_node.value.value
286+
287+
if isinstance(node.name, astroid.nodes.AssignName):
288+
name = node.name.name
289+
elif isinstance(node.name, astroid.nodes.AssignAttr):
290+
name = node.name.attrname
291+
else:
292+
return []
293+
294+
data = {
295+
"type": "data",
296+
"name": name,
297+
"qual_name": self._get_qual_name(name),
298+
"full_name": self._get_full_name(name),
299+
"doc": _prepare_docstring(doc),
300+
"value": node.value.as_string(),
301+
"from_line_no": node.fromlineno,
302+
"to_line_no": node.tolineno,
303+
"annotation": "TypeAlias",
304+
}
305+
306+
return [data]
307+
277308
def parse(self, node):
278309
data = {}
279310

autoapi/templates/python/data.rst

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
{% endif %}
1212
{% if obj.value is not none %}
1313

14-
{% if obj.value is string and obj.value.splitlines()|count > 1 %}
14+
{% if obj.value.splitlines()|count > 1 %}
1515
:value: Multiline-String
1616

1717
.. raw:: html
@@ -20,18 +20,14 @@
2020

2121
.. code-block:: python
2222
23-
"""{{ obj.value|indent(width=6,blank=true) }}"""
23+
{{ obj.value|indent(width=6,blank=true) }}
2424
2525
.. raw:: html
2626

2727
</details>
2828

2929
{% else %}
30-
{% if obj.value is string %}
31-
:value: {{ "%r" % obj.value|string|truncate(100) }}
32-
{% else %}
33-
:value: {{ obj.value|string|truncate(100) }}
34-
{% endif %}
30+
:value: {{ obj.value|truncate(100) }}
3531
{% endif %}
3632
{% endif %}
3733

docs/changes/414.feature

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Render PEP-695 type aliases as TypeAlias assignments.
2+
3+
Values are also always rendered for TypeAlises and PEP-695 type aliases.

tests/python/pep695/conf.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# -*- coding: utf-8 -*-
2+
3+
templates_path = ["_templates"]
4+
source_suffix = ".rst"
5+
master_doc = "index"
6+
project = "pyexample"
7+
copyright = "2015, readthedocs"
8+
author = "readthedocs"
9+
version = "0.1"
10+
release = "0.1"
11+
language = "en"
12+
exclude_patterns = ["_build"]
13+
pygments_style = "sphinx"
14+
todo_include_todos = False
15+
html_theme = "alabaster"
16+
htmlhelp_basename = "pyexampledoc"
17+
extensions = ["sphinx.ext.intersphinx", "sphinx.ext.autodoc", "autoapi.extension"]
18+
intersphinx_mapping = {"python": ("https://docs.python.org/3.10", None)}
19+
autoapi_dirs = ["example"]
20+
autoapi_file_pattern = "*.py"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from typing import TypeAlias
2+
3+
MyTypeAliasA: TypeAlias = tuple[str, int]
4+
type MyTypeAliasB = tuple[str, int]

tests/python/pep695/index.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
.. pyexample documentation master file, created by
2+
sphinx-quickstart on Fri May 29 13:34:37 2015.
3+
You can adapt this file completely to your liking, but it should at least
4+
contain the root `toctree` directive.
5+
6+
Welcome to pyexample's documentation!
7+
=====================================
8+
9+
.. toctree::
10+
11+
autoapi/index
12+
13+
Contents:
14+
15+
.. toctree::
16+
:maxdepth: 2
17+
18+
19+
20+
Indices and tables
21+
==================
22+
23+
* :ref:`genindex`
24+
* :ref:`modindex`
25+
* :ref:`search`
26+

tests/python/test_parser.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def test_parses_all(self):
2525
"""
2626
data = self.parse(source)[0]
2727
assert data["name"] == "__all__"
28-
assert data["value"] == ["Foo", 5.0]
28+
assert data["value"] == "['Foo', 5.0]"
2929

3030
def test_parses_all_multiline(self):
3131
source = """
@@ -35,23 +35,23 @@ def test_parses_all_multiline(self):
3535
]
3636
"""
3737
data = self.parse(source)[0]
38-
assert data["value"] == ["foo", "bar"]
38+
assert data["value"] == "['foo', 'bar']"
3939

4040
def test_parses_name(self):
4141
source = "foo.bar"
4242
assert self.parse(source) == {}
4343

4444
def test_parses_list(self):
4545
name = "__all__"
46-
value = [1, 2, 3, 4]
46+
value = "[1, 2, 3, 4]"
4747
source = "{} = {}".format(name, value)
4848
data = self.parse(source)[0]
4949
assert data["name"] == name
5050
assert data["value"] == value
5151

5252
def test_parses_nested_list(self):
5353
name = "__all__"
54-
value = [[1, 2], [3, 4]]
54+
value = "[[1, 2], [3, 4]]"
5555
source = "{} = {}".format(name, value)
5656
data = self.parse(source)[0]
5757
assert data["name"] == name

tests/python/test_pyintegration.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,34 @@ def test_integration(self, parse):
587587
assert links[1].text == "None"
588588

589589

590+
@pytest.mark.skipif(
591+
sys.version_info < (3, 12), reason="PEP-695 support requires Python >=3.12"
592+
)
593+
class TestPEP695:
594+
@pytest.fixture(autouse=True, scope="class")
595+
def built(self, builder):
596+
builder("pep695", warningiserror=True)
597+
598+
def test_integration(self, parse):
599+
example_file = parse("_build/html/autoapi/example/index.html")
600+
601+
alias = example_file.find(id="example.MyTypeAliasA")
602+
properties = alias.find_all(class_="property")
603+
assert len(properties) == 2
604+
annotation = properties[0].text
605+
assert annotation == ": TypeAlias"
606+
value = properties[1].text
607+
assert value == " = tuple[str, int]"
608+
609+
alias = example_file.find(id="example.MyTypeAliasB")
610+
properties = alias.find_all(class_="property")
611+
assert len(properties) == 2
612+
annotation = properties[0].text
613+
assert annotation == ": TypeAlias"
614+
value = properties[1].text
615+
assert value == " = tuple[str, int]"
616+
617+
590618
def test_napoleon_integration_loaded(builder, parse):
591619
confoverrides = {
592620
"exclude_patterns": ["manualapi.rst"],

tests/test_astroid_utils.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,17 @@ class ThisClass({}): #@
9292
@pytest.mark.parametrize(
9393
("source", "expected"),
9494
[
95-
('a = "a"', ("a", "a")),
96-
("a = 1", ("a", 1)),
95+
('a = "a"', ("a", "'a'")),
96+
("a = 1", ("a", "1")),
9797
("a, b, c = (1, 2, 3)", None),
9898
("a = b = 1", None),
99+
("a = [1, 2, [3, 4]]", ("a", "[1, 2, [3, 4]]")),
100+
("a = [1, 2, variable[subscript]]", ("a", None)),
101+
('a = """multiline\nstring"""', ("a", '"""multiline\nstring"""')),
102+
('a = ["""multiline\nstring"""]', ("a", None)),
103+
("a = (1, 2, 3)", ("a", "(1, 2, 3)")),
104+
("a = (1, 'two', 3)", ("a", "(1, 'two', 3)")),
105+
("a = None", ("a", "None")),
99106
],
100107
)
101108
def test_can_get_assign_values(self, source, expected):

0 commit comments

Comments
 (0)