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

gh-85668: Create public API for typing._eval_type #21753

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 8 additions & 1 deletion Doc/library/typing.rst
Expand Up @@ -1610,6 +1610,14 @@ Functions and decorators
Introspection helpers
---------------------

.. function:: eval_type(t, globalns=None, localns=None)

Evaluate the given type ``t`` by resolving any forward references.

For usage of ``globalns`` and ``localns`` see :func:`get_type_hints`.

.. versionadded:: 3.10

.. function:: get_type_hints(obj, globalns=None, localns=None, include_extras=False)

Return a dictionary containing type hints for a function, method, module
Expand Down Expand Up @@ -1693,4 +1701,3 @@ Constant
(see :pep:`563`).

.. versionadded:: 3.5.2

15 changes: 14 additions & 1 deletion Lib/test/test_typing.py
Expand Up @@ -13,7 +13,7 @@
from typing import Tuple, List, Dict, MutableMapping
from typing import Callable
from typing import Generic, ClassVar, Final, final, Protocol
from typing import cast, runtime_checkable
from typing import cast, eval_type, runtime_checkable
from typing import get_type_hints
from typing import get_origin, get_args
from typing import no_type_check, no_type_check_decorator
Expand Down Expand Up @@ -4175,6 +4175,19 @@ def test_annotated_in_other_types(self):
self.assertEqual(X[int], List[Annotated[int, 5]])


class EvalTypeTests(BaseTestCase):
def test_simple_reference(self):
self.assertIs(eval_type(str), str)

def test_forward_reference(self):
self.assertIs(eval_type(ForwardRef('int')), int)

class C:
pass

self.assertIs(eval_type(ForwardRef('C'), localns=locals()), C)


class AllTests(BaseTestCase):
"""Tests for __all__."""

Expand Down
8 changes: 8 additions & 0 deletions Lib/typing.py
Expand Up @@ -99,6 +99,7 @@
# One-off things.
'AnyStr',
'cast',
'eval_type',
'final',
'get_args',
'get_origin',
Expand Down Expand Up @@ -1278,6 +1279,13 @@ def cast(typ, val):
return val


def eval_type(t, globalns=None, localns=None):
"""Evaluate all forward references in the given type t.
For use of globalns and localns see the docstring for get_type_hints().
"""
return _eval_type(t, globalns, localns)
Copy link
Member

Choose a reason for hiding this comment

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

Hm, you have to fill in globalns and localns if they are None, the same as get_type_hints does. And add tests for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What should I fill them with? Unlike get_type_hints, eval_type is not aware of the context it's being used in, so it's the user's responsibility to provide one (in form of globalns and localns). get_type_hints retrieves the namespace from the passed object, but eval_type only receives a type (declaration).

Copy link
Member

Choose a reason for hiding this comment

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

Good point, but then you shouldn't give then a default value of None. :-)

My proposal: require globalns, and let localns default to globalns.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The intention behind giving a default of None to both parameters was to not prioritize one over the other. If one should take priority, globalns is certainly the better choice already from considering the order of parameters (to be consistent with get_type_hints).

But do we even need a distinction between global and local namespace at this point? What about exposing a single parameter namespace without default? The user can always do something like eval_type(t, namespace=globals() | locals()).

By the way, I realized that ForwardRef._evaluate sets globalns = localns if the former is None and the latter isn't. These are then passed to eval. Since eval accepts any mapping for locals but requires a dict for globals this can lead to the following error:

>>> from types import MappingProxyType
>>> _eval_type(ForwardRef('int'), globalns=None, localns=MappingProxyType({}))
[...]
  File ".../lib/python3.8/typing.py", line 518, in _evaluate
    eval(self.__forward_code__, globalns, localns),
TypeError: globals must be a real dict; try eval(expr, {}, mapping)

Does that mean that either ForwardRef._evaluate or the new eval_type should include another safety check?



def _get_defaults(func):
"""Internal helper to extract the default arguments, by name."""
try:
Expand Down