Compare commits

...

2 Commits

Author SHA1 Message Date
Ashley Whetter
3b037c7643 Moved bugfix announcement into a separate news item
Closes #224
2024-04-01 22:29:52 -07:00
Ashley Whetter
bc71226c3b Render PEP-695 type aliases as TypeAlias assignments
Partially addresses #414
2024-04-01 22:24:36 -07:00
12 changed files with 162 additions and 35 deletions

View File

@ -116,27 +116,39 @@ def get_full_basenames(node):
yield _resolve_annotation(base)
def _get_const_values(node):
value = None
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)
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
class NotConstException(Exception):
pass
if isinstance(node, astroid.nodes.Tuple):
value = tuple(new_value)
elif isinstance(node, astroid.nodes.Const):
value = node.value
def _inner(node):
if isinstance(node, (astroid.nodes.List, astroid.nodes.Tuple)):
new_value = []
for element in node.elts:
new_value.append(_inner(element))
return value
if isinstance(node, astroid.nodes.Tuple):
return tuple(new_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 repr(result)
def get_assign_value(node):
@ -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
@ -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

View File

@ -91,6 +91,8 @@ class Parser:
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_,
@ -274,6 +276,35 @@ class Parser:
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 = {}

View File

@ -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
@ -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 %}

1
docs/changes/224.bugfix Normal file
View File

@ -0,0 +1 @@
Values are always rendered for TypeAlises and PEP-695 type aliases.

1
docs/changes/414.feature Normal file
View File

@ -0,0 +1 @@
Render PEP-695 type aliases as TypeAlias assignments.

View File

@ -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"

View File

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

View File

@ -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`

View File

@ -25,7 +25,7 @@ class TestPythonParser:
"""
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 = """
@ -35,7 +35,7 @@ class TestPythonParser:
]
"""
data = self.parse(source)[0]
assert data["value"] == ["foo", "bar"]
assert data["value"] == "['foo', 'bar']"
def test_parses_name(self):
source = "foo.bar"
@ -43,7 +43,7 @@ class TestPythonParser:
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
@ -51,7 +51,7 @@ class TestPythonParser:
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

View File

@ -587,6 +587,34 @@ class TestPipeUnionModule:
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"],

View File

@ -92,10 +92,17 @@ class TestAstroidUtils:
@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):

View File

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