https://github.com/python/cpython/commit/305be5fb1a1ece7f9651ae98053dbe79bf439aa4 commit: 305be5fb1a1ece7f9651ae98053dbe79bf439aa4 branch: main author: Jelle Zijlstra <jelle.zijls...@gmail.com> committer: JelleZijlstra <jelle.zijls...@gmail.com> date: 2025-04-04T08:57:10-07:00 summary:
gh-118761: Lazily import annotationlib in typing (#132060) annotationlib is used quite a few times in typing.py, but I think the usages are just rare enough that this makes sense. The import would get triggered by: - Using get_type_hints(), evaluate_forward_ref(), and similar introspection functions - Using a string annotation anywhere that goes through _type_convert (e.g., "Final['x']" will trigger an annotationlib import in order to access the ForwardRef class). - Creating a TypedDict or NamedTuple (unless it's empty or PEP 563 is on). Lots of programs will want to use typing without any of these, so the tradeoff seems worth it. files: M Lib/typing.py diff --git a/Lib/typing.py b/Lib/typing.py index 6536a97f6e1910..e67284fc571b1b 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -19,8 +19,6 @@ """ from abc import abstractmethod, ABCMeta -import annotationlib -from annotationlib import ForwardRef import collections from collections import defaultdict import collections.abc @@ -163,6 +161,15 @@ 'Unpack', ] +class _LazyAnnotationLib: + def __getattr__(self, attr): + global _lazy_annotationlib + import annotationlib + _lazy_annotationlib = annotationlib + return getattr(annotationlib, attr) + +_lazy_annotationlib = _LazyAnnotationLib() + def _type_convert(arg, module=None, *, allow_special_forms=False): """For converting None to type(None), and strings to ForwardRef.""" @@ -246,7 +253,7 @@ def _type_repr(obj): if isinstance(obj, tuple): # Special case for `repr` of types with `ParamSpec`: return '[' + ', '.join(_type_repr(t) for t in obj) + ']' - return annotationlib.value_to_string(obj) + return _lazy_annotationlib.value_to_string(obj) def _collect_type_parameters(args, *, enforce_default_ordering: bool = True): @@ -423,7 +430,7 @@ def __repr__(self): def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=frozenset(), - format=annotationlib.Format.VALUE, owner=None): + format=None, owner=None): """Evaluate all forward references in the given type t. For use of globalns and localns see the docstring for get_type_hints(). @@ -433,7 +440,7 @@ def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=f if type_params is _sentinel: _deprecation_warning_for_no_type_params_passed("typing._eval_type") type_params = () - if isinstance(t, ForwardRef): + if isinstance(t, _lazy_annotationlib.ForwardRef): # If the forward_ref has __forward_module__ set, evaluate() infers the globals # from the module, and it will probably pick better than the globals we have here. if t.__forward_module__ is not None: @@ -930,7 +937,7 @@ def run(arg: Child | Unrelated): def _make_forward_ref(code, **kwargs): - forward_ref = ForwardRef(code, **kwargs) + forward_ref = _lazy_annotationlib.ForwardRef(code, **kwargs) # For compatibility, eagerly compile the forwardref's code. forward_ref.__forward_code__ return forward_ref @@ -943,7 +950,7 @@ def evaluate_forward_ref( globals=None, locals=None, type_params=None, - format=annotationlib.Format.VALUE, + format=None, _recursive_guard=frozenset(), ): """Evaluate a forward reference as a type hint. @@ -965,10 +972,11 @@ def evaluate_forward_ref( evaluating the forward reference. This parameter should be provided (though it may be an empty tuple) if *owner* is not given and the forward reference does not already have an owner set. *format* specifies the format of the - annotation and is a member of the annotationlib.Format enum. + annotation and is a member of the annotationlib.Format enum, defaulting to + VALUE. """ - if format == annotationlib.Format.STRING: + if format == _lazy_annotationlib.Format.STRING: return forward_ref.__forward_arg__ if forward_ref.__forward_arg__ in _recursive_guard: return forward_ref @@ -977,7 +985,7 @@ def evaluate_forward_ref( value = forward_ref.evaluate(globals=globals, locals=locals, type_params=type_params, owner=owner) except NameError: - if format == annotationlib.Format.FORWARDREF: + if format == _lazy_annotationlib.Format.FORWARDREF: return forward_ref else: raise @@ -2257,7 +2265,7 @@ def greet(name: str) -> None: def get_type_hints(obj, globalns=None, localns=None, include_extras=False, - *, format=annotationlib.Format.VALUE): + *, format=None): """Return type hints for an object. This is often the same as obj.__annotations__, but it handles @@ -2290,12 +2298,15 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False, """ if getattr(obj, '__no_type_check__', None): return {} + Format = _lazy_annotationlib.Format + if format is None: + format = Format.VALUE # Classes require a special treatment. if isinstance(obj, type): hints = {} for base in reversed(obj.__mro__): - ann = annotationlib.get_annotations(base, format=format) - if format == annotationlib.Format.STRING: + ann = _lazy_annotationlib.get_annotations(base, format=format) + if format == Format.STRING: hints.update(ann) continue if globalns is None: @@ -2319,12 +2330,12 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False, value = _eval_type(value, base_globals, base_locals, base.__type_params__, format=format, owner=obj) hints[name] = value - if include_extras or format == annotationlib.Format.STRING: + if include_extras or format == Format.STRING: return hints else: return {k: _strip_annotations(t) for k, t in hints.items()} - hints = annotationlib.get_annotations(obj, format=format) + hints = _lazy_annotationlib.get_annotations(obj, format=format) if ( not hints and not isinstance(obj, types.ModuleType) @@ -2333,7 +2344,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False, and not hasattr(obj, '__annotate__') ): raise TypeError(f"{obj!r} is not a module, class, or callable.") - if format == annotationlib.Format.STRING: + if format == Format.STRING: return hints if globalns is None: @@ -2850,10 +2861,10 @@ def _make_eager_annotate(types): for key, val in types.items()} def annotate(format): match format: - case annotationlib.Format.VALUE | annotationlib.Format.FORWARDREF: + case _lazy_annotationlib.Format.VALUE | _lazy_annotationlib.Format.FORWARDREF: return checked_types - case annotationlib.Format.STRING: - return annotationlib.annotations_to_string(types) + case _lazy_annotationlib.Format.STRING: + return _lazy_annotationlib.annotations_to_string(types) case _: raise NotImplementedError(format) return annotate @@ -2884,7 +2895,8 @@ def __new__(cls, typename, bases, ns): annotate = _make_eager_annotate(types) elif "__annotate__" in ns: original_annotate = ns["__annotate__"] - types = annotationlib.call_annotate_function(original_annotate, annotationlib.Format.FORWARDREF) + types = _lazy_annotationlib.call_annotate_function( + original_annotate, _lazy_annotationlib.Format.FORWARDREF) field_names = list(types) # For backward compatibility, type-check all the types at creation time @@ -2892,8 +2904,9 @@ def __new__(cls, typename, bases, ns): _type_check(typ, "field annotation must be a type") def annotate(format): - annos = annotationlib.call_annotate_function(original_annotate, format) - if format != annotationlib.Format.STRING: + annos = _lazy_annotationlib.call_annotate_function( + original_annotate, format) + if format != _lazy_annotationlib.Format.STRING: return {key: _type_check(val, f"field {key} annotation must be a type") for key, val in annos.items()} return annos @@ -3069,8 +3082,8 @@ def __new__(cls, name, bases, ns, total=True): own_annotations = ns["__annotations__"] elif "__annotate__" in ns: own_annotate = ns["__annotate__"] - own_annotations = annotationlib.call_annotate_function( - own_annotate, annotationlib.Format.FORWARDREF, owner=tp_dict + own_annotations = _lazy_annotationlib.call_annotate_function( + own_annotate, _lazy_annotationlib.Format.FORWARDREF, owner=tp_dict ) else: own_annotate = None @@ -3137,18 +3150,20 @@ def __annotate__(format): base_annotate = base.__annotate__ if base_annotate is None: continue - base_annos = annotationlib.call_annotate_function(base.__annotate__, format, owner=base) + base_annos = _lazy_annotationlib.call_annotate_function( + base.__annotate__, format, owner=base) annos.update(base_annos) if own_annotate is not None: - own = annotationlib.call_annotate_function(own_annotate, format, owner=tp_dict) - if format != annotationlib.Format.STRING: + own = _lazy_annotationlib.call_annotate_function( + own_annotate, format, owner=tp_dict) + if format != _lazy_annotationlib.Format.STRING: own = { n: _type_check(tp, msg, module=tp_dict.__module__) for n, tp in own.items() } - elif format == annotationlib.Format.STRING: - own = annotationlib.annotations_to_string(own_annotations) - elif format in (annotationlib.Format.FORWARDREF, annotationlib.Format.VALUE): + elif format == _lazy_annotationlib.Format.STRING: + own = _lazy_annotationlib.annotations_to_string(own_annotations) + elif format in (_lazy_annotationlib.Format.FORWARDREF, _lazy_annotationlib.Format.VALUE): own = own_checked_annotations else: raise NotImplementedError(format) @@ -3732,7 +3747,9 @@ def __getattr__(attr): Soft-deprecated objects which are costly to create are only created on-demand here. """ - if attr in {"Pattern", "Match"}: + if attr == "ForwardRef": + obj = _lazy_annotationlib.ForwardRef + elif attr in {"Pattern", "Match"}: import re obj = _alias(getattr(re, attr), 1) elif attr in {"ContextManager", "AsyncContextManager"}: _______________________________________________ Python-checkins mailing list -- python-checkins@python.org To unsubscribe send an email to python-checkins-le...@python.org https://mail.python.org/mailman3/lists/python-checkins.python.org/ Member address: arch...@mail-archive.com