Render PEP-695 type aliases as TypeAlias assignments

Partially addresses #414
pull/431/head
Ashley Whetter 2 months ago
parent 007077a7db
commit bc71226c3b

@ -116,27 +116,39 @@ def get_full_basenames(node):
yield _resolve_annotation(base) yield _resolve_annotation(base)
def _get_const_values(node): def _get_const_value(node):
value = None if isinstance(node, astroid.nodes.Const):
if isinstance(node.value, str) and "\n" in node.value:
if isinstance(node, (astroid.nodes.List, astroid.nodes.Tuple)): return '"""{0}"""'.format(node.value)
new_value = []
for element in node.elts: class NotConstException(Exception):
if isinstance(element, astroid.nodes.Const): pass
new_value.append(element.value)
elif isinstance(element, (astroid.nodes.List, astroid.nodes.Tuple)): def _inner(node):
new_value.append(_get_const_values(element)) if isinstance(node, (astroid.nodes.List, astroid.nodes.Tuple)):
else: new_value = []
break for element in node.elts:
else: new_value.append(_inner(element))
value = new_value
if isinstance(node, astroid.nodes.Tuple):
return tuple(new_value)
if isinstance(node, astroid.nodes.Tuple): return new_value
value = tuple(new_value) elif isinstance(node, astroid.nodes.Const):
elif isinstance(node, astroid.nodes.Const): # Don't allow multi-line strings inside a data structure.
value = node.value if isinstance(node.value, str) and "\n" in node.value:
raise NotConstException()
return node.value
raise NotConstException()
try:
result = _inner(node)
except NotConstException:
return None
return value return repr(result)
def get_assign_value(node): def get_assign_value(node):
@ -149,8 +161,9 @@ def get_assign_value(node):
to get the assignment value from. to get the assignment value from.
Returns: Returns:
tuple(str, object or None) or None: The name that is assigned tuple(str, str or None) or None: The name that is assigned
to, and the value assigned to the name (if it can be converted). to, and the string representation of the value assigned to the name
(if it can be converted).
""" """
try: try:
targets = node.targets targets = node.targets
@ -165,7 +178,7 @@ def get_assign_value(node):
name = target.attrname name = target.attrname
else: else:
return None return None
return (name, _get_const_values(node.value)) return (name, _get_const_value(node.value))
return None return None

@ -91,6 +91,8 @@ class Parser:
value = assign_value[1] value = assign_value[1]
annotation = _astroid_utils.get_assign_annotation(node) annotation = _astroid_utils.get_assign_annotation(node)
if annotation in ("TypeAlias", "typing.TypeAlias"):
value = node.value.as_string()
data = { data = {
"type": type_, "type": type_,
@ -274,6 +276,35 @@ class Parser:
return data return data
def parse_typealias(self, node):
doc = ""
doc_node = node.next_sibling()
if isinstance(doc_node, astroid.nodes.Expr) and isinstance(
doc_node.value, astroid.nodes.Const
):
doc = doc_node.value.value
if isinstance(node.name, astroid.nodes.AssignName):
name = node.name.name
elif isinstance(node.name, astroid.nodes.AssignAttr):
name = node.name.attrname
else:
return []
data = {
"type": "data",
"name": name,
"qual_name": self._get_qual_name(name),
"full_name": self._get_full_name(name),
"doc": _prepare_docstring(doc),
"value": node.value.as_string(),
"from_line_no": node.fromlineno,
"to_line_no": node.tolineno,
"annotation": "TypeAlias",
}
return [data]
def parse(self, node): def parse(self, node):
data = {} data = {}

@ -11,7 +11,7 @@
{% endif %} {% endif %}
{% if obj.value is not none %} {% if obj.value is not none %}
{% if obj.value is string and obj.value.splitlines()|count > 1 %} {% if obj.value.splitlines()|count > 1 %}
:value: Multiline-String :value: Multiline-String
.. raw:: html .. raw:: html
@ -20,18 +20,14 @@
.. code-block:: python .. code-block:: python
"""{{ obj.value|indent(width=6,blank=true) }}""" {{ obj.value|indent(width=6,blank=true) }}
.. raw:: html .. raw:: html
</details> </details>
{% else %} {% else %}
{% if obj.value is string %} :value: {{ obj.value|truncate(100) }}
:value: {{ "%r" % obj.value|string|truncate(100) }}
{% else %}
:value: {{ obj.value|string|truncate(100) }}
{% endif %}
{% endif %} {% endif %}
{% endif %} {% endif %}

@ -0,0 +1,3 @@
Render PEP-695 type aliases as TypeAlias assignments.
Values are also always rendered for TypeAlises and PEP-695 type aliases.

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
templates_path = ["_templates"]
source_suffix = ".rst"
master_doc = "index"
project = "pyexample"
copyright = "2015, readthedocs"
author = "readthedocs"
version = "0.1"
release = "0.1"
language = "en"
exclude_patterns = ["_build"]
pygments_style = "sphinx"
todo_include_todos = False
html_theme = "alabaster"
htmlhelp_basename = "pyexampledoc"
extensions = ["sphinx.ext.intersphinx", "sphinx.ext.autodoc", "autoapi.extension"]
intersphinx_mapping = {"python": ("https://docs.python.org/3.10", None)}
autoapi_dirs = ["example"]
autoapi_file_pattern = "*.py"

@ -0,0 +1,4 @@
from typing import TypeAlias
MyTypeAliasA: TypeAlias = tuple[str, int]
type MyTypeAliasB = tuple[str, int]

@ -0,0 +1,26 @@
.. pyexample documentation master file, created by
sphinx-quickstart on Fri May 29 13:34:37 2015.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to pyexample's documentation!
=====================================
.. toctree::
autoapi/index
Contents:
.. toctree::
:maxdepth: 2
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

@ -25,7 +25,7 @@ class TestPythonParser:
""" """
data = self.parse(source)[0] data = self.parse(source)[0]
assert data["name"] == "__all__" assert data["name"] == "__all__"
assert data["value"] == ["Foo", 5.0] assert data["value"] == "['Foo', 5.0]"
def test_parses_all_multiline(self): def test_parses_all_multiline(self):
source = """ source = """
@ -35,7 +35,7 @@ class TestPythonParser:
] ]
""" """
data = self.parse(source)[0] data = self.parse(source)[0]
assert data["value"] == ["foo", "bar"] assert data["value"] == "['foo', 'bar']"
def test_parses_name(self): def test_parses_name(self):
source = "foo.bar" source = "foo.bar"
@ -43,7 +43,7 @@ class TestPythonParser:
def test_parses_list(self): def test_parses_list(self):
name = "__all__" name = "__all__"
value = [1, 2, 3, 4] value = "[1, 2, 3, 4]"
source = "{} = {}".format(name, value) source = "{} = {}".format(name, value)
data = self.parse(source)[0] data = self.parse(source)[0]
assert data["name"] == name assert data["name"] == name
@ -51,7 +51,7 @@ class TestPythonParser:
def test_parses_nested_list(self): def test_parses_nested_list(self):
name = "__all__" name = "__all__"
value = [[1, 2], [3, 4]] value = "[[1, 2], [3, 4]]"
source = "{} = {}".format(name, value) source = "{} = {}".format(name, value)
data = self.parse(source)[0] data = self.parse(source)[0]
assert data["name"] == name assert data["name"] == name

@ -587,6 +587,34 @@ class TestPipeUnionModule:
assert links[1].text == "None" assert links[1].text == "None"
@pytest.mark.skipif(
sys.version_info < (3, 12), reason="PEP-695 support requires Python >=3.12"
)
class TestPEP695:
@pytest.fixture(autouse=True, scope="class")
def built(self, builder):
builder("pep695", warningiserror=True)
def test_integration(self, parse):
example_file = parse("_build/html/autoapi/example/index.html")
alias = example_file.find(id="example.MyTypeAliasA")
properties = alias.find_all(class_="property")
assert len(properties) == 2
annotation = properties[0].text
assert annotation == ": TypeAlias"
value = properties[1].text
assert value == " = tuple[str, int]"
alias = example_file.find(id="example.MyTypeAliasB")
properties = alias.find_all(class_="property")
assert len(properties) == 2
annotation = properties[0].text
assert annotation == ": TypeAlias"
value = properties[1].text
assert value == " = tuple[str, int]"
def test_napoleon_integration_loaded(builder, parse): def test_napoleon_integration_loaded(builder, parse):
confoverrides = { confoverrides = {
"exclude_patterns": ["manualapi.rst"], "exclude_patterns": ["manualapi.rst"],

@ -92,10 +92,17 @@ class TestAstroidUtils:
@pytest.mark.parametrize( @pytest.mark.parametrize(
("source", "expected"), ("source", "expected"),
[ [
('a = "a"', ("a", "a")), ('a = "a"', ("a", "'a'")),
("a = 1", ("a", 1)), ("a = 1", ("a", "1")),
("a, b, c = (1, 2, 3)", None), ("a, b, c = (1, 2, 3)", None),
("a = b = 1", None), ("a = b = 1", None),
("a = [1, 2, [3, 4]]", ("a", "[1, 2, [3, 4]]")),
("a = [1, 2, variable[subscript]]", ("a", None)),
('a = """multiline\nstring"""', ("a", '"""multiline\nstring"""')),
('a = ["""multiline\nstring"""]', ("a", None)),
("a = (1, 2, 3)", ("a", "(1, 2, 3)")),
("a = (1, 'two', 3)", ("a", "(1, 'two', 3)")),
("a = None", ("a", "None")),
], ],
) )
def test_can_get_assign_values(self, source, expected): def test_can_get_assign_values(self, source, expected):

@ -25,7 +25,7 @@ commands =
pytest {posargs} pytest {posargs}
[testenv:formatting] [testenv:formatting]
basepython = python3 basepython = python312
skip_install = true skip_install = true
deps = deps =
black black

Loading…
Cancel
Save