Skip to content

Commit

Permalink
Render PEP-695 type aliases as TypeAlias assignments
Browse files Browse the repository at this point in the history
Partially addresses #414
  • Loading branch information
AWhetter committed Apr 2, 2024
1 parent 007077a commit bc71226
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 36 deletions.
57 changes: 35 additions & 22 deletions autoapi/_astroid_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,27 +116,39 @@ def get_full_basenames(node):
yield _resolve_annotation(base)


def _get_const_values(node):
value = None

if isinstance(node, (astroid.nodes.List, astroid.nodes.Tuple)):
new_value = []
for element in node.elts:
if isinstance(element, astroid.nodes.Const):
new_value.append(element.value)
elif isinstance(element, (astroid.nodes.List, astroid.nodes.Tuple)):
new_value.append(_get_const_values(element))
else:
break
else:
value = new_value
def _get_const_value(node):
if isinstance(node, astroid.nodes.Const):
if isinstance(node.value, str) and "\n" in node.value:
return '"""{0}"""'.format(node.value)

class NotConstException(Exception):
pass

def _inner(node):
if isinstance(node, (astroid.nodes.List, astroid.nodes.Tuple)):
new_value = []
for element in node.elts:
new_value.append(_inner(element))

if isinstance(node, astroid.nodes.Tuple):
return tuple(new_value)

if isinstance(node, astroid.nodes.Tuple):
value = tuple(new_value)
elif isinstance(node, astroid.nodes.Const):
value = node.value
return new_value
elif isinstance(node, astroid.nodes.Const):
# Don't allow multi-line strings inside a data structure.
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):
Expand All @@ -149,8 +161,9 @@ def get_assign_value(node):
to get the assignment value from.
Returns:
tuple(str, object or None) or None: The name that is assigned
to, and the value assigned to the name (if it can be converted).
tuple(str, str or None) or None: The name that is assigned
to, and the string representation of the value assigned to the name
(if it can be converted).
"""
try:
targets = node.targets
Expand All @@ -165,7 +178,7 @@ def get_assign_value(node):
name = target.attrname
else:
return None
return (name, _get_const_values(node.value))
return (name, _get_const_value(node.value))

return None

Expand Down
31 changes: 31 additions & 0 deletions autoapi/_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ def _parse_assign(self, node):
value = assign_value[1]

annotation = _astroid_utils.get_assign_annotation(node)
if annotation in ("TypeAlias", "typing.TypeAlias"):
value = node.value.as_string()

data = {
"type": type_,
Expand Down Expand Up @@ -274,6 +276,35 @@ def parse_module(self, node):

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):
data = {}

Expand Down
10 changes: 3 additions & 7 deletions autoapi/templates/python/data.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
{% endif %}
{% 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

.. raw:: html
Expand All @@ -20,18 +20,14 @@

.. code-block:: python
"""{{ obj.value|indent(width=6,blank=true) }}"""
{{ obj.value|indent(width=6,blank=true) }}
.. raw:: html

</details>

{% else %}
{% if obj.value is string %}
:value: {{ "%r" % obj.value|string|truncate(100) }}
{% else %}
:value: {{ obj.value|string|truncate(100) }}
{% endif %}
:value: {{ obj.value|truncate(100) }}
{% endif %}
{% endif %}

Expand Down
3 changes: 3 additions & 0 deletions docs/changes/414.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Render PEP-695 type aliases as TypeAlias assignments.

Values are also always rendered for TypeAlises and PEP-695 type aliases.
20 changes: 20 additions & 0 deletions tests/python/pep695/conf.py
Original file line number Diff line number Diff line change
@@ -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"
4 changes: 4 additions & 0 deletions tests/python/pep695/example/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from typing import TypeAlias

MyTypeAliasA: TypeAlias = tuple[str, int]
type MyTypeAliasB = tuple[str, int]
26 changes: 26 additions & 0 deletions tests/python/pep695/index.rst
Original file line number Diff line number Diff line change
@@ -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`

8 changes: 4 additions & 4 deletions tests/python/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def test_parses_all(self):
"""
data = self.parse(source)[0]
assert data["name"] == "__all__"
assert data["value"] == ["Foo", 5.0]
assert data["value"] == "['Foo', 5.0]"

def test_parses_all_multiline(self):
source = """
Expand All @@ -35,23 +35,23 @@ def test_parses_all_multiline(self):
]
"""
data = self.parse(source)[0]
assert data["value"] == ["foo", "bar"]
assert data["value"] == "['foo', 'bar']"

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

def test_parses_list(self):
name = "__all__"
value = [1, 2, 3, 4]
value = "[1, 2, 3, 4]"
source = "{} = {}".format(name, value)
data = self.parse(source)[0]
assert data["name"] == name
assert data["value"] == value

def test_parses_nested_list(self):
name = "__all__"
value = [[1, 2], [3, 4]]
value = "[[1, 2], [3, 4]]"
source = "{} = {}".format(name, value)
data = self.parse(source)[0]
assert data["name"] == name
Expand Down
28 changes: 28 additions & 0 deletions tests/python/test_pyintegration.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,34 @@ def test_integration(self, parse):
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):
confoverrides = {
"exclude_patterns": ["manualapi.rst"],
Expand Down
11 changes: 9 additions & 2 deletions tests/test_astroid_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,17 @@ class ThisClass({}): #@
@pytest.mark.parametrize(
("source", "expected"),
[
('a = "a"', ("a", "a")),
("a = 1", ("a", 1)),
('a = "a"', ("a", "'a'")),
("a = 1", ("a", "1")),
("a, b, c = (1, 2, 3)", 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):
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ commands =
pytest {posargs}

[testenv:formatting]
basepython = python3
basepython = python312
skip_install = true
deps =
black
Expand Down

0 comments on commit bc71226

Please sign in to comment.