diff --git a/CHANGELOG.md b/CHANGELOG.md index b2379816..bbad4264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ # Unreleased -- Fix tests on Python 3.13.0a6 and newer. 3.13.0a6 adds a new +- Backport the `typing.NoDefault` sentinel object from Python 3.13. + TypeVars, ParamSpecs and TypeVarTuples without default values now have + their `__default__` attribute set to this sentinel value. +- TypeVars, ParamSpecs and TypeVarTuples now have a `has_default()` + method, matching `typing.TypeVar`, `typing.ParamSpec` and + `typing.TypeVarTuple` on Python 3.13+. +- TypeVars, ParamSpecs and TypeVarTuples with `default=None` passed to + their constructors now have their `__default__` attribute set to `None` + at runtime rather than `types.NoneType`. +- Fix most tests for `TypeVar`, `ParamSpec` and `TypeVarTuple` on Python + 3.13.0b1 and newer. +- Fix `Protocol` tests on Python 3.13.0a6 and newer. 3.13.0a6 adds a new `__static_attributes__` attribute to all classes in Python, which broke some assumptions made by the implementation of `typing_extensions.Protocol`. Similarly, 3.13.0b1 adds the new diff --git a/doc/index.rst b/doc/index.rst index f9097a41..3486ae74 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -253,13 +253,19 @@ Special typing primitives The improvements from Python 3.10 and 3.11 were backported. +.. data:: NoDefault + + See :py:class:`typing.NoDefault`. In ``typing`` since 3.13.0. + + .. versionadded:: 4.12.0 + .. data:: NotRequired See :py:data:`typing.NotRequired` and :pep:`655`. In ``typing`` since 3.11. .. versionadded:: 4.0.0 -.. class:: ParamSpec(name, *, default=...) +.. class:: ParamSpec(name, *, default=NoDefault) See :py:class:`typing.ParamSpec` and :pep:`612`. In ``typing`` since 3.10. @@ -284,6 +290,20 @@ Special typing primitives Passing an ellipsis literal (``...``) to *default* now works on Python 3.10 and lower. + .. versionchanged:: 4.12.0 + + The :attr:`!__default__` attribute is now set to ``None`` if + ``default=None`` is passed, and to :data:`NoDefault` if no value is passed. + + Previously, passing ``None`` would result in :attr:`!__default__` being set + to :py:class:`types.NoneType`, and passing no value for the parameter would + result in :attr:`!__default__` being set to ``None``. + + .. versionchanged:: 4.12.0 + + ParamSpecs now have a ``has_default()`` method, for compatibility + with :py:class:`typing.ParamSpec` on Python 3.13+. + .. class:: ParamSpecArgs .. class:: ParamSpecKwargs @@ -395,7 +415,7 @@ Special typing primitives are mutable if they do not carry the :data:`ReadOnly` qualifier. .. versionadded:: 4.9.0 - + The experimental ``closed`` keyword argument and the special key ``__extra_items__`` proposed in :pep:`728` are supported. @@ -466,7 +486,7 @@ Special typing primitives when ``closed=True`` is given were supported. .. class:: TypeVar(name, *constraints, bound=None, covariant=False, - contravariant=False, infer_variance=False, default=...) + contravariant=False, infer_variance=False, default=NoDefault) See :py:class:`typing.TypeVar`. @@ -484,7 +504,21 @@ Special typing primitives The implementation was changed for compatibility with Python 3.12. -.. class:: TypeVarTuple(name, *, default=...) + .. versionchanged:: 4.12.0 + + The :attr:`!__default__` attribute is now set to ``None`` if + ``default=None`` is passed, and to :data:`NoDefault` if no value is passed. + + Previously, passing ``None`` would result in :attr:`!__default__` being set + to :py:class:`types.NoneType`, and passing no value for the parameter would + result in :attr:`!__default__` being set to ``None``. + + .. versionchanged:: 4.12.0 + + TypeVars now have a ``has_default()`` method, for compatibility + with :py:class:`typing.TypeVar` on Python 3.13+. + +.. class:: TypeVarTuple(name, *, default=NoDefault) See :py:class:`typing.TypeVarTuple` and :pep:`646`. In ``typing`` since 3.11. @@ -501,6 +535,20 @@ Special typing primitives The implementation was changed for compatibility with Python 3.12. + .. versionchanged:: 4.12.0 + + The :attr:`!__default__` attribute is now set to ``None`` if + ``default=None`` is passed, and to :data:`NoDefault` if no value is passed. + + Previously, passing ``None`` would result in :attr:`!__default__` being set + to :py:class:`types.NoneType`, and passing no value for the parameter would + result in :attr:`!__default__` being set to ``None``. + + .. versionchanged:: 4.12.0 + + TypeVarTuples now have a ``has_default()`` method, for compatibility + with :py:class:`typing.TypeVarTuple` on Python 3.13+. + .. data:: Unpack See :py:data:`typing.Unpack` and :pep:`646`. In ``typing`` since 3.11. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index a2fe368b..c7c2f0d5 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -38,7 +38,7 @@ from typing_extensions import clear_overloads, get_overloads, overload from typing_extensions import NamedTuple, TypeIs from typing_extensions import override, deprecated, Buffer, TypeAliasType, TypeVar, get_protocol_members, is_protocol -from typing_extensions import Doc +from typing_extensions import Doc, NoDefault from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated # Flags used to mark tests that only apply after a specific @@ -59,9 +59,9 @@ # versions, but not all HAS_FORWARD_MODULE = "module" in inspect.signature(typing._type_check).parameters -skip_if_early_py313_alpha = skipIf( - sys.version_info[:4] == (3, 13, 0, 'alpha') and sys.version_info.serial < 3, - "Bugfixes will be released in 3.13.0a3" +skip_if_py313_beta_1 = skipIf( + sys.version_info[:5] == (3, 13, 0, 'beta', 1), + "Bugfixes will be released in 3.13.0b2" ) ANN_MODULE_SOURCE = '''\ @@ -3485,7 +3485,6 @@ def method(self) -> None: ... self.assertIsInstance(Foo(), ProtocolWithMixedMembers) self.assertNotIsInstance(42, ProtocolWithMixedMembers) - @skip_if_early_py313_alpha def test_protocol_issubclass_error_message(self): @runtime_checkable class Vec2D(Protocol): @@ -5917,7 +5916,6 @@ class GenericNamedTuple(NamedTuple, Generic[T]): self.assertEqual(CallNamedTuple.__orig_bases__, (NamedTuple,)) - @skip_if_early_py313_alpha def test_setname_called_on_values_in_class_dictionary(self): class Vanilla: def __set_name__(self, owner, name): @@ -5989,7 +5987,6 @@ class NamedTupleClass(NamedTuple): TYPING_3_12_0, "__set_name__ behaviour changed on py312+ to use BaseException.add_note()" ) - @skip_if_early_py313_alpha def test_setname_raises_the_same_as_on_other_classes_py312_plus(self): class CustomException(BaseException): pass @@ -6029,7 +6026,6 @@ class NamedTupleClass(NamedTuple): normal_exception.__notes__[0].replace("NormalClass", "NamedTupleClass") ) - @skip_if_early_py313_alpha def test_strange_errors_when_accessing_set_name_itself(self): class CustomException(Exception): pass @@ -6207,12 +6203,15 @@ class A(Generic[T]): ... def test_typevar_none(self): U = typing_extensions.TypeVar('U') U_None = typing_extensions.TypeVar('U_None', default=None) - self.assertEqual(U.__default__, None) - self.assertEqual(U_None.__default__, type(None)) + self.assertIs(U.__default__, NoDefault) + self.assertFalse(U.has_default()) + self.assertEqual(U_None.__default__, None) + self.assertTrue(U_None.has_default()) def test_paramspec(self): P = ParamSpec('P', default=(str, int)) self.assertEqual(P.__default__, (str, int)) + self.assertTrue(P.has_default()) self.assertIsInstance(P, ParamSpec) if hasattr(typing, "ParamSpec"): self.assertIsInstance(P, typing.ParamSpec) @@ -6225,11 +6224,13 @@ class A(Generic[P]): ... P_default = ParamSpec('P_default', default=...) self.assertIs(P_default.__default__, ...) + self.assertTrue(P_default.has_default()) def test_typevartuple(self): Ts = TypeVarTuple('Ts', default=Unpack[Tuple[str, int]]) self.assertEqual(Ts.__default__, Unpack[Tuple[str, int]]) self.assertIsInstance(Ts, TypeVarTuple) + self.assertTrue(Ts.has_default()) if hasattr(typing, "TypeVarTuple"): self.assertIsInstance(Ts, typing.TypeVarTuple) typing_Ts = typing.TypeVarTuple('Ts') @@ -6276,6 +6277,32 @@ def test_pickle(self): self.assertEqual(z.__default__, typevar.__default__) +class NoDefaultTests(BaseTestCase): + @skip_if_py313_beta_1 + def test_pickling(self): + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + s = pickle.dumps(NoDefault, proto) + loaded = pickle.loads(s) + self.assertIs(NoDefault, loaded) + + def test_constructor(self): + self.assertIs(NoDefault, type(NoDefault)()) + with self.assertRaises(TypeError): + type(NoDefault)(1) + + def test_repr(self): + self.assertRegex(repr(NoDefault), r'typing(_extensions)?\.NoDefault') + + def test_no_call(self): + with self.assertRaises(TypeError): + NoDefault() + + @skip_if_py313_beta_1 + def test_immutable(self): + with self.assertRaises(AttributeError): + NoDefault.foo = 'bar' + + class TypeVarInferVarianceTests(BaseTestCase): def test_typevar(self): T = typing_extensions.TypeVar('T') diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 9eb45ce2..70a31193 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -116,6 +116,7 @@ 'MutableMapping', 'MutableSequence', 'MutableSet', + 'NoDefault', 'Optional', 'Pattern', 'Reversible', @@ -134,6 +135,7 @@ # for backward compatibility PEP_560 = True GenericMeta = type +_PEP_696_IMPLEMENTED = sys.version_info >= (3, 13, 0, "beta") # The functions below are modified copies of typing internal helpers. # They are needed by _ProtocolMeta and they provide support for PEP 646. @@ -1355,17 +1357,37 @@ def TypeAlias(self, parameters): ) +if hasattr(typing, "NoDefault"): + NoDefault = typing.NoDefault +else: + class NoDefaultType: + __slots__ = () + + def __new__(cls): + return globals().get("NoDefault") or object.__new__(cls) + + def __repr__(self): + return "typing_extensions.NoDefault" + + def __reduce__(self): + return "NoDefault" + + NoDefault = NoDefaultType() + del NoDefaultType + + def _set_default(type_param, default): + type_param.has_default = lambda: default is not NoDefault if isinstance(default, (tuple, list)): type_param.__default__ = tuple((typing._type_check(d, "Default must be a type") for d in default)) - elif default != _marker: + elif default in (None, NoDefault): + type_param.__default__ = default + else: if isinstance(type_param, ParamSpec) and default is ...: # ... not valid <3.11 type_param.__default__ = default else: type_param.__default__ = typing._type_check(default, "Default must be a type") - else: - type_param.__default__ = None def _set_module(typevarlike): @@ -1388,32 +1410,35 @@ def __instancecheck__(cls, __instance: Any) -> bool: return isinstance(__instance, cls._backported_typevarlike) -# Add default and infer_variance parameters from PEP 696 and 695 -class TypeVar(metaclass=_TypeVarLikeMeta): - """Type variable.""" +if _PEP_696_IMPLEMENTED: + from typing import TypeVar +else: + # Add default and infer_variance parameters from PEP 696 and 695 + class TypeVar(metaclass=_TypeVarLikeMeta): + """Type variable.""" - _backported_typevarlike = typing.TypeVar + _backported_typevarlike = typing.TypeVar - def __new__(cls, name, *constraints, bound=None, - covariant=False, contravariant=False, - default=_marker, infer_variance=False): - if hasattr(typing, "TypeAliasType"): - # PEP 695 implemented (3.12+), can pass infer_variance to typing.TypeVar - typevar = typing.TypeVar(name, *constraints, bound=bound, - covariant=covariant, contravariant=contravariant, - infer_variance=infer_variance) - else: - typevar = typing.TypeVar(name, *constraints, bound=bound, - covariant=covariant, contravariant=contravariant) - if infer_variance and (covariant or contravariant): - raise ValueError("Variance cannot be specified with infer_variance.") - typevar.__infer_variance__ = infer_variance - _set_default(typevar, default) - _set_module(typevar) - return typevar + def __new__(cls, name, *constraints, bound=None, + covariant=False, contravariant=False, + default=NoDefault, infer_variance=False): + if hasattr(typing, "TypeAliasType"): + # PEP 695 implemented (3.12+), can pass infer_variance to typing.TypeVar + typevar = typing.TypeVar(name, *constraints, bound=bound, + covariant=covariant, contravariant=contravariant, + infer_variance=infer_variance) + else: + typevar = typing.TypeVar(name, *constraints, bound=bound, + covariant=covariant, contravariant=contravariant) + if infer_variance and (covariant or contravariant): + raise ValueError("Variance cannot be specified with infer_variance.") + typevar.__infer_variance__ = infer_variance + _set_default(typevar, default) + _set_module(typevar) + return typevar - def __init_subclass__(cls) -> None: - raise TypeError(f"type '{__name__}.TypeVar' is not an acceptable base type") + def __init_subclass__(cls) -> None: + raise TypeError(f"type '{__name__}.TypeVar' is not an acceptable base type") # Python 3.10+ has PEP 612 @@ -1478,8 +1503,12 @@ def __eq__(self, other): return NotImplemented return self.__origin__ == other.__origin__ + +if _PEP_696_IMPLEMENTED: + from typing import ParamSpec + # 3.10+ -if hasattr(typing, 'ParamSpec'): +elif hasattr(typing, 'ParamSpec'): # Add default parameter - PEP 696 class ParamSpec(metaclass=_TypeVarLikeMeta): @@ -1489,7 +1518,7 @@ class ParamSpec(metaclass=_TypeVarLikeMeta): def __new__(cls, name, *, bound=None, covariant=False, contravariant=False, - infer_variance=False, default=_marker): + infer_variance=False, default=NoDefault): if hasattr(typing, "TypeAliasType"): # PEP 695 implemented, can pass infer_variance to typing.TypeVar paramspec = typing.ParamSpec(name, bound=bound, @@ -1572,7 +1601,7 @@ def kwargs(self): return ParamSpecKwargs(self) def __init__(self, name, *, bound=None, covariant=False, contravariant=False, - infer_variance=False, default=_marker): + infer_variance=False, default=NoDefault): super().__init__([self]) self.__name__ = name self.__covariant__ = bool(covariant) @@ -2226,7 +2255,10 @@ def _is_unpack(obj): return isinstance(obj, _UnpackAlias) -if hasattr(typing, "TypeVarTuple"): # 3.11+ +if _PEP_696_IMPLEMENTED: + from typing import TypeVarTuple + +elif hasattr(typing, "TypeVarTuple"): # 3.11+ # Add default parameter - PEP 696 class TypeVarTuple(metaclass=_TypeVarLikeMeta): @@ -2234,7 +2266,7 @@ class TypeVarTuple(metaclass=_TypeVarLikeMeta): _backported_typevarlike = typing.TypeVarTuple - def __new__(cls, name, *, default=_marker): + def __new__(cls, name, *, default=NoDefault): tvt = typing.TypeVarTuple(name) _set_default(tvt, default) _set_module(tvt) @@ -2294,7 +2326,7 @@ def get_shape(self) -> Tuple[*Ts]: def __iter__(self): yield self.__unpacked__ - def __init__(self, name, *, default=_marker): + def __init__(self, name, *, default=NoDefault): self.__name__ = name _DefaultMixin.__init__(self, default) @@ -2679,11 +2711,14 @@ def _check_generic(cls, parameters, elen=_marker): if alen < elen: # since we validate TypeVarLike default in _collect_type_vars # or _collect_parameters we can safely check parameters[alen] - if getattr(parameters[alen], '__default__', None) is not None: + if ( + getattr(parameters[alen], '__default__', NoDefault) + is not NoDefault + ): return - num_default_tv = sum(getattr(p, '__default__', None) - is not None for p in parameters) + num_default_tv = sum(getattr(p, '__default__', NoDefault) + is not NoDefault for p in parameters) elen -= num_default_tv @@ -2713,11 +2748,14 @@ def _check_generic(cls, parameters, elen): if alen < elen: # since we validate TypeVarLike default in _collect_type_vars # or _collect_parameters we can safely check parameters[alen] - if getattr(parameters[alen], '__default__', None) is not None: + if ( + getattr(parameters[alen], '__default__', NoDefault) + is not NoDefault + ): return - num_default_tv = sum(getattr(p, '__default__', None) - is not None for p in parameters) + num_default_tv = sum(getattr(p, '__default__', NoDefault) + is not NoDefault for p in parameters) elen -= num_default_tv @@ -2747,7 +2785,7 @@ def _collect_type_vars(types, typevar_types=None): t not in tvars and not _is_unpack(t) ): - if getattr(t, '__default__', None) is not None: + if getattr(t, '__default__', NoDefault) is not NoDefault: default_encountered = True elif default_encountered: raise TypeError(f'Type parameter {t!r} without a default' @@ -2784,7 +2822,7 @@ def _collect_parameters(args): parameters.append(collected) elif hasattr(t, '__typing_subst__'): if t not in parameters: - if getattr(t, '__default__', None) is not None: + if getattr(t, '__default__', NoDefault) is not NoDefault: default_encountered = True elif default_encountered: raise TypeError(f'Type parameter {t!r} without a default'