From 1020121327b705614d073b44b18a7d5500b27fd7 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 10 May 2024 06:24:24 -0700 Subject: [PATCH 1/5] Rename typing._collect_parameters function. Unfortunately, released versions of typing_extensions monkeypatch this function without the extra parameter, which makes it so things break badly if current main is used with typing_extensions. ``` % ~/py/cpython/python.exe test_typing_extensions.py Traceback (most recent call last): File "/Users/jelle/py/typing_extensions/src/test_typing_extensions.py", line 42, in from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated File "/Users/jelle/py/typing_extensions/src/_typed_dict_test_helper.py", line 17, in class FooGeneric(TypedDict, Generic[T]): ~~~~~~~^^^ File "/Users/jelle/py/cpython/Lib/typing.py", line 431, in inner return func(*args, **kwds) File "/Users/jelle/py/cpython/Lib/typing.py", line 1243, in _generic_class_getitem return _GenericAlias(cls, args) File "/Users/jelle/py/cpython/Lib/typing.py", line 1420, in __init__ self.__parameters__ = _collect_parameters( ~~~~~~~~~~~~~~~~~~~^ args, ^^^^^ enforce_default_ordering=enforce_default_ordering, ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ) ^ TypeError: _collect_parameters() got an unexpected keyword argument 'enforce_default_ordering' ``` Fortunately, the monkeypatching is not needed on Python 3.13, because CPython now implements PEP 696. By renaming the function, we prevent the monkeypatch from breaking typing.py internals. --- Lib/typing.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index e75a627d226e50..03165f7c5657a0 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -256,15 +256,15 @@ def _type_repr(obj): return repr(obj) -def _collect_parameters(args, *, enforce_default_ordering: bool = True): - """Collect all type variables and parameter specifications in args +def _collect_type_parameters(args, *, enforce_default_ordering: bool = True): + """Collect all type parameters in args in order of first appearance (lexicographic order). For example:: >>> P = ParamSpec('P') >>> T = TypeVar('T') - >>> _collect_parameters((T, Callable[P, T])) + >>> _collect_type_parameters((T, Callable[P, T])) (~T, ~P) """ # required type parameter cannot appear after parameter with default @@ -280,7 +280,7 @@ def _collect_parameters(args, *, enforce_default_ordering: bool = True): # `t` might be a tuple, when `ParamSpec` is substituted with # `[T, int]`, or `[int, *Ts]`, etc. for x in t: - for collected in _collect_parameters([x]): + for collected in _collect_type_parameters([x]): if collected not in parameters: parameters.append(collected) elif hasattr(t, '__typing_subst__'): @@ -320,7 +320,7 @@ def _check_generic_specialization(cls, arguments): if actual_len < expected_len: # If the parameter at index `actual_len` in the parameters list # has a default, then all parameters after it must also have - # one, because we validated as much in _collect_parameters(). + # one, because we validated as much in _collect_type_parameters(). # That means that no error needs to be raised here, despite # the number of arguments being passed not matching the number # of parameters: all parameters that aren't explicitly @@ -1255,7 +1255,7 @@ def _generic_init_subclass(cls, *args, **kwargs): if error: raise TypeError("Cannot inherit from plain Generic") if '__orig_bases__' in cls.__dict__: - tvars = _collect_parameters(cls.__orig_bases__) + tvars = _collect_type_parameters(cls.__orig_bases__) # Look for Generic[T1, ..., Tn]. # If found, tvars must be a subset of it. # If not found, tvars is it. @@ -1417,7 +1417,7 @@ def __init__(self, origin, args, *, inst=True, name=None): self.__args__ = tuple(... if a is _TypingEllipsis else a for a in args) enforce_default_ordering = origin in (Generic, Protocol) - self.__parameters__ = _collect_parameters( + self.__parameters__ = _collect_type_parameters( args, enforce_default_ordering=enforce_default_ordering, ) From d7a7c0c7d9ee08bc88f570066794c1712a6b86b3 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 10 May 2024 08:45:04 -0700 Subject: [PATCH 2/5] Readd as deprecated --- Lib/test/test_typing.py | 6 ++++++ Lib/typing.py | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 8dde8aedd2a2b1..7f820ca370ee14 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6325,6 +6325,8 @@ def test_or(self): self.assertEqual(X | "x", Union[X, "x"]) self.assertEqual("x" | X, Union["x", X]) + +class InternalsTests(BaseTestCase): def test_deprecation_for_no_type_params_passed_to__evaluate(self): with self.assertWarnsRegex( DeprecationWarning, @@ -6350,6 +6352,10 @@ def test_deprecation_for_no_type_params_passed_to__evaluate(self): self.assertEqual(cm.filename, __file__) + def test_collect_parameters(self): + with self.assertWarns(DeprecationWarning): + from typing import _collect_parameters + @lru_cache() def cached_func(x, y): diff --git a/Lib/typing.py b/Lib/typing.py index 03165f7c5657a0..8b9c8b8e39b5fc 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -3770,6 +3770,16 @@ def __getattr__(attr): elif attr in {"ContextManager", "AsyncContextManager"}: import contextlib obj = _alias(getattr(contextlib, f"Abstract{attr}"), 2, name=attr, defaults=(bool | None,)) + elif attr == "_collect_parameters": + import warnings + + depr_message = ( + "The private _collect_parameters function is deprecated and will be" + " removed in a future version of Python. Any use of private functions" + " is discouraged and may break in the future." + ) + warnings.warn(depr_message, category=DeprecationWarning, stacklevel=1) + obj = _collect_type_parameters else: raise AttributeError(f"module {__name__!r} has no attribute {attr!r}") globals()[attr] = obj From 0afb4a7eedf0e6826f7ed5900f4e2202726f4fbd Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 10 May 2024 08:47:28 -0700 Subject: [PATCH 3/5] Better test --- Lib/test/test_typing.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index ae2879b1794b2b..8c6a9e40b8a20e 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -45,7 +45,7 @@ import weakref import types -from test.support import captured_stderr, cpython_only, infinite_recursion, requires_docstrings +from test.support import captured_stderr, cpython_only, infinite_recursion, requires_docstrings, import_helper from test.typinganndata import ann_module695, mod_generics_cache, _typed_dict_helper @@ -6353,8 +6353,9 @@ def test_deprecation_for_no_type_params_passed_to__evaluate(self): self.assertEqual(cm.filename, __file__) def test_collect_parameters(self): + typing = import_helper.import_fresh_module("typing") with self.assertWarns(DeprecationWarning): - from typing import _collect_parameters + typing._collect_parameters @lru_cache() From b022ddc422d7f743b169567426c21b5557ec6b1b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 10 May 2024 08:55:06 -0700 Subject: [PATCH 4/5] fix stacklevel --- Lib/test/test_typing.py | 3 ++- Lib/typing.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 8c6a9e40b8a20e..087f068c286b6b 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6354,8 +6354,9 @@ def test_deprecation_for_no_type_params_passed_to__evaluate(self): def test_collect_parameters(self): typing = import_helper.import_fresh_module("typing") - with self.assertWarns(DeprecationWarning): + with self.assertWarns(DeprecationWarning) as cm: typing._collect_parameters + self.assertEqual(cm.filename, __file__) @lru_cache() diff --git a/Lib/typing.py b/Lib/typing.py index 8b9c8b8e39b5fc..434574559e04fc 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -3778,7 +3778,7 @@ def __getattr__(attr): " removed in a future version of Python. Any use of private functions" " is discouraged and may break in the future." ) - warnings.warn(depr_message, category=DeprecationWarning, stacklevel=1) + warnings.warn(depr_message, category=DeprecationWarning, stacklevel=2) obj = _collect_type_parameters else: raise AttributeError(f"module {__name__!r} has no attribute {attr!r}") From 388d56bbc71c25d91ecd0081cc1efbbd0dcc3bff Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 10 May 2024 09:00:56 -0700 Subject: [PATCH 5/5] Update Lib/test/test_typing.py Co-authored-by: Alex Waygood --- Lib/test/test_typing.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 087f068c286b6b..f10b0aea3cd7b9 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6354,7 +6354,10 @@ def test_deprecation_for_no_type_params_passed_to__evaluate(self): def test_collect_parameters(self): typing = import_helper.import_fresh_module("typing") - with self.assertWarns(DeprecationWarning) as cm: + with self.assertWarnsRegex( + DeprecationWarning, + "The private _collect_parameters function is deprecated" + ) as cm: typing._collect_parameters self.assertEqual(cm.filename, __file__)