Skip to content

Commit 2a8879c

Browse files
authored
Don't render Annotated by default; add include_extras (#8)
* Add include_extras and don't render Annotated by default * Add PR and issue to changelog * Add new environment to GitHub Actions matrix * Just use two steps * Do the matrix correctly * Try this * Just run the two commands * Remove extra curly brace --------- Co-authored-by: Jay Qi <jayqi@users.noreply.github.com>
1 parent fb025d1 commit 2a8879c

File tree

6 files changed

+133
-7
lines changed

6 files changed

+133
-7
lines changed

.github/workflows/tests.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
strategy:
3737
matrix:
3838
os: [ubuntu-latest, macos-latest, windows-latest]
39-
python-version: [3.8, 3.9, "3.10", "3.11", "3.12"]
39+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
4040

4141
steps:
4242
- uses: actions/checkout@v4
@@ -52,6 +52,13 @@ jobs:
5252
pipx install hatch
5353
5454
- name: Run tests
55+
if: matrix.python-version == '3.8'
56+
run: |
57+
hatch run tests.py${{ matrix.python-version }}-typing_extensions:run
58+
hatch run tests.py${{ matrix.python-version }}-no-typing_extensions:run
59+
60+
- name: Run tests
61+
if: matrix.python-version != '3.8'
5562
run: |
5663
hatch run tests.py${{ matrix.python-version }}:run
5764

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## v1.3.0 (2024-07-16)
4+
5+
- Changed how `typenames` handles type annotations that include `typing.Annotated` or `typing_extensions.Annotated`. ([PR #8](https://github.com/jayqi/typenames/pull/8), [Issue #7](https://github.com/jayqi/typenames/issues/7))
6+
- Added `include_extras` configuration option to `typenames` to control whether `Annotated` and metadata should be shown.
7+
- By default, `include_extras` is `False`, and `Annotated` and extra metadata will _not_ be rendered.
8+
39
## v1.2.0 (2024-03-19)
410

511
- Fixed the type signatures of `typenames` and `parse_type_tree` to reflect the typing of input type annotations, according to static type checkers. ([PR #6](https://github.com/jayqi/typenames/pull/6))

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,22 @@ typenames(
169169

170170
To remove all module names, you can use `REMOVE_ALL_MODULES`, which contains the pattern `re.compile(r"^(<?\w+>?\.)+")`.
171171

172+
### Annotated (`include_extras`)
173+
174+
This option controls whether to render `typing.Annotated` (or `typing_extensions.Annotated` if using Python 3.8) and the extra metadata. `Annotated` is a [typing special form](https://docs.python.org/3/library/typing.html#typing.Annotated) introduced in Python 3.9 and originally specified by [PEP 593](https://peps.python.org/pep-0593/). Many libraries like [Pydantic](https://docs.pydantic.dev/latest/concepts/fields/#using-annotated), [FastAPI](https://fastapi.tiangolo.com/python-types/#type-hints-with-metadata-annotations), and [Typer](https://typer.tiangolo.com/tutorial/arguments/optional/#an-alternative-cli-argument-declaration) use it to attach metadata to type annotations that are used at runtime.
175+
176+
By default, typenames will _not_ render `Annotated` and extra metadata. Set `include_extras=True` to render them.
177+
178+
```python
179+
from typing import Annotated
180+
from typenames import typenames
181+
182+
typenames(Annotated[int, "some metadata"])
183+
#> 'int'
184+
typenames(Annotated[int, "some metadata"], include_extras=True)
185+
#> "Annotated[int, 'some metadata']"
186+
```
187+
172188
---
173189

174190
<sup>Reproducible examples created by <a href="https://github.com/jayqi/reprexlite">reprexlite</a>.</sup>

pyproject.toml

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ classifiers = [
2222
"Programming Language :: Python :: 3.12",
2323
]
2424
requires-python = ">=3.8"
25-
dependencies = ["typing_extensions>4 ; python_version < '3.8'"]
2625

2726
[project.optional-dependencies]
2827
tests = ["pytest>=6"]
@@ -51,6 +50,11 @@ lint = [
5150
"ruff check",
5251
]
5352
typecheck = ["mypy --install-types --non-interactive"]
53+
clean-coverage = [
54+
"rm .coverage || true",
55+
"rm coverage.xml || true",
56+
"rm -r htmlcov || true",
57+
]
5458

5559
## TESTS ENVIRONMENT ##
5660

@@ -60,10 +64,22 @@ dependencies = ["coverage", "pytest-cov"]
6064
template = "tests"
6165

6266
[[tool.hatch.envs.tests.matrix]]
63-
python = ["3.8", "3.9", "3.10", "3.11", "3.12"]
67+
python = ["3.8"]
68+
typing_extensions = ["typing_extensions", "no-typing_extensions"]
69+
70+
[[tool.hatch.envs.tests.matrix]]
71+
python = ["3.9", "3.10", "3.11", "3.12"]
72+
73+
[tool.hatch.envs.tests.overrides]
74+
matrix.typing_extensions.dependencies = [
75+
{ value = "typing_extensions>4", if = ["typing_extensions"], python = "3.8" },
76+
]
77+
matrix.typing_extensions.env-vars = [
78+
{ key = "NO_TYPING_EXTENSIONS", value = "1", if = ["no-typing_extensions"] },
79+
]
6480

6581
[tool.hatch.envs.tests.scripts]
66-
run = "pytest tests --cov=typenames --cov-report=term --cov-report=html --cov-report=xml"
82+
run = "pytest tests -vv --cov=typenames --cov-report=term --cov-report=html --cov-report=xml --cov-append"
6783
run-debug = "run --pdb"
6884

6985
## DOCS ENVIRONMENT ##

tests/test_typenames.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import collections
22
import enum
3+
import os
34
import sys
45
import typing
56

@@ -9,6 +10,7 @@
910
DEFAULT_REMOVE_MODULES,
1011
REMOVE_ALL_MODULES,
1112
TypenamesConfig,
13+
is_annotated_special_form,
1214
is_standard_collection_type_alias,
1315
is_typing_module_collection_alias,
1416
is_union_or_operator,
@@ -17,6 +19,19 @@
1719
typenames,
1820
)
1921

22+
if sys.version_info >= (3, 9):
23+
from typing import Annotated
24+
25+
ANNOTATED_AVAILABLE = True
26+
else:
27+
try:
28+
from typing_extensions import Annotated
29+
30+
ANNOTATED_AVAILABLE = True
31+
except ModuleNotFoundError:
32+
ANNOTATED_AVAILABLE = False
33+
34+
2035
T = typing.TypeVar("T")
2136

2237

@@ -91,6 +106,17 @@ class MyTypedDict(typing.TypedDict):
91106
]
92107
)
93108

109+
if ANNOTATED_AVAILABLE:
110+
# typing.Annotated is available in Python 3.9, or backported with typing_extensions
111+
cases.extend(
112+
[
113+
(Annotated[str, "some metadata"], "str"),
114+
(Annotated[str, object()], "str"),
115+
(typing.Optional[Annotated[str, "some metadata"]], "Optional[str]"),
116+
(typing.Optional[Annotated[str, object()]], "Optional[str]"),
117+
]
118+
)
119+
94120
if sys.version_info >= (3, 10):
95121
# Python 3.10 adds union syntax with the | operator (bitwise or),
96122
# typing.Concatenate, typing.ParamSpec, typing.TypeAlias
@@ -252,6 +278,19 @@ def test_standard_collection_syntax_typing_module():
252278
assert typenames(list[int], standard_collection_syntax="typing_module") == "List[int]"
253279

254280

281+
if ANNOTATED_AVAILABLE:
282+
283+
def test_annotated_include_extras():
284+
"""Test that Annotated is included in the output when include_extras=True."""
285+
assert (
286+
typenames(Annotated[str, "some metadata"], include_extras=True)
287+
== "Annotated[str, 'some metadata']"
288+
)
289+
290+
obj = object()
291+
assert typenames(Annotated[str, obj], include_extras=True) == f"Annotated[str, {obj}]"
292+
293+
255294
def test_node_repr():
256295
assert repr(parse_type_tree(int)) == "<TypeNode <class 'int'>>"
257296
assert repr(parse_type_tree(typing.Any)) == "<TypeNode typing.Any>"
@@ -402,3 +441,20 @@ def test_nested_collection_types_nested_both():
402441
tp2 = dict[typing.Tuple[int, int], str]
403442
assert is_typing_module_collection_alias(tp2) is False
404443
assert is_standard_collection_type_alias(tp2) is True
444+
445+
446+
if ANNOTATED_AVAILABLE:
447+
448+
def test_annotated():
449+
assert is_annotated_special_form(Annotated[str, "some metadata"]) is True
450+
assert is_annotated_special_form(Annotated[str, object()]) is True
451+
452+
453+
@pytest.mark.skipif(
454+
os.getenv("NO_TYPING_EXTENSIONS") != "1",
455+
reason="Test for the no-typing_extensions environment only.",
456+
)
457+
def test_no_typing_extensions():
458+
"""typing_extensions should not be installed in test environment that shouldn't have it."""
459+
with pytest.raises(ModuleNotFoundError):
460+
import typing_extensions # noqa: F401

typenames/__init__.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,30 @@
77
import sys
88
import types
99
import typing
10-
from typing import Any, List, Optional, Union, get_args, get_origin
10+
from typing import Any, List, Optional, Union
1111

12-
__version__ = "1.2.0"
12+
__version__ = "1.3.0"
1313

1414
OR_OPERATOR_SUPPORTED = sys.version_info >= (3, 10)
1515
"""Flag for whether PEP 604's | operator (bitwise or) between types is supported."""
1616

1717
LITERAL_TYPE_SUPPORTED = sys.version_info >= (3, 8)
18-
"""(DEPRECATED) Flag for whether PEP 586's typing.Literal is supported. typenames no longer
18+
"""(DEPRECATED) Flag for whether PEP 586's typing.Literal is supported. typenames no longer
1919
supports Python versions where this is false.
2020
"""
2121

22+
# Annotated was introduced in Python 3.9 but backported in typing_extensions
23+
# Need to use get_args and get_origin from typing_extensions to work correctly with
24+
# typing_extensions.Annotated
25+
if sys.version_info >= (3, 9):
26+
from typing import Annotated, get_args, get_origin
27+
else:
28+
try:
29+
from typing_extensions import Annotated, get_args, get_origin
30+
except ModuleNotFoundError:
31+
Annotated = object()
32+
from typing import get_args, get_origin
33+
2234
# TypeVar for type annotations
2335
# Most typing special forms have type 'object'
2436
# There is a stalled proposal for TypeForm: https://github.com/python/mypy/issues/9773
@@ -80,6 +92,7 @@ class TypenamesConfig:
8092
remove_modules: List[Union[str, re.Pattern]] = dataclasses.field(
8193
default_factory=lambda: list(DEFAULT_REMOVE_MODULES)
8294
)
95+
include_extras: bool = False
8396

8497
def __post_init__(self):
8598
self.union_syntax = UnionSyntax(self.union_syntax)
@@ -257,6 +270,13 @@ def __str__(self) -> str:
257270
else:
258271
origin_module_prefix = "typing."
259272
origin_name = self.tp._name
273+
# Case: typing.Annotated
274+
elif is_annotated_special_form(self.tp):
275+
if self.config.include_extras:
276+
origin_module_prefix = "typing."
277+
origin_name = "Annotated"
278+
else:
279+
return str(arg_nodes[0])
260280
# Case: Some other generic type
261281
else:
262282
if hasattr(self.origin, "__module__"):
@@ -449,3 +469,8 @@ def is_typing_module_collection_alias(tp: type) -> bool:
449469
return (
450470
get_origin(tp) in STANDARD_COLLECTION_CLASSES and type(tp) is typing._GenericAlias # type: ignore[attr-defined]
451471
)
472+
473+
474+
def is_annotated_special_form(tp: type) -> bool:
475+
"""Check if type annotation is the typing.Annotated special form."""
476+
return get_origin(tp) is Annotated

0 commit comments

Comments
 (0)