Skip to content

Commit 642b12c

Browse files
committed
get_type_hints
1 parent fc45b1a commit 642b12c

File tree

2 files changed

+139
-0
lines changed

2 files changed

+139
-0
lines changed

basedtyping/__init__.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"as_functiontype",
7373
"ForwardRef",
7474
"BASEDMYPY_TYPE_CHECKING",
75+
"get_type_hints",
7576
)
7677

7778
if TYPE_CHECKING:
@@ -743,3 +744,110 @@ def _type_check(arg: object, msg: str) -> object:
743744
if not callable(arg):
744745
raise TypeError(f"{msg} Got {arg!r:.100}.")
745746
return arg
747+
748+
749+
_strip_annotations = typing._strip_annotations # type: ignore[attr-defined]
750+
751+
752+
def get_type_hints( # type: ignore[no-any-explicit]
753+
obj: object
754+
| Callable[..., object]
755+
| FunctionType[..., object]
756+
| types.BuiltinFunctionType[..., object]
757+
| types.MethodType
758+
| types.ModuleType
759+
| types.WrapperDescriptorType
760+
| types.MethodWrapperType
761+
| types.MethodDescriptorType,
762+
globalns: dict[str, object] | None = None,
763+
localns: dict[str, object] | None = None,
764+
include_extras: bool = False, # noqa: FBT001, FBT002
765+
) -> dict[str, object]:
766+
"""Return type hints for an object.
767+
768+
same as `typing.get_type_hints` except:
769+
- supports based typing denotations
770+
- adds the class to the scope:
771+
772+
```py
773+
class Base:
774+
def __init_subclass__(cls):
775+
get_type_hints(cls)
776+
777+
class A(Base):
778+
a: A
779+
```
780+
"""
781+
if getattr(obj, "__no_type_check__", None): # type: ignore[no-any-expr]
782+
return {}
783+
# Classes require a special treatment.
784+
if isinstance(obj, type): # type: ignore[no-any-expr]
785+
hints = {}
786+
for base in reversed(obj.__mro__):
787+
if globalns is None:
788+
base_globals = getattr(sys.modules.get(base.__module__, None), "__dict__", {}) # type: ignore[no-any-expr]
789+
else:
790+
base_globals = globalns
791+
ann = base.__dict__.get("__annotations__", {}) # type: ignore[no-any-expr]
792+
if isinstance(ann, types.GetSetDescriptorType): # type: ignore[no-any-expr]
793+
ann = {} # type: ignore[no-any-expr]
794+
base_locals = dict(vars(base)) if localns is None else localns # type: ignore[no-any-expr]
795+
if localns is None and globalns is None:
796+
# This is surprising, but required. Before Python 3.10,
797+
# get_type_hints only evaluated the globalns of
798+
# a class. To maintain backwards compatibility, we reverse
799+
# the globalns and localns order so that eval() looks into
800+
# *base_globals* first rather than *base_locals*.
801+
# This only affects ForwardRefs.
802+
base_globals, base_locals = base_locals, base_globals
803+
# start not copied section
804+
if base is obj:
805+
# add the class to the scope
806+
base_locals[obj.__name__] = obj # type: ignore[no-any-expr]
807+
# end not copied section
808+
for name, value in ann.items(): # type: ignore[no-any-expr]
809+
if value is None: # type: ignore[no-any-expr]
810+
value = type(None)
811+
if isinstance(value, str): # type: ignore[no-any-expr]
812+
if sys.version_info < (3, 9):
813+
value = ForwardRef(value, is_argument=False)
814+
else:
815+
value = ForwardRef(value, is_argument=False, is_class=True)
816+
value = typing._eval_type(value, base_globals, base_locals, recursive_guard=1) # type: ignore[attr-defined, no-any-expr]
817+
hints[name] = value # type: ignore[no-any-expr]
818+
819+
return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()} # type: ignore[no-any-expr]
820+
821+
if globalns is None:
822+
if isinstance(obj, types.ModuleType): # type: ignore[no-any-expr]
823+
globalns = obj.__dict__
824+
else:
825+
nsobj = obj
826+
# Find globalns for the unwrapped object.
827+
while hasattr(nsobj, "__wrapped__"):
828+
nsobj = nsobj.__wrapped__ # type: ignore[no-any-expr]
829+
globalns = getattr(nsobj, "__globals__", {}) # type: ignore[no-any-expr]
830+
if localns is None:
831+
localns = globalns
832+
elif localns is None:
833+
localns = globalns
834+
hints = getattr(obj, "__annotations__", None) # type: ignore[assignment, no-any-expr]
835+
if hints is None: # type: ignore[no-any-expr, redundant-expr]
836+
# Return empty annotations for something that _could_ have them.
837+
if isinstance(obj, typing._allowed_types): # type: ignore[ unreachable]
838+
return {}
839+
raise TypeError(f"{obj!r} is not a module, class, method, " "or function.")
840+
hints = dict(hints) # type: ignore[no-any-expr]
841+
for name, value in hints.items(): # type: ignore[no-any-expr]
842+
if value is None: # type: ignore[no-any-expr]
843+
value = type(None)
844+
if isinstance(value, str): # type: ignore[no-any-expr]
845+
# class-level forward refs were handled above, this must be either
846+
# a module-level annotation or a function argument annotation
847+
value = ForwardRef(
848+
value,
849+
is_argument=not isinstance(cast(object, obj), types.ModuleType),
850+
is_class=False,
851+
)
852+
hints[name] = typing._eval_type(value, globalns, localns) # type: ignore[no-any-expr, attr-defined]
853+
return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()} # type: ignore[no-any-expr]

tests/test_get_type_hints.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from __future__ import annotations
2+
3+
import re
4+
5+
from typing_extensions import Literal, Union, override
6+
7+
from basedtyping import get_type_hints
8+
9+
10+
def test_get_type_hints_class():
11+
result: object = None
12+
13+
class Base:
14+
@override
15+
def __init_subclass__(cls):
16+
nonlocal result
17+
result = get_type_hints(cls)
18+
19+
class A(Base):
20+
a: A
21+
22+
assert result == {"a": A}
23+
24+
25+
def test_get_type_hints_based():
26+
class A:
27+
a: Union[re.RegexFlag.ASCII, re.RegexFlag.DOTALL]
28+
29+
assert get_type_hints(A) == {
30+
"a": Union[Literal[re.RegexFlag.ASCII], Literal[re.RegexFlag.DOTALL]] # noqa: PYI030
31+
}

0 commit comments

Comments
 (0)