-
Notifications
You must be signed in to change notification settings - Fork 250
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
TypeVar substitution in get_type_hints #776
Comments
Here is a POC of implementation, using the feature of generic MRO described in #777: import sys
from typing import *
from typing import _collect_type_vars, _eval_type, _strip_annotations
##### From #777
def _generic_mro(result, tp):
origin = get_origin(tp)
if origin is None:
origin = tp
result[origin] = tp
if hasattr(origin, "__orig_bases__"):
parameters = _collect_type_vars(origin.__orig_bases__)
if origin is tp and parameters:
result[origin] = origin[parameters]
substitution = dict(zip(parameters, get_args(tp)))
for base in origin.__orig_bases__:
if get_origin(base) in result:
continue
base_parameters = getattr(base, "__parameters__", ())
if base_parameters:
base = base[tuple(substitution.get(p, p) for p in base_parameters)]
_generic_mro(result, base)
def generic_mro(tp):
origin = get_origin(tp)
if origin is None and not hasattr(tp, "__orig_bases__"):
if not isinstance(tp, type):
raise TypeError(f"{tp!r} is not a type or a generic alias")
return tp.__mro__
# sentinel value to avoid to subscript Generic and Protocol
result = {Generic: Generic, Protocol: Protocol}
_generic_mro(result, tp)
cls = origin if origin is not None else tp
return tuple(result.get(sub_cls, sub_cls) for sub_cls in cls.__mro__)
#####
def _class_annotations(cls, globalns, localns):
hints = {}
if globalns is None:
base_globals = sys.modules[cls.__module__].__dict__
else:
base_globals = globalns
for name, value in cls.__dict__.get("__annotations__", {}).items():
if value is None:
value = type(None)
if isinstance(value, str):
value = ForwardRef(value, is_argument=False)
hints[name] = _eval_type(value, base_globals, localns)
return hints
# For brevety of the example, the implementation just add the substitute_type_vars
# implementation and default to get_type_hints. Of course, it would have to be directly
# integrated into get_type_hints
def get_type_hints2(
obj, globalns=None, localns=None, include_extras=False, substitute_type_vars=False
):
if substitute_type_vars and (isinstance(obj, type) or isinstance(get_origin(obj), type)):
hints = {}
for base in reversed(generic_mro(obj)):
origin = get_origin(base)
if hasattr(origin, "__orig_bases__"):
parameters = _collect_type_vars(origin.__orig_bases__)
substitution = dict(zip(parameters, get_args(base)))
annotations = _class_annotations(get_origin(base), globalns, localns)
for name, tp in annotations.items():
if isinstance(tp, TypeVar):
hints[name] = substitution.get(tp, tp)
elif tp_params := getattr(tp, "__parameters__", ()):
hints[name] = tp[
tuple(substitution.get(p, p) for p in tp_params)
]
else:
hints[name] = tp
else:
hints.update(_class_annotations(base, globalns, localns))
return (
hints
if include_extras
else {k: _strip_annotations(t) for k, t in hints.items()}
)
else:
return get_type_hints(obj, globalns, localns, include_extras) This implementation has been tested with |
IMO subtleties related to type variables are best left to static type checkers. What's the use case that led you down this path? |
Actually, this code is (almost) directly extracted from one of my library: apischema Type annotations are not exclusively used by type checkers, that's what makes their strength. In my library, i use type annotations to generate (de)serializers/JSON schema/GraphQL schema/etc. just from type annotations. And this is actually a (very) big use case : pydantic and thus FastAPI are literally built on this. In all of these libraries, generics are not so easy to handle (pydantic has even gave up to support them in 3.6) and it requires a lot of code to be done properly; you have a direct example with my code above. |
But you can do these calculations yourself using other public APIs defined in typing.py, right? I am not in favor of complexifcation of get_type_hints() for such a corner case (even though I understand it's important to you). |
It requires a little bit of private ( Acutally, I doesn't need this feature in typing for myself, but I thought it could be good to make easier the creation of new tools using type annotations (and FastAPI skyrocketing shows that there is a demand). |
@wyfo I've been working on extracting out python's type-annotations in runtime: python-type-extractor To see how it works, look into files in test_fixtures folder folder, and the corresponding test files in type_extractor/types folder For example, this sample code rel. generics is extracted to this test output you can use the library however you want (it's "do whatever you want just add a small credit on readme" license... ), and file issues if you have any problems. @gvanrossum I think having a better 'annotation-processing' should be a priority for the python ecosystem. A lot of type-safe languages try to provide some form of annotation-processing tools, which is then used to 'extend' the language in various ways (eg. code-generations):
Also, some standard build-time API-hook that can be invoked by something like |
I have to be honest and direct, I am already spread too thinly. You need to find someone else to champion this. |
@devdoomari3 Briefly, one strength of Python is that you don't need core language features/syntax to do a lot of manipulations of pseudo-static elements like annotations (while you made a comparison with languages where annotations are truly static things). Your concern is about IDE handling of this manipulations (and I understand it), but you could write a plugin for your favorite one which will execute your code and adapt his typeshed from the result. No need to change the typing module. |
@wyfo it's not static-evaluation -- it's runtime type extraction, which is what you're trying to do. As for IDE handling, I'm hoping for some standardization for runtime-annotation-processing or codegen: \
by having some standard, we can get:
Another thing: because there are multiple implementations of typecheckers, there's the problem of 'plugins only written for mypy not working for pytype / pylance / etc' --- extremely useful things like sqlalchemy-mypy plugin, pydantic-mypy plugin only works for mypy... Anyway, I don't know a lot about python / how python decisions are made / writing IDE plugin, |
I just ran into this issue myself. from dataclasses import dataclass
from typing import Any, Iterable, get_type_hints, get_args
@dataclass
class List[T: Any]:
value: Iterable[T]
@dataclass
class StrList1[T: str](List[T]):
...
@dataclass
class StrList2(List[str]):
...
get_args(get_type_hints(StrList1)['value'])[0].__bound__ # returns typing.Any
get_args(get_type_hints(StrList2)['value'])[0].__bound__ # returns typing.Any
# I want to know that, in the case of StrList1 and StrList2, `value` is an `Iterable[str]` |
When it comes to generic classes, type annotations retrieved with
get_type_hints
are not really easy to use, as in the following example:It could be useful to add a
TypeVar
substitution toget_type_hints
, for example with an additionalsubstitute_type_vars
parameter (with aFalse
default)By the way,
get_type_hints
could also allow to pass generic alias in order to substitute directlyTypeVar
s with their related argument.It would give for the example above:
The text was updated successfully, but these errors were encountered: