Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add typing_extensions.get_annotations #423

Merged
merged 9 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Unreleased

- Add `typing_extensions.get_annotations`, a backport of
`inspect.get_annotations` that adds features specified
by PEP 649. Patch by Jelle Zijlstra.
- Fix regression in v4.12.0 where specialization of certain
generics with an overridden `__eq__` method would raise errors.
Patch by Jelle Zijlstra.
Expand Down
58 changes: 58 additions & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,25 @@ Functions

.. versionadded:: 4.2.0

.. function:: get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE)

See :py:func:`inspect.get_annotations`. In the standard library since Python 3.10.

``typing_extensions`` adds the keyword argument ``format``, as specified
by :pep:`649`. The supported formats are listed in the :class:`Format` enum.
The default format, :attr:`Format.VALUE`, behaves the same across all versions.
For the other two formats, ``typing_extensions`` provides a rough approximation
of the :pep:`649` behavior on versions of Python that do not support it.

The purpose of this backport is to allow users who would like to use
:attr:`Format.FORWARDREF` or :attr:`Format.SOURCE` semantics once
:pep:`649` is implemented, but who also
want to support earlier Python versions, to simply write::

typing_extensions.get_annotations(obj, format=Format.FORWARDREF)

.. versionadded:: 4.13.0

.. function:: get_args(tp)

See :py:func:`typing.get_args`. In ``typing`` since 3.8.
Expand Down Expand Up @@ -857,6 +876,45 @@ Functions

.. versionadded:: 4.1.0

Enums
~~~~~

.. class:: Format

The formats for evaluating annotations introduced by :pep:`649`.
Members of this enum can be passed as the *format* argument
to :func:`get_annotations`.

The final place of this enum in the standard library has not yet
been determined (see :pep:`649` and :pep:`749`), but the names
and integer values are stable and will continue to work.

.. attribute:: VALUE

Equal to 1. The default value. The function will return the conventional Python values
for the annotations. This format is identical to the return value for
the function under earlier versions of Python.

.. attribute:: FORWARDREF

Equal to 2. When :pep:`649` is implemented, this format will attempt to return the
conventional Python values for the annotations. However, if it encounters
an undefined name, it dynamically creates a proxy object (a ForwardRef)
that substitutes for that value in the expression.

``typing_extensions`` emulates this value on versions of Python which do
not support :pep:`649` by returning the same value as for ``VALUE`` semantics.

.. attribute:: SOURCE

Equal to 3. When :pep:`649` is implemented, this format will produce an annotation
dictionary where the values have been replaced by strings containing
an approximation of the original source code for the annotation expressions.

``typing_extensions`` emulates this by evaluating the annotations using
``VALUE`` semantics and then stringifying the results.

.. versionadded:: 4.13.0

Annotation metadata
~~~~~~~~~~~~~~~~~~~
Expand Down
12 changes: 9 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ name = "Guido van Rossum, Jukka Lehtosalo, Łukasz Langa, Michael Lee"
email = "[email protected]"

[tool.flit.sdist]
include = ["CHANGELOG.md", "README.md", "tox.ini", "*/*test*.py"]
include = ["CHANGELOG.md", "README.md", "tox.ini", "*/*test*.py", "*/*inspect*.py"]
exclude = []

[tool.ruff]
Expand All @@ -83,7 +83,7 @@ select = [

# Ignore various "modernization" rules that tell you off for importing/using
# deprecated things from the typing module, etc.
ignore = ["UP006", "UP007", "UP013", "UP014", "UP019", "UP035", "UP038"]
ignore = ["UP006", "UP007", "UP013", "UP014", "UP019", "UP035", "UP037", "UP038"]

[tool.ruff.lint.per-file-ignores]
"!src/typing_extensions.py" = [
Expand All @@ -98,4 +98,10 @@ ignore = ["UP006", "UP007", "UP013", "UP014", "UP019", "UP035", "UP038"]

[tool.ruff.lint.isort]
extra-standard-library = ["tomllib"]
known-first-party = ["typing_extensions", "_typed_dict_test_helper"]
known-first-party = [
"typing_extensions",
"_typed_dict_test_helper",
"_inspect_stock_annotations",
"_inspect_stringized_annotations",
"_inspect_stringized_annotations_2",
]
28 changes: 28 additions & 0 deletions src/_inspect_stock_annotations.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #151, the consensus seemed to be that we shouldn't add new "data files" to the src directory for usage in tests, because these will end up in the site-packages directory for people who've installed typing_extensions. Should we just store these as strings in test_typing_extensions.py, like we did for the data files in that PR?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might also be time to split the test file, as it's approaching 10k lines.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could have an src/test_typing_extensons directory: then it's only a single importable module in site-packages, but we'd be able to split it up so that it doesn't get too huge.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of making a test directory. I'll send a separate PR for that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried this and I think it's not worth it. I like to simply run python test_typing_extensions.py to run the test suite; that would no longer work with a package. I'll move the test files from this PR into exec() calls instead.

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
a:int=3
b:str="foo"

class MyClass:
a:int=4
b:str="bar"
def __init__(self, a, b):
self.a = a
self.b = b
def __eq__(self, other):
return isinstance(other, MyClass) and self.a == other.a and self.b == other.b

def function(a:int, b:str) -> MyClass:
return MyClass(a, b)


def function2(a:int, b:"str", c:MyClass) -> MyClass:
pass


def function3(a:"int", b:"str", c:"MyClass"):
pass


class UnannotatedClass:
pass

def unannotated_function(a, b, c): pass
34 changes: 34 additions & 0 deletions src/_inspect_stringized_annotations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from __future__ import annotations

a:int=3
b:str="foo"

class MyClass:
a:int=4
b:str="bar"
def __init__(self, a, b):
self.a = a
self.b = b
def __eq__(self, other):
return isinstance(other, MyClass) and self.a == other.a and self.b == other.b

def function(a:int, b:str) -> MyClass:
return MyClass(a, b)


def function2(a:int, b:"str", c:MyClass) -> MyClass:
pass


def function3(a:"int", b:"str", c:"MyClass"):
pass


class UnannotatedClass:
pass

def unannotated_function(a, b, c): pass

class MyClassWithLocalAnnotations:
mytype = int
x: mytype
4 changes: 4 additions & 0 deletions src/_inspect_stringized_annotations_2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from __future__ import annotations


def foo(a, b, c): pass
Loading
Loading