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

Reply via email to