From 677b446666a2cf8395078911522cdd1d00bf05bc Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Mon, 20 Feb 2023 16:39:06 -0800 Subject: [PATCH 01/46] Include tox.ini in sdist (#120) Fixes #117 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 41bf2bed..67d81a17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,5 +57,5 @@ name = "Guido van Rossum, Jukka Lehtosalo, Łukasz Langa, Michael Lee" email = "levkivskyi@gmail.com" [tool.flit.sdist] -include = ["CHANGELOG.md", "README.md", "*/*test*.py"] +include = ["CHANGELOG.md", "README.md", "tox.ini", "*/*test*.py"] exclude = [] From a0858e6ba9b46996f3f74dde8749ab86e1561012 Mon Sep 17 00:00:00 2001 From: JosephSBoyle <48555120+JosephSBoyle@users.noreply.github.com> Date: Sun, 5 Mar 2023 15:43:19 +0000 Subject: [PATCH 02/46] Fix unused classes in test case (#122) --- src/test_typing_extensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 208382a0..ad32e0e2 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1859,8 +1859,8 @@ def __init__(self): class DI: def __init__(self): self.x = None - self.assertIsInstance(C(), P) - self.assertIsInstance(D(), P) + self.assertIsInstance(CI(), P) + self.assertIsInstance(DI(), P) def test_protocols_in_unions(self): class P(Protocol): From ac52ac5f2cb0e00e7988bae1e2a1b8257ac88d6d Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 6 Mar 2023 01:48:46 +0000 Subject: [PATCH 03/46] Remove duplicate test (#123) This method is identical to `test_hash_eq` on lines 2296-2304: https://github.com/python/typing_extensions/blob/a0858e6ba9b46996f3f74dde8749ab86e1561012/src/test_typing_extensions.py#L2296-L2304 (This is a backport of the only relevant part of https://github.com/python/cpython/pull/102445) --- src/test_typing_extensions.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index ad32e0e2..7f9e59ae 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -2368,16 +2368,6 @@ class C: get_type_hints(C, globals())["const"], Annotated[Final[int], "Const"] ) - def test_hash_eq(self): - self.assertEqual(len({Annotated[int, 4, 5], Annotated[int, 4, 5]}), 1) - self.assertNotEqual(Annotated[int, 4, 5], Annotated[int, 5, 4]) - self.assertNotEqual(Annotated[int, 4, 5], Annotated[str, 4, 5]) - self.assertNotEqual(Annotated[int, 4], Annotated[int, 4, 4]) - self.assertEqual( - {Annotated[int, 4, 5], Annotated[int, 4, 5], Annotated[T, 4, 5]}, - {Annotated[int, 4, 5], Annotated[T, 4, 5]} - ) - def test_cannot_subclass(self): with self.assertRaisesRegex(TypeError, "Cannot subclass .*Annotated"): class C(Annotated): From 2ec0122279252ebf6c567df3a075f3c68f83ef54 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 8 Mar 2023 05:47:41 -0800 Subject: [PATCH 04/46] deprecated: Update docstring (#124) --- src/typing_extensions.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 6ae0c34c..71d2a7a5 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2163,7 +2163,15 @@ def g(x: str) -> int: ... When this decorator is applied to an object, the type checker will generate a diagnostic on usage of the deprecated object. - No runtime warning is issued. The decorator sets the ``__deprecated__`` + The warning specified by ``category`` will be emitted on use + of deprecated objects. For functions, that happens on calls; + for classes, on instantiation. If the ``category`` is ``None``, + no warning is emitted. The ``stacklevel`` determines where the + warning is emitted. If it is ``1`` (the default), the warning + is emitted at the direct caller of the deprecated object; if it + is higher, it is emitted further up the stack. + + The decorator sets the ``__deprecated__`` attribute on the decorated object to the deprecation message passed to the decorator. If applied to an overload, the decorator must be after the ``@overload`` decorator for the attribute to From 37909ec67adf56070fa7218efa475e87812834e5 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 8 Mar 2023 05:50:09 -0800 Subject: [PATCH 05/46] Add typing_extensions.Buffer (#125) --- CHANGELOG.md | 6 ++++++ README.md | 8 ++++++-- src/test_typing_extensions.py | 32 +++++++++++++++++++++++++++++++- src/typing_extensions.py | 30 ++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d330a0f5..d4bc0322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# Unreleased + +- Add `typing_extensions.Buffer`, a marker class for buffer types, as proposed + by PEP 688. Equivalent to `collections.abc.Buffer` in Python 3.12. Patch by + Jelle Zijlstra. + # Release 4.5.0 (February 14, 2023) - Runtime support for PEP 702, adding `typing_extensions.deprecated`. Patch diff --git a/README.md b/README.md index 6da36c37..b29378ba 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,15 @@ This module currently contains the following: - Experimental features - - `override` (see [PEP 698](https://peps.python.org/pep-0698/)) - The `default=` argument to `TypeVar`, `ParamSpec`, and `TypeVarTuple` (see [PEP 696](https://peps.python.org/pep-0696/)) - The `infer_variance=` argument to `TypeVar` (see [PEP 695](https://peps.python.org/pep-0695/)) - The `@deprecated` decorator (see [PEP 702](https://peps.python.org/pep-0702/)) +- In the standard library since Python 3.12 + + - `override` (equivalent to `typing.override`; see [PEP 698](https://peps.python.org/pep-0698/)) + - `Buffer` (equivalent to `collections.abc.Buffer`; see [PEP 688](https://peps.python.org/pep-0688/)) + - In `typing` since Python 3.11 - `assert_never` @@ -159,4 +163,4 @@ These types are only guaranteed to work for static type checking. ## Running tests To run tests, navigate into the appropriate source directory and run -`test_typing_extensions.py`. \ No newline at end of file +`test_typing_extensions.py`. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 7f9e59ae..b4e21d63 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -29,7 +29,7 @@ from typing_extensions import assert_type, get_type_hints, get_origin, get_args from typing_extensions import clear_overloads, get_overloads, overload from typing_extensions import NamedTuple -from typing_extensions import override, deprecated +from typing_extensions import override, deprecated, Buffer from _typed_dict_test_helper import Foo, FooGeneric import warnings @@ -3677,5 +3677,35 @@ def test_pickle(self): self.assertEqual(z.__infer_variance__, typevar.__infer_variance__) +class BufferTests(BaseTestCase): + def test(self): + self.assertIsInstance(memoryview(b''), Buffer) + self.assertIsInstance(bytearray(), Buffer) + self.assertIsInstance(b"x", Buffer) + self.assertNotIsInstance(1, Buffer) + + self.assertIsSubclass(bytearray, Buffer) + self.assertIsSubclass(memoryview, Buffer) + self.assertIsSubclass(bytes, Buffer) + self.assertNotIsSubclass(int, Buffer) + + class MyRegisteredBuffer: + def __buffer__(self, flags: int) -> memoryview: + return memoryview(b'') + + self.assertNotIsInstance(MyRegisteredBuffer(), Buffer) + self.assertNotIsSubclass(MyRegisteredBuffer, Buffer) + Buffer.register(MyRegisteredBuffer) + self.assertIsInstance(MyRegisteredBuffer(), Buffer) + self.assertIsSubclass(MyRegisteredBuffer, Buffer) + + class MySubclassedBuffer(Buffer): + def __buffer__(self, flags: int) -> memoryview: + return memoryview(b'') + + self.assertIsInstance(MySubclassedBuffer(), Buffer) + self.assertIsSubclass(MySubclassedBuffer, Buffer) + + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 71d2a7a5..b29e37cb 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -33,6 +33,7 @@ 'Coroutine', 'AsyncGenerator', 'AsyncContextManager', + 'Buffer', 'ChainMap', # Concrete collection types. @@ -2318,3 +2319,32 @@ def _namedtuple_mro_entries(bases): return (_NamedTuple,) NamedTuple.__mro_entries__ = _namedtuple_mro_entries + + +if hasattr(collections.abc, "Buffer"): + Buffer = collections.abc.Buffer +else: + class Buffer(abc.ABC): + """Base class for classes that implement the buffer protocol. + + The buffer protocol allows Python objects to expose a low-level + memory buffer interface. Before Python 3.12, it is not possible + to implement the buffer protocol in pure Python code, or even + to check whether a class implements the buffer protocol. In + Python 3.12 and higher, the ``__buffer__`` method allows access + to the buffer protocol from Python code, and the + ``collections.abc.Buffer`` ABC allows checking whether a class + implements the buffer protocol. + + To indicate support for the buffer protocol in earlier versions, + inherit from this ABC, either in a stub file or at runtime, + or use ABC registration. This ABC provides no methods, because + there is no Python-accessible methods shared by pre-3.12 buffer + classes. It is useful primarily for static checks. + + """ + + # As a courtesy, register the most common stdlib buffer classes. + Buffer.register(memoryview) + Buffer.register(bytearray) + Buffer.register(bytes) From c661cde89a3dcf5c240c6e26b20ab14afce4b9e9 Mon Sep 17 00:00:00 2001 From: YouJiacheng <1503679330@qq.com> Date: Thu, 6 Apr 2023 07:21:50 +0800 Subject: [PATCH 06/46] Move builtin protocol whitelist to mapping instead of list (#128) Co-authored-by: Alex Waygood --- src/test_typing_extensions.py | 8 ++++++++ src/typing_extensions.py | 15 +++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index b4e21d63..ab0cc256 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1910,6 +1910,14 @@ def close(self): self.assertIsSubclass(B, Custom) self.assertNotIsSubclass(A, Custom) + def test_builtin_protocol_allowlist(self): + with self.assertRaises(TypeError): + class CustomProtocol(TestCase, Protocol): + pass + + class CustomContextManager(typing.ContextManager, Protocol): + pass + def test_no_init_same_for_different_protocol_implementations(self): class CustomProtocolWithoutInitA(Protocol): pass diff --git a/src/typing_extensions.py b/src/typing_extensions.py index b29e37cb..9166d46a 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -389,10 +389,13 @@ def clear_overloads(): TYPE_CHECKING = typing.TYPE_CHECKING -_PROTO_WHITELIST = ['Callable', 'Awaitable', - 'Iterable', 'Iterator', 'AsyncIterable', 'AsyncIterator', - 'Hashable', 'Sized', 'Container', 'Collection', 'Reversible', - 'ContextManager', 'AsyncContextManager'] +_PROTO_ALLOWLIST = { + 'collections.abc': [ + 'Callable', 'Awaitable', 'Iterable', 'Iterator', 'AsyncIterable', + 'Hashable', 'Sized', 'Container', 'Collection', 'Reversible', + ], + 'contextlib': ['AbstractContextManager', 'AbstractAsyncContextManager'], +} def _get_protocol_attrs(cls): @@ -608,8 +611,8 @@ def _proto_hook(other): # Check consistency of bases. for base in cls.__bases__: if not (base in (object, typing.Generic) or - base.__module__ == 'collections.abc' and - base.__name__ in _PROTO_WHITELIST or + base.__module__ in _PROTO_ALLOWLIST and + base.__name__ in _PROTO_ALLOWLIST[base.__module__] or isinstance(base, _ProtocolMeta) and base._is_protocol): raise TypeError('Protocols can only inherit from other' f' protocols, got {repr(base)}') From 745ff293e4977b857186f3619d8f4c6c77023ae6 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 9 Apr 2023 21:20:47 +0100 Subject: [PATCH 07/46] Backport CPython PR 27387 (#130) --- src/typing_extensions.py | 53 ++++++++++++++-------------------------- 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 9166d46a..e39057ac 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -461,6 +461,13 @@ def _maybe_adjust_parameters(cls): cls.__parameters__ = tuple(tvars) +def _caller(depth=2): + try: + return sys._getframe(depth).f_globals.get('__name__', '__main__') + except (AttributeError, ValueError): # For platforms without _getframe() + return None + + # 3.8+ if hasattr(typing, 'Protocol'): Protocol = typing.Protocol @@ -574,12 +581,12 @@ def _proto_hook(other): if not cls.__dict__.get('_is_protocol', None): return NotImplemented if not getattr(cls, '_is_runtime_protocol', False): - if sys._getframe(2).f_globals['__name__'] in ['abc', 'functools']: + if _caller(depth=3) in {'abc', 'functools'}: return NotImplemented raise TypeError("Instance and class checks can only be used with" " @runtime protocols") if not _is_callable_members_only(cls): - if sys._getframe(2).f_globals['__name__'] in ['abc', 'functools']: + if _caller(depth=3) in {'abc', 'functools'}: return NotImplemented raise TypeError("Protocols with non-method members" " don't support issubclass()") @@ -671,9 +678,7 @@ def __index__(self) -> int: else: def _check_fails(cls, other): try: - if sys._getframe(1).f_globals['__name__'] not in ['abc', - 'functools', - 'typing']: + if _caller() not in {'abc', 'functools', 'typing'}: # Typed dicts are only for static structural subtyping. raise TypeError('TypedDict does not support instance and class checks') except (AttributeError, ValueError): @@ -724,11 +729,10 @@ def _typeddict_new(*args, total=True, **kwargs): " but not both") ns = {'__annotations__': dict(fields)} - try: + module = _caller() + if module is not None: # Setting correct module is necessary to make typed dict classes pickleable. - ns['__module__'] = sys._getframe(1).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - pass + ns['__module__'] = module return _TypedDictMeta(typename, (), ns, total=total) @@ -1189,10 +1193,7 @@ def __init__(self, name, *constraints, bound=None, self.__infer_variance__ = infer_variance # for pickling: - try: - def_mod = sys._getframe(1).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - def_mod = None + def_mod = _caller() if def_mod != 'typing_extensions': self.__module__ = def_mod @@ -1275,10 +1276,7 @@ def __init__(self, name, *, bound=None, covariant=False, contravariant=False, _DefaultMixin.__init__(self, default) # for pickling: - try: - def_mod = sys._getframe(1).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - def_mod = None + def_mod = _caller() if def_mod != 'typing_extensions': self.__module__ = def_mod @@ -1357,10 +1355,7 @@ def __init__(self, name, *, bound=None, covariant=False, contravariant=False, _DefaultMixin.__init__(self, default) # for pickling: - try: - def_mod = sys._getframe(1).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - def_mod = None + def_mod = _caller() if def_mod != 'typing_extensions': self.__module__ = def_mod @@ -1866,10 +1861,7 @@ def __init__(self, name, *, default=_marker): _DefaultMixin.__init__(self, default) # for pickling: - try: - def_mod = sys._getframe(1).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - def_mod = None + def_mod = _caller() if def_mod != 'typing_extensions': self.__module__ = def_mod @@ -1929,10 +1921,7 @@ def __init__(self, name, *, default=_marker): _DefaultMixin.__init__(self, default) # for pickling: - try: - def_mod = sys._getframe(1).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - def_mod = None + def_mod = _caller() if def_mod != 'typing_extensions': self.__module__ = def_mod @@ -2241,12 +2230,6 @@ def wrapper(*args, **kwargs): if sys.version_info >= (3, 11): NamedTuple = typing.NamedTuple else: - def _caller(): - try: - return sys._getframe(2).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): # For platforms without _getframe() - return None - def _make_nmtuple(name, types, module, defaults=()): fields = [n for n, t in types] annotations = {n: typing._type_check(t, f"field {n} annotation must be a type") From 8d7c7982bcf964b7af7ed5c2fc3436e72a1beeaf Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 9 Apr 2023 21:22:11 +0100 Subject: [PATCH 08/46] Allow "test and lint" workflow to be run manually from forks (#129) --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a2a04098..82b3887b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,7 @@ name: Test and lint on: push: pull_request: + workflow_dispatch: permissions: contents: read From 6db30674f922806103b515404c5277d360de3784 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 9 Apr 2023 22:28:46 +0100 Subject: [PATCH 09/46] Suppress DeprecationWarnings and `print`s when running tests (#131) --- src/test_typing_extensions.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index ab0cc256..21630ad0 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1,6 +1,7 @@ import sys import os import abc +import io import contextlib import collections from collections import defaultdict @@ -62,6 +63,13 @@ def assertNotIsSubclass(self, cls, class_or_tuple, msg=None): message += f' : {msg}' raise self.failureException(message) + @contextlib.contextmanager + def assertWarnsIf(self, condition: bool, expected_warning: Type[Warning]): + with contextlib.ExitStack() as stack: + if condition: + stack.enter_context(self.assertWarns(expected_warning)) + yield + class Employee: pass @@ -238,8 +246,9 @@ class A: with self.assertWarnsRegex(DeprecationWarning, "A will go away soon"): A() - with self.assertRaises(TypeError): - A(42) + with self.assertWarnsRegex(DeprecationWarning, "A will go away soon"): + with self.assertRaises(TypeError): + A(42) @deprecated("HasInit will go away soon") class HasInit: @@ -1959,7 +1968,8 @@ def test_basics_iterable_syntax(self): self.assertEqual(Emp.__total__, True) def test_basics_keywords_syntax(self): - Emp = TypedDict('Emp', name=str, id=int) + with self.assertWarnsIf(sys.version_info >= (3, 11), DeprecationWarning): + Emp = TypedDict('Emp', name=str, id=int) self.assertIsSubclass(Emp, dict) self.assertIsSubclass(Emp, typing.MutableMapping) self.assertNotIsSubclass(Emp, collections.abc.Sequence) @@ -1974,8 +1984,9 @@ def test_basics_keywords_syntax(self): self.assertEqual(Emp.__total__, True) def test_typeddict_special_keyword_names(self): - TD = TypedDict("TD", cls=type, self=object, typename=str, _typename=int, - fields=list, _fields=dict) + with self.assertWarnsIf(sys.version_info >= (3, 11), DeprecationWarning): + TD = TypedDict("TD", cls=type, self=object, typename=str, _typename=int, + fields=list, _fields=dict) self.assertEqual(TD.__name__, 'TD') self.assertEqual(TD.__annotations__, {'cls': type, 'self': object, 'typename': str, '_typename': int, 'fields': list, '_fields': dict}) @@ -2044,7 +2055,7 @@ def test_py36_class_syntax_usage(self): def test_pickle(self): global EmpD # pickle wants to reference the class by name - EmpD = TypedDict('EmpD', name=str, id=int) + EmpD = TypedDict('EmpD', {"name": str, "id": int}) jane = EmpD({'name': 'jane', 'id': 37}) point = Point2DGeneric(a=5.0, b=3.0) for proto in range(pickle.HIGHEST_PROTOCOL + 1): @@ -2066,7 +2077,7 @@ def test_pickle(self): self.assertEqual(Point2DGenericNew({'a': 5.0, 'b': 3.0}), point) def test_optional(self): - EmpD = TypedDict('EmpD', name=str, id=int) + EmpD = TypedDict('EmpD', {"name": str, "id": int}) self.assertEqual(typing.Optional[EmpD], typing.Union[None, EmpD]) self.assertNotEqual(typing.List[EmpD], typing.Tuple[EmpD]) @@ -3134,7 +3145,10 @@ def cached(self): ... class RevealTypeTests(BaseTestCase): def test_reveal_type(self): obj = object() - self.assertIs(obj, reveal_type(obj)) + + with contextlib.redirect_stderr(io.StringIO()) as stderr: + self.assertIs(obj, reveal_type(obj)) + self.assertEqual("Runtime type is 'object'", stderr.getvalue().strip()) class DataclassTransformTests(BaseTestCase): From ad8a08bec49b9d89ef32ef10307d438abedab2db Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 10 Apr 2023 14:34:25 +0100 Subject: [PATCH 10/46] Improve flake8 config (#133) --- .flake8 | 1 + .flake8-tests | 1 + src/test_typing_extensions.py | 6 +++--- src/typing_extensions.py | 8 ++++---- test-requirements.txt | 2 +- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.flake8 b/.flake8 index fc71a6e2..1d2ae2b6 100644 --- a/.flake8 +++ b/.flake8 @@ -13,3 +13,4 @@ exclude = # tests have more relaxed formatting rules # and its own specific config in .flake8-tests src/test_typing_extensions.py, +noqa_require_code = true diff --git a/.flake8-tests b/.flake8-tests index 5a97fe89..db99f22a 100644 --- a/.flake8-tests +++ b/.flake8-tests @@ -26,3 +26,4 @@ ignore = DW12, # consistency with mypy W504 +noqa_require_code = true diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 21630ad0..1183eaba 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -582,7 +582,7 @@ def test_no_isinstance(self): class IntVarTests(BaseTestCase): def test_valid(self): - T_ints = IntVar("T_ints") # noqa + T_ints = IntVar("T_ints") def test_invalid(self): with self.assertRaises(TypeError): @@ -590,7 +590,7 @@ def test_invalid(self): with self.assertRaises(TypeError): T_ints = IntVar("T_ints", bound=int) with self.assertRaises(TypeError): - T_ints = IntVar("T_ints", covariant=True) # noqa + T_ints = IntVar("T_ints", covariant=True) class LiteralTests(BaseTestCase): @@ -936,7 +936,7 @@ def test_respect_no_type_check(self): @no_type_check class NoTpCheck: class Inn: - def __init__(self, x: 'not a type'): ... # noqa + def __init__(self, x: 'not a type'): ... self.assertTrue(NoTpCheck.__no_type_check__) self.assertTrue(NoTpCheck.Inn.__init__.__no_type_check__) self.assertEqual(gth(ann_module2.NTC.meth), {}) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index e39057ac..9ef87b97 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -289,7 +289,7 @@ def __getitem__(self, parameters): instead of a type.""") -_overload_dummy = typing._overload_dummy # noqa +_overload_dummy = typing._overload_dummy if hasattr(typing, "get_overloads"): # 3.11+ @@ -478,7 +478,7 @@ def _no_init(self, *args, **kwargs): if type(self)._is_protocol: raise TypeError('Protocols cannot be instantiated') - class _ProtocolMeta(abc.ABCMeta): # noqa: B024 + class _ProtocolMeta(abc.ABCMeta): # This metaclass is a bit unfortunate and exists only because of the lack # of __instancehook__. def __instancecheck__(cls, instance): @@ -545,7 +545,7 @@ def __class_getitem__(cls, params): raise TypeError( f"Parameter list to {cls.__qualname__}[...] cannot be empty") msg = "Parameters to generic types must be types." - params = tuple(typing._type_check(p, msg) for p in params) # noqa + params = tuple(typing._type_check(p, msg) for p in params) if cls is Protocol: # Generic can only be subscripted with unique type variables. if not all(isinstance(p, typing.TypeVar) for p in params): @@ -1435,7 +1435,7 @@ def _concatenate_getitem(self, parameters): # 3.10+ if hasattr(typing, 'Concatenate'): Concatenate = typing.Concatenate - _ConcatenateGenericAlias = typing._ConcatenateGenericAlias # noqa + _ConcatenateGenericAlias = typing._ConcatenateGenericAlias # noqa: F811 # 3.9 elif sys.version_info[:2] >= (3, 9): @_TypeAliasForm diff --git a/test-requirements.txt b/test-requirements.txt index 05c4c918..ad5c3e58 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,3 @@ flake8 flake8-bugbear -flake8-pyi>=22.8.0 +flake8-noqa From 0fc655dcae9c7efeb592cce5cd5677a7be951e56 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 10 Apr 2023 16:53:15 +0100 Subject: [PATCH 11/46] Runtime-checkable protocol tests: Use `@runtime_checkable`, not `@runtime` (#134) --- src/test_typing_extensions.py | 60 ++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 1183eaba..9092eae9 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -850,7 +850,7 @@ def __str__(self): def __add__(self, other): return 0 -@runtime +@runtime_checkable class HasCallProtocol(Protocol): __call__: typing.Callable @@ -1324,7 +1324,7 @@ class Coordinate(Protocol): x: int y: int -@runtime +@runtime_checkable class Point(Coordinate, Protocol): label: str @@ -1339,11 +1339,11 @@ class XAxis(Protocol): class YAxis(Protocol): y: int -@runtime +@runtime_checkable class Position(XAxis, YAxis, Protocol): pass -@runtime +@runtime_checkable class Proto(Protocol): attr: int @@ -1367,9 +1367,11 @@ class NT(NamedTuple): class ProtocolTests(BaseTestCase): + def test_runtime_alias(self): + self.assertIs(runtime, runtime_checkable) def test_basic_protocol(self): - @runtime + @runtime_checkable class P(Protocol): def meth(self): pass @@ -1387,7 +1389,7 @@ def f(): self.assertNotIsInstance(f, P) def test_everything_implements_empty_protocol(self): - @runtime + @runtime_checkable class Empty(Protocol): pass class C: pass def f(): @@ -1437,7 +1439,7 @@ class CG(PG[T]): pass self.assertIsInstance(CG[int](), CG) def test_cannot_instantiate_abstract(self): - @runtime + @runtime_checkable class P(Protocol): @abc.abstractmethod def ameth(self) -> int: @@ -1455,7 +1457,7 @@ def test_subprotocols_extending(self): class P1(Protocol): def meth1(self): pass - @runtime + @runtime_checkable class P2(P1, Protocol): def meth2(self): pass @@ -1484,7 +1486,7 @@ def meth1(self): class P2(Protocol): def meth2(self): pass - @runtime + @runtime_checkable class P(P1, P2, Protocol): pass class C: @@ -1507,10 +1509,10 @@ def meth2(self): def test_protocols_issubclass(self): T = TypeVar('T') - @runtime + @runtime_checkable class P(Protocol): def x(self): ... - @runtime + @runtime_checkable class PG(Protocol[T]): def x(self): ... class BadP(Protocol): @@ -1538,7 +1540,7 @@ def x(self): ... def test_protocols_issubclass_non_callable(self): class C: x = 1 - @runtime + @runtime_checkable class PNonCall(Protocol): x = 1 with self.assertRaises(TypeError): @@ -1560,10 +1562,10 @@ class D(PNonCall): ... def test_protocols_isinstance(self): T = TypeVar('T') - @runtime + @runtime_checkable class P(Protocol): def meth(x): ... - @runtime + @runtime_checkable class PG(Protocol[T]): def meth(x): ... class BadP(Protocol): @@ -1616,10 +1618,10 @@ class Bad: pass def test_protocols_isinstance_init(self): T = TypeVar('T') - @runtime + @runtime_checkable class P(Protocol): x = 1 - @runtime + @runtime_checkable class PG(Protocol[T]): x = 1 class C: @@ -1629,7 +1631,7 @@ def __init__(self, x): self.assertIsInstance(C(1), PG) def test_protocols_support_register(self): - @runtime + @runtime_checkable class P(Protocol): x = 1 class PM(Protocol): @@ -1642,7 +1644,7 @@ class C: pass self.assertIsInstance(C(), D) def test_none_on_non_callable_doesnt_block_implementation(self): - @runtime + @runtime_checkable class P(Protocol): x = 1 class A: @@ -1656,7 +1658,7 @@ def __init__(self): self.assertIsInstance(C(), P) def test_none_on_callable_blocks_implementation(self): - @runtime + @runtime_checkable class P(Protocol): def x(self): ... class A: @@ -1672,7 +1674,7 @@ def __init__(self): def test_non_protocol_subclasses(self): class P(Protocol): x = 1 - @runtime + @runtime_checkable class PR(Protocol): def meth(self): pass class NonP(P): @@ -1705,7 +1707,7 @@ def __subclasshook__(cls, other): self.assertNotIsSubclass(BadClass, C) def test_issubclass_fails_correctly(self): - @runtime + @runtime_checkable class P(Protocol): x = 1 class C: pass @@ -1715,7 +1717,7 @@ class C: pass def test_defining_generic_protocols(self): T = TypeVar('T') S = TypeVar('S') - @runtime + @runtime_checkable class PR(Protocol[T, S]): def meth(self): pass class P(PR[int, T], Protocol[T]): @@ -1739,7 +1741,7 @@ class C(PR[int, T]): pass def test_defining_generic_protocols_old_style(self): T = TypeVar('T') S = TypeVar('S') - @runtime + @runtime_checkable class PR(Protocol, Generic[T, S]): def meth(self): pass class P(PR[int, str], Protocol): @@ -1756,7 +1758,7 @@ class P1(Protocol, Generic[T]): def bar(self, x: T) -> str: ... class P2(Generic[T], Protocol): def bar(self, x: T) -> str: ... - @runtime + @runtime_checkable class PSub(P1[str], Protocol): x = 1 class Test: @@ -1813,7 +1815,7 @@ class P(Protocol[T]): pass self.assertIs(P[int].__origin__, P) def test_generic_protocols_special_from_protocol(self): - @runtime + @runtime_checkable class PR(Protocol): x = 1 class P(Protocol): @@ -1841,17 +1843,17 @@ def meth(self): def test_no_runtime_deco_on_nominal(self): with self.assertRaises(TypeError): - @runtime + @runtime_checkable class C: pass class Proto(Protocol): x = 1 with self.assertRaises(TypeError): - @runtime + @runtime_checkable class Concrete(Proto): pass def test_none_treated_correctly(self): - @runtime + @runtime_checkable class P(Protocol): x: int = None class B(object): pass @@ -1882,7 +1884,7 @@ def test_protocols_pickleable(self): global P, CP # pickle wants to reference the class by name T = TypeVar('T') - @runtime + @runtime_checkable class P(Protocol[T]): x = 1 class CP(P[int]): From 25b09718c4125894242e4f5955a4f5f6cd57c91f Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 12 Apr 2023 09:09:43 +0100 Subject: [PATCH 12/46] flake8 config: ignore W503 (#135) --- .flake8 | 2 ++ .flake8-tests | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.flake8 b/.flake8 index 1d2ae2b6..03237510 100644 --- a/.flake8 +++ b/.flake8 @@ -7,6 +7,8 @@ ignore = DW12, # code is sometimes better without this E129, + # Contradicts PEP8 nowadays + W503, # consistency with mypy W504 exclude = diff --git a/.flake8-tests b/.flake8-tests index db99f22a..634160ab 100644 --- a/.flake8-tests +++ b/.flake8-tests @@ -24,6 +24,8 @@ ignore = # irrelevant plugins B3, DW12, + # Contradicts PEP8 nowadays + W503, # consistency with mypy W504 noqa_require_code = true From 7e998c28b9969dcc2bc13e84ea98a0bfa475b2ae Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 12 Apr 2023 09:39:24 +0100 Subject: [PATCH 13/46] Fix issue when non runtime_protocol does not raise TypeError (#132) Backport of CPython PR 26067 (https://github.com/python/cpython/pull/26067) --- CHANGELOG.md | 4 ++ src/test_typing_extensions.py | 31 +++++++++++++--- src/typing_extensions.py | 69 ++++++++++++++++++++++++----------- 3 files changed, 76 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4bc0322..5b09065e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ - Add `typing_extensions.Buffer`, a marker class for buffer types, as proposed by PEP 688. Equivalent to `collections.abc.Buffer` in Python 3.12. Patch by Jelle Zijlstra. +- Backport [CPython PR 26067](https://github.com/python/cpython/pull/26067) + (originally by Yurii Karabas), ensuring that `isinstance()` calls on + protocols raise `TypeError` when the protocol is not decorated with + `@runtime_checkable`. Patch by Alex Waygood. # Release 4.5.0 (February 14, 2023) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 9092eae9..b8e48af8 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1421,6 +1421,22 @@ class E(C, BP): pass self.assertNotIsInstance(D(), E) self.assertNotIsInstance(E(), D) + @skipUnless( + hasattr(typing, "Protocol"), + "Test is only relevant if typing.Protocol exists" + ) + def test_runtimecheckable_on_typing_dot_Protocol(self): + @runtime_checkable + class Foo(typing.Protocol): + x: int + + class Bar: + def __init__(self): + self.x = 42 + + self.assertIsInstance(Bar(), Foo) + self.assertNotIsInstance(object(), Foo) + def test_no_instantiation(self): class P(Protocol): pass with self.assertRaises(TypeError): @@ -1829,11 +1845,7 @@ def meth(self): self.assertTrue(P._is_protocol) self.assertTrue(PR._is_protocol) self.assertTrue(PG._is_protocol) - if hasattr(typing, 'Protocol'): - self.assertFalse(P._is_runtime_protocol) - else: - with self.assertRaises(AttributeError): - self.assertFalse(P._is_runtime_protocol) + self.assertFalse(P._is_runtime_protocol) self.assertTrue(PR._is_runtime_protocol) self.assertTrue(PG[int]._is_protocol) self.assertEqual(typing_extensions._get_protocol_attrs(P), {'meth'}) @@ -1929,6 +1941,13 @@ class CustomProtocol(TestCase, Protocol): class CustomContextManager(typing.ContextManager, Protocol): pass + def test_non_runtime_protocol_isinstance_check(self): + class P(Protocol): + x: int + + with self.assertRaisesRegex(TypeError, "@runtime_checkable"): + isinstance(1, P) + def test_no_init_same_for_different_protocol_implementations(self): class CustomProtocolWithoutInitA(Protocol): pass @@ -3314,7 +3333,7 @@ def test_typing_extensions_defers_when_possible(self): 'is_typeddict', } if sys.version_info < (3, 10): - exclude |= {'get_args', 'get_origin'} + exclude |= {'get_args', 'get_origin', 'Protocol', 'runtime_checkable'} if sys.version_info < (3, 11): exclude |= {'final', 'NamedTuple', 'Any'} for item in typing_extensions.__all__: diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 9ef87b97..6527cdb6 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -398,6 +398,25 @@ def clear_overloads(): } +_EXCLUDED_ATTRS = { + "__abstractmethods__", "__annotations__", "__weakref__", "_is_protocol", + "_is_runtime_protocol", "__dict__", "__slots__", "__parameters__", + "__orig_bases__", "__module__", "_MutableMapping__marker", "__doc__", + "__subclasshook__", "__orig_class__", "__init__", "__new__", +} + +if sys.version_info < (3, 8): + _EXCLUDED_ATTRS |= { + "_gorg", "__next_in_mro__", "__extra__", "__tree_hash__", "__args__", + "__origin__" + } + +if sys.version_info >= (3, 9): + _EXCLUDED_ATTRS.add("__class_getitem__") + +_EXCLUDED_ATTRS = frozenset(_EXCLUDED_ATTRS) + + def _get_protocol_attrs(cls): attrs = set() for base in cls.__mro__[:-1]: # without object @@ -405,14 +424,7 @@ def _get_protocol_attrs(cls): continue annotations = getattr(base, '__annotations__', {}) for attr in list(base.__dict__.keys()) + list(annotations.keys()): - if (not attr.startswith('_abc_') and attr not in ( - '__abstractmethods__', '__annotations__', '__weakref__', - '_is_protocol', '_is_runtime_protocol', '__dict__', - '__args__', '__slots__', - '__next_in_mro__', '__parameters__', '__origin__', - '__orig_bases__', '__extra__', '__tree_hash__', - '__doc__', '__subclasshook__', '__init__', '__new__', - '__module__', '_MutableMapping__marker', '_gorg')): + if (not attr.startswith('_abc_') and attr not in _EXCLUDED_ATTRS): attrs.add(attr) return attrs @@ -468,11 +480,18 @@ def _caller(depth=2): return None -# 3.8+ -if hasattr(typing, 'Protocol'): +# A bug in runtime-checkable protocols was fixed in 3.10+, +# but we backport it to all versions +if sys.version_info >= (3, 10): Protocol = typing.Protocol -# 3.7 + runtime_checkable = typing.runtime_checkable else: + def _allow_reckless_class_checks(depth=4): + """Allow instance and class checks for special stdlib modules. + The abc and functools modules indiscriminately call isinstance() and + issubclass() on the whole MRO of a user class, which may contain protocols. + """ + return _caller(depth) in {'abc', 'functools', None} def _no_init(self, *args, **kwargs): if type(self)._is_protocol: @@ -484,11 +503,19 @@ class _ProtocolMeta(abc.ABCMeta): def __instancecheck__(cls, instance): # We need this method for situations where attributes are # assigned in __init__. - if ((not getattr(cls, '_is_protocol', False) or + is_protocol_cls = getattr(cls, "_is_protocol", False) + if ( + is_protocol_cls and + not getattr(cls, '_is_runtime_protocol', False) and + not _allow_reckless_class_checks(depth=2) + ): + raise TypeError("Instance and class checks can only be used with" + " @runtime_checkable protocols") + if ((not is_protocol_cls or _is_callable_members_only(cls)) and issubclass(instance.__class__, cls)): return True - if cls._is_protocol: + if is_protocol_cls: if all(hasattr(instance, attr) and (not callable(getattr(cls, attr, None)) or getattr(instance, attr) is not None) @@ -530,6 +557,7 @@ def meth(self) -> T: """ __slots__ = () _is_protocol = True + _is_runtime_protocol = False def __new__(cls, *args, **kwds): if cls is Protocol: @@ -581,12 +609,12 @@ def _proto_hook(other): if not cls.__dict__.get('_is_protocol', None): return NotImplemented if not getattr(cls, '_is_runtime_protocol', False): - if _caller(depth=3) in {'abc', 'functools'}: + if _allow_reckless_class_checks(): return NotImplemented raise TypeError("Instance and class checks can only be used with" " @runtime protocols") if not _is_callable_members_only(cls): - if _caller(depth=3) in {'abc', 'functools'}: + if _allow_reckless_class_checks(): return NotImplemented raise TypeError("Protocols with non-method members" " don't support issubclass()") @@ -625,12 +653,6 @@ def _proto_hook(other): f' protocols, got {repr(base)}') cls.__init__ = _no_init - -# 3.8+ -if hasattr(typing, 'runtime_checkable'): - runtime_checkable = typing.runtime_checkable -# 3.7 -else: def runtime_checkable(cls): """Mark a protocol class as a runtime protocol, so that it can be used with isinstance() and issubclass(). Raise TypeError @@ -639,7 +661,10 @@ def runtime_checkable(cls): This allows a simple-minded structural check very similar to the one-offs in collections.abc such as Hashable. """ - if not isinstance(cls, _ProtocolMeta) or not cls._is_protocol: + if not ( + (isinstance(cls, _ProtocolMeta) or issubclass(cls, typing.Generic)) + and getattr(cls, "_is_protocol", False) + ): raise TypeError('@runtime_checkable can be only applied to protocol classes,' f' got {cls!r}') cls._is_runtime_protocol = True From 31741e07fd32eb1d7f6e5f0e0f882da306838b4c Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 12 Apr 2023 12:56:22 +0100 Subject: [PATCH 14/46] Backport test coverage improvements for runtime-checkable protocols (#136) --- src/test_typing_extensions.py | 123 +++++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 2 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index b8e48af8..632487d0 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1584,14 +1584,31 @@ def meth(x): ... @runtime_checkable class PG(Protocol[T]): def meth(x): ... + @runtime_checkable + class WeirdProto(Protocol): + meth = str.maketrans + @runtime_checkable + class WeirdProto2(Protocol): + meth = lambda *args, **kwargs: None # noqa: E731 + class CustomCallable: + def __call__(self, *args, **kwargs): + pass + @runtime_checkable + class WeirderProto(Protocol): + meth = CustomCallable() class BadP(Protocol): def meth(x): ... class BadPG(Protocol[T]): def meth(x): ... class C: def meth(x): ... - self.assertIsInstance(C(), P) - self.assertIsInstance(C(), PG) + class C2: + def __init__(self): + self.meth = lambda: None + for klass in C, C2: + for proto in P, PG, WeirdProto, WeirdProto2, WeirderProto: + with self.subTest(klass=klass.__name__, proto=proto.__name__): + self.assertIsInstance(klass(), proto) with self.assertRaises(TypeError): isinstance(C(), PG[T]) with self.assertRaises(TypeError): @@ -1601,6 +1618,94 @@ def meth(x): ... with self.assertRaises(TypeError): isinstance(C(), BadPG) + def test_protocols_isinstance_properties_and_descriptors(self): + class C: + @property + def attr(self): + return 42 + + class CustomDescriptor: + def __get__(self, obj, objtype=None): + return 42 + + class D: + attr = CustomDescriptor() + + # Check that properties set on superclasses + # are still found by the isinstance() logic + class E(C): ... + class F(D): ... + + class Empty: ... + + T = TypeVar('T') + + @runtime_checkable + class P(Protocol): + @property + def attr(self): ... + + @runtime_checkable + class P1(Protocol): + attr: int + + @runtime_checkable + class PG(Protocol[T]): + @property + def attr(self): ... + + @runtime_checkable + class PG1(Protocol[T]): + attr: T + + for protocol_class in P, P1, PG, PG1: + for klass in C, D, E, F: + with self.subTest( + klass=klass.__name__, + protocol_class=protocol_class.__name__ + ): + self.assertIsInstance(klass(), protocol_class) + + with self.subTest(klass="Empty", protocol_class=protocol_class.__name__): + self.assertNotIsInstance(Empty(), protocol_class) + + class BadP(Protocol): + @property + def attr(self): ... + + class BadP1(Protocol): + attr: int + + class BadPG(Protocol[T]): + @property + def attr(self): ... + + class BadPG1(Protocol[T]): + attr: T + + for obj in PG[T], PG[C], PG1[T], PG1[C], BadP, BadP1, BadPG, BadPG1: + for klass in C, D, E, F, Empty: + with self.subTest(klass=klass.__name__, obj=obj): + with self.assertRaises(TypeError): + isinstance(klass(), obj) + + def test_protocols_isinstance_not_fooled_by_custom_dir(self): + @runtime_checkable + class HasX(Protocol): + x: int + + class CustomDirWithX: + x = 10 + def __dir__(self): + return [] + + class CustomDirWithoutX: + def __dir__(self): + return ["x"] + + self.assertIsInstance(CustomDirWithX(), HasX) + self.assertNotIsInstance(CustomDirWithoutX(), HasX) + def test_protocols_isinstance_py36(self): class APoint: def __init__(self, x, y, label): @@ -1646,6 +1751,20 @@ def __init__(self, x): self.assertIsInstance(C(1), P) self.assertIsInstance(C(1), PG) + def test_protocols_isinstance_monkeypatching(self): + @runtime_checkable + class HasX(Protocol): + x: int + + class Foo: ... + + f = Foo() + self.assertNotIsInstance(f, HasX) + f.x = 42 + self.assertIsInstance(f, HasX) + del f.x + self.assertNotIsInstance(f, HasX) + def test_protocols_support_register(self): @runtime_checkable class P(Protocol): From 4dfc5c53325d95fa9faf842fa367b6c696c8c92c Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 12 Apr 2023 13:22:33 +0100 Subject: [PATCH 15/46] Remove flake8-noqa from test-requirements.txt (#138) --- .github/workflows/ci.yml | 3 +++ test-requirements.txt | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82b3887b..e813cb72 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,6 +57,9 @@ jobs: run: | pip install --upgrade pip pip install -r test-requirements.txt + # not included in test-requirements.txt as it depends on typing-extensions, + # so it's a pain to have it installed locally + pip install flake8-noqa - name: Lint implementation run: flake8 diff --git a/test-requirements.txt b/test-requirements.txt index ad5c3e58..675b2c5d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,2 @@ flake8 flake8-bugbear -flake8-noqa From 6c9395698bb31650ad3d53348379f483ebef496d Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 12 Apr 2023 18:07:21 +0100 Subject: [PATCH 16/46] Backport performance improvements to runtime-checkable protocols (#137) --- CHANGELOG.md | 12 ++++++++ src/test_typing_extensions.py | 4 ++- src/typing_extensions.py | 55 ++++++++++++++++++++++------------- 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b09065e..82ec6605 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ (originally by Yurii Karabas), ensuring that `isinstance()` calls on protocols raise `TypeError` when the protocol is not decorated with `@runtime_checkable`. Patch by Alex Waygood. +- Backport several significant performance improvements to runtime-checkable + protocols that have been made in Python 3.12 (see + https://github.com/python/cpython/issues/74690 for details). Patch by Alex + Waygood. + + A side effect of one of the performance improvements is that the members of + a runtime-checkable protocol are now considered “frozen” at runtime as soon + as the class has been created. Monkey-patching attributes onto a + runtime-checkable protocol will still work, but will have no impact on + `isinstance()` checks comparing objects to the protocol. See + ["What's New in Python 3.12"](https://docs.python.org/3.12/whatsnew/3.12.html#typing) + for more details. # Release 4.5.0 (February 14, 2023) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 632487d0..3a483b50 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -3452,9 +3452,11 @@ def test_typing_extensions_defers_when_possible(self): 'is_typeddict', } if sys.version_info < (3, 10): - exclude |= {'get_args', 'get_origin', 'Protocol', 'runtime_checkable'} + exclude |= {'get_args', 'get_origin'} if sys.version_info < (3, 11): exclude |= {'final', 'NamedTuple', 'Any'} + if sys.version_info < (3, 12): + exclude |= {'Protocol', 'runtime_checkable'} for item in typing_extensions.__all__: if item not in exclude and hasattr(typing, item): self.assertIs( diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 6527cdb6..c28680c3 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -403,6 +403,7 @@ def clear_overloads(): "_is_runtime_protocol", "__dict__", "__slots__", "__parameters__", "__orig_bases__", "__module__", "_MutableMapping__marker", "__doc__", "__subclasshook__", "__orig_class__", "__init__", "__new__", + "__protocol_attrs__", "__callable_proto_members_only__", } if sys.version_info < (3, 8): @@ -420,19 +421,15 @@ def clear_overloads(): def _get_protocol_attrs(cls): attrs = set() for base in cls.__mro__[:-1]: # without object - if base.__name__ in ('Protocol', 'Generic'): + if base.__name__ in {'Protocol', 'Generic'}: continue annotations = getattr(base, '__annotations__', {}) - for attr in list(base.__dict__.keys()) + list(annotations.keys()): + for attr in (*base.__dict__, *annotations): if (not attr.startswith('_abc_') and attr not in _EXCLUDED_ATTRS): attrs.add(attr) return attrs -def _is_callable_members_only(cls): - return all(callable(getattr(cls, attr, None)) for attr in _get_protocol_attrs(cls)) - - def _maybe_adjust_parameters(cls): """Helper function used in Protocol.__init_subclass__ and _TypedDictMeta.__new__. @@ -442,7 +439,7 @@ def _maybe_adjust_parameters(cls): """ tvars = [] if '__orig_bases__' in cls.__dict__: - tvars = typing._collect_type_vars(cls.__orig_bases__) + tvars = _collect_type_vars(cls.__orig_bases__) # Look for Generic[T1, ..., Tn] or Protocol[T1, ..., Tn]. # If found, tvars must be a subset of it. # If not found, tvars is it. @@ -480,9 +477,9 @@ def _caller(depth=2): return None -# A bug in runtime-checkable protocols was fixed in 3.10+, -# but we backport it to all versions -if sys.version_info >= (3, 10): +# The performance of runtime-checkable protocols is significantly improved on Python 3.12, +# so we backport the 3.12 version of Protocol to Python <=3.11 +if sys.version_info >= (3, 12): Protocol = typing.Protocol runtime_checkable = typing.runtime_checkable else: @@ -500,6 +497,15 @@ def _no_init(self, *args, **kwargs): class _ProtocolMeta(abc.ABCMeta): # This metaclass is a bit unfortunate and exists only because of the lack # of __instancehook__. + def __init__(cls, *args, **kwargs): + super().__init__(*args, **kwargs) + cls.__protocol_attrs__ = _get_protocol_attrs(cls) + # PEP 544 prohibits using issubclass() + # with protocols that have non-method members. + cls.__callable_proto_members_only__ = all( + callable(getattr(cls, attr, None)) for attr in cls.__protocol_attrs__ + ) + def __instancecheck__(cls, instance): # We need this method for situations where attributes are # assigned in __init__. @@ -511,17 +517,22 @@ def __instancecheck__(cls, instance): ): raise TypeError("Instance and class checks can only be used with" " @runtime_checkable protocols") - if ((not is_protocol_cls or - _is_callable_members_only(cls)) and - issubclass(instance.__class__, cls)): + + if super().__instancecheck__(instance): return True + if is_protocol_cls: - if all(hasattr(instance, attr) and - (not callable(getattr(cls, attr, None)) or - getattr(instance, attr) is not None) - for attr in _get_protocol_attrs(cls)): + for attr in cls.__protocol_attrs__: + try: + val = getattr(instance, attr) + except AttributeError: + break + if val is None and callable(getattr(cls, attr, None)): + break + else: return True - return super().__instancecheck__(instance) + + return False class Protocol(metaclass=_ProtocolMeta): # There is quite a lot of overlapping code with typing.Generic. @@ -613,7 +624,7 @@ def _proto_hook(other): return NotImplemented raise TypeError("Instance and class checks can only be used with" " @runtime protocols") - if not _is_callable_members_only(cls): + if not cls.__callable_proto_members_only__: if _allow_reckless_class_checks(): return NotImplemented raise TypeError("Protocols with non-method members" @@ -621,7 +632,7 @@ def _proto_hook(other): if not isinstance(other, type): # Same error as for issubclass(1, int) raise TypeError('issubclass() arg 1 must be a class') - for attr in _get_protocol_attrs(cls): + for attr in cls.__protocol_attrs__: for base in other.__mro__: if attr in base.__dict__: if base.__dict__[attr] is None: @@ -1819,6 +1830,10 @@ class Movie(TypedDict): if hasattr(typing, "Unpack"): # 3.11+ Unpack = typing.Unpack + + def _is_unpack(obj): + return get_origin(obj) is Unpack + elif sys.version_info[:2] >= (3, 9): class _UnpackSpecialForm(typing._SpecialForm, _root=True): def __repr__(self): From 8e14ace53573edb360653b1df454d68da0b4a663 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 13 Apr 2023 00:35:35 +0100 Subject: [PATCH 17/46] Use `inspect.getattr_static` in `_ProtocolMeta.__instancecheck__` (#140) --- CHANGELOG.md | 10 ++++ src/test_typing_extensions.py | 93 ++++++++++++++++++++++++++++++++++- src/typing_extensions.py | 2 +- 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82ec6605..792f25e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,16 @@ `isinstance()` checks comparing objects to the protocol. See ["What's New in Python 3.12"](https://docs.python.org/3.12/whatsnew/3.12.html#typing) for more details. +- `isinstance()` checks against runtime-checkable protocols now use + `inspect.getattr_static()` rather than `hasattr()` to lookup whether + attributes exist (backporting https://github.com/python/cpython/pull/103034). + This means that descriptors and `__getattr__` methods are no longer + unexpectedly evaluated during `isinstance()` checks against runtime-checkable + protocols. However, it may also mean that some objects which used to be + considered instances of a runtime-checkable protocol on older versions of + `typing_extensions` may no longer be considered instances of that protocol + using the new release, and vice versa. Most users are unlikely to be affected + by this change. Patch by Alex Waygood. # Release 4.5.0 (February 14, 2023) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 3a483b50..db4cf899 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1658,7 +1658,15 @@ def attr(self): ... class PG1(Protocol[T]): attr: T - for protocol_class in P, P1, PG, PG1: + @runtime_checkable + class MethodP(Protocol): + def attr(self): ... + + @runtime_checkable + class MethodPG(Protocol[T]): + def attr(self) -> T: ... + + for protocol_class in P, P1, PG, PG1, MethodP, MethodPG: for klass in C, D, E, F: with self.subTest( klass=klass.__name__, @@ -1683,7 +1691,12 @@ def attr(self): ... class BadPG1(Protocol[T]): attr: T - for obj in PG[T], PG[C], PG1[T], PG1[C], BadP, BadP1, BadPG, BadPG1: + cases = ( + PG[T], PG[C], PG1[T], PG1[C], MethodPG[T], + MethodPG[C], BadP, BadP1, BadPG, BadPG1 + ) + + for obj in cases: for klass in C, D, E, F, Empty: with self.subTest(klass=klass.__name__, obj=obj): with self.assertRaises(TypeError): @@ -1706,6 +1719,82 @@ def __dir__(self): self.assertIsInstance(CustomDirWithX(), HasX) self.assertNotIsInstance(CustomDirWithoutX(), HasX) + def test_protocols_isinstance_attribute_access_with_side_effects(self): + class C: + @property + def attr(self): + raise AttributeError('no') + + class CustomDescriptor: + def __get__(self, obj, objtype=None): + raise RuntimeError("NO") + + class D: + attr = CustomDescriptor() + + # Check that properties set on superclasses + # are still found by the isinstance() logic + class E(C): ... + class F(D): ... + + class WhyWouldYouDoThis: + def __getattr__(self, name): + raise RuntimeError("wut") + + T = TypeVar('T') + + @runtime_checkable + class P(Protocol): + @property + def attr(self): ... + + @runtime_checkable + class P1(Protocol): + attr: int + + @runtime_checkable + class PG(Protocol[T]): + @property + def attr(self): ... + + @runtime_checkable + class PG1(Protocol[T]): + attr: T + + @runtime_checkable + class MethodP(Protocol): + def attr(self): ... + + @runtime_checkable + class MethodPG(Protocol[T]): + def attr(self) -> T: ... + + for protocol_class in P, P1, PG, PG1, MethodP, MethodPG: + for klass in C, D, E, F: + with self.subTest( + klass=klass.__name__, + protocol_class=protocol_class.__name__ + ): + self.assertIsInstance(klass(), protocol_class) + + with self.subTest( + klass="WhyWouldYouDoThis", + protocol_class=protocol_class.__name__ + ): + self.assertNotIsInstance(WhyWouldYouDoThis(), protocol_class) + + def test_protocols_isinstance___slots__(self): + # As per the consensus in https://github.com/python/typing/issues/1367, + # this is desirable behaviour + @runtime_checkable + class HasX(Protocol): + x: int + + class HasNothingButSlots: + __slots__ = ("x",) + + self.assertIsInstance(HasNothingButSlots(), HasX) + def test_protocols_isinstance_py36(self): class APoint: def __init__(self, x, y, label): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index c28680c3..fc023921 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -524,7 +524,7 @@ def __instancecheck__(cls, instance): if is_protocol_cls: for attr in cls.__protocol_attrs__: try: - val = getattr(instance, attr) + val = inspect.getattr_static(instance, attr) except AttributeError: break if val is None and callable(getattr(cls, attr, None)): From 90c866b69c9bf887b056059727e0240c8b2f5d09 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 13 Apr 2023 14:32:25 +0100 Subject: [PATCH 18/46] Speedup `isinstance(3, typing_extensions.SupportsIndex)` by >10x (#141) --- CHANGELOG.md | 2 ++ src/test_typing_extensions.py | 2 +- src/typing_extensions.py | 5 ++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 792f25e6..d22b4049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ `typing_extensions` may no longer be considered instances of that protocol using the new release, and vice versa. Most users are unlikely to be affected by this change. Patch by Alex Waygood. +- Speedup `isinstance(3, typing_extensions.SupportsIndex)` by >10x on Python + <3.12. Patch by Alex Waygood. # Release 4.5.0 (February 14, 2023) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index db4cf899..0e4a1b7e 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -3545,7 +3545,7 @@ def test_typing_extensions_defers_when_possible(self): if sys.version_info < (3, 11): exclude |= {'final', 'NamedTuple', 'Any'} if sys.version_info < (3, 12): - exclude |= {'Protocol', 'runtime_checkable'} + exclude |= {'Protocol', 'runtime_checkable', 'SupportsIndex'} for item in typing_extensions.__all__: if item not in exclude and hasattr(typing, item): self.assertIs( diff --git a/src/typing_extensions.py b/src/typing_extensions.py index fc023921..b7864a98 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -686,10 +686,9 @@ def runtime_checkable(cls): runtime = runtime_checkable -# 3.8+ -if hasattr(typing, 'SupportsIndex'): +# Our version of runtime-checkable protocols is faster on Python 3.7-3.11 +if sys.version_info >= (3, 12): SupportsIndex = typing.SupportsIndex -# 3.7 else: @runtime_checkable class SupportsIndex(Protocol): From 501a00e4701185ec93220155a0caa3da71d327e8 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 13 Apr 2023 15:41:48 +0100 Subject: [PATCH 19/46] Backport the ability to define `__init__` methods on Protocol classes (#142) --- CHANGELOG.md | 4 ++++ src/test_typing_extensions.py | 26 ++++++++++++++++++++++++++ src/typing_extensions.py | 3 ++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d22b4049..7fa13198 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,10 @@ `typing_extensions` may no longer be considered instances of that protocol using the new release, and vice versa. Most users are unlikely to be affected by this change. Patch by Alex Waygood. +- Backport the ability to define `__init__` methods on Protocol classes, a + change made in Python 3.11 (originally implemented in + https://github.com/python/cpython/pull/31628 by Adrian Garcia Badaracco). + Patch by Alex Waygood. - Speedup `isinstance(3, typing_extensions.SupportsIndex)` by >10x on Python <3.12. Patch by Alex Waygood. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 0e4a1b7e..19a116fd 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1454,6 +1454,32 @@ class PG(Protocol[T]): pass class CG(PG[T]): pass self.assertIsInstance(CG[int](), CG) + def test_protocol_defining_init_does_not_get_overridden(self): + # check that P.__init__ doesn't get clobbered + # see https://bugs.python.org/issue44807 + + class P(Protocol): + x: int + def __init__(self, x: int) -> None: + self.x = x + class C: pass + + c = C() + P.__init__(c, 1) + self.assertEqual(c.x, 1) + + def test_concrete_class_inheriting_init_from_protocol(self): + class P(Protocol): + x: int + def __init__(self, x: int) -> None: + self.x = x + + class C(P): pass + + c = C(1) + self.assertIsInstance(c, C) + self.assertEqual(c.x, 1) + def test_cannot_instantiate_abstract(self): @runtime_checkable class P(Protocol): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index b7864a98..16a8fdd3 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -662,7 +662,8 @@ def _proto_hook(other): isinstance(base, _ProtocolMeta) and base._is_protocol): raise TypeError('Protocols can only inherit from other' f' protocols, got {repr(base)}') - cls.__init__ = _no_init + if cls.__init__ is Protocol.__init__: + cls.__init__ = _no_init def runtime_checkable(cls): """Mark a protocol class as a runtime protocol, so that it From 8bff0a36f3cd05236a4339e082915fd460d5a725 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 17 Apr 2023 12:02:33 -0600 Subject: [PATCH 20/46] Fix various things with `Literal` (#145) --- CHANGELOG.md | 10 ++++++ README.md | 3 ++ src/test_typing_extensions.py | 39 +++++++++++++++++++++- src/typing_extensions.py | 61 +++++++++++++++++++++++++++++++---- 4 files changed, 106 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fa13198..04f1a3f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,16 @@ - Add `typing_extensions.Buffer`, a marker class for buffer types, as proposed by PEP 688. Equivalent to `collections.abc.Buffer` in Python 3.12. Patch by Jelle Zijlstra. +- Backport two CPython PRs fixing various issues with `typing.Literal`: + https://github.com/python/cpython/pull/23294 and + https://github.com/python/cpython/pull/23383. Both CPython PRs were + originally by Yurii Karabas, and both were backported to Python >=3.9.1, but + no earlier. Patch by Alex Waygood. + + A side effect of one of the changes is that equality comparisons of `Literal` + objects will now raise a `TypeError` if one of the `Literal` objects being + compared has a mutable parameter. (Using mutable parameters with `Literal` is + not supported by PEP 586 or by any major static type checkers.) - Backport [CPython PR 26067](https://github.com/python/cpython/pull/26067) (originally by Yurii Karabas), ensuring that `isinstance()` calls on protocols raise `TypeError` when the protocol is not decorated with diff --git a/README.md b/README.md index b29378ba..46678afa 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,9 @@ Certain objects were changed after they were added to `typing`, and - `TypeVar` gains two additional parameters, `default=` and `infer_variance=`, in the draft PEPs [695](https://peps.python.org/pep-0695/) and [696](https://peps.python.org/pep-0696/), which are being considered for inclusion in Python 3.12. +- `Literal` does not flatten or deduplicate parameters on Python <3.9.1. The + `typing_extensions` version flattens and deduplicates parameters on all + Python versions. There are a few types whose interface was modified between different versions of typing. For example, `typing.Sequence` was modified to diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 19a116fd..0dbc3301 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -615,7 +615,8 @@ def test_literals_inside_other_types(self): List[Literal[("foo", "bar", "baz")]] def test_repr(self): - if hasattr(typing, 'Literal'): + # we backport various bugfixes that were added in 3.9.1 + if sys.version_info >= (3, 9, 1): mod_name = 'typing' else: mod_name = 'typing_extensions' @@ -624,6 +625,7 @@ def test_repr(self): self.assertEqual(repr(Literal[int]), mod_name + ".Literal[int]") self.assertEqual(repr(Literal), mod_name + ".Literal") self.assertEqual(repr(Literal[None]), mod_name + ".Literal[None]") + self.assertEqual(repr(Literal[1, 2, 3, 3]), mod_name + ".Literal[1, 2, 3]") def test_cannot_init(self): with self.assertRaises(TypeError): @@ -655,6 +657,39 @@ def test_no_multiple_subscripts(self): with self.assertRaises(TypeError): Literal[1][1] + def test_equal(self): + self.assertNotEqual(Literal[0], Literal[False]) + self.assertNotEqual(Literal[True], Literal[1]) + self.assertNotEqual(Literal[1], Literal[2]) + self.assertNotEqual(Literal[1, True], Literal[1]) + self.assertEqual(Literal[1], Literal[1]) + self.assertEqual(Literal[1, 2], Literal[2, 1]) + self.assertEqual(Literal[1, 2, 3], Literal[1, 2, 3, 3]) + + def test_hash(self): + self.assertEqual(hash(Literal[1]), hash(Literal[1])) + self.assertEqual(hash(Literal[1, 2]), hash(Literal[2, 1])) + self.assertEqual(hash(Literal[1, 2, 3]), hash(Literal[1, 2, 3, 3])) + + def test_args(self): + self.assertEqual(Literal[1, 2, 3].__args__, (1, 2, 3)) + self.assertEqual(Literal[1, 2, 3, 3].__args__, (1, 2, 3)) + self.assertEqual(Literal[1, Literal[2], Literal[3, 4]].__args__, (1, 2, 3, 4)) + # Mutable arguments will not be deduplicated + self.assertEqual(Literal[[], []].__args__, ([], [])) + + def test_flatten(self): + l1 = Literal[Literal[1], Literal[2], Literal[3]] + l2 = Literal[Literal[1, 2], 3] + l3 = Literal[Literal[1, 2, 3]] + for lit in l1, l2, l3: + self.assertEqual(lit, Literal[1, 2, 3]) + self.assertEqual(lit.__args__, (1, 2, 3)) + + def test_caching_of_Literal_respects_type(self): + self.assertIs(type(Literal[1].__args__[0]), int) + self.assertIs(type(Literal[True].__args__[0]), bool) + class MethodHolder: @classmethod @@ -3566,6 +3601,8 @@ def test_typing_extensions_defers_when_possible(self): 'get_type_hints', 'is_typeddict', } + if sys.version_info < (3, 9, 1): + exclude |= {"Literal"} if sys.version_info < (3, 10): exclude |= {'get_args', 'get_origin'} if sys.version_info < (3, 11): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 16a8fdd3..cd02b3f2 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -261,21 +261,70 @@ def IntVar(name): return typing.TypeVar(name) -# 3.8+: -if hasattr(typing, 'Literal'): +# Various Literal bugs were fixed in 3.9.1, but not backported earlier than that +if sys.version_info >= (3, 9, 1): Literal = typing.Literal -# 3.7: else: + def _flatten_literal_params(parameters): + """An internal helper for Literal creation: flatten Literals among parameters""" + params = [] + for p in parameters: + if isinstance(p, _LiteralGenericAlias): + params.extend(p.__args__) + else: + params.append(p) + return tuple(params) + + def _value_and_type_iter(params): + for p in params: + yield p, type(p) + + class _LiteralGenericAlias(typing._GenericAlias, _root=True): + def __eq__(self, other): + if not isinstance(other, _LiteralGenericAlias): + return NotImplemented + these_args_deduped = set(_value_and_type_iter(self.__args__)) + other_args_deduped = set(_value_and_type_iter(other.__args__)) + return these_args_deduped == other_args_deduped + + def __hash__(self): + return hash(frozenset(_value_and_type_iter(self.__args__))) + class _LiteralForm(typing._SpecialForm, _root=True): + def __init__(self, doc: str): + self._name = 'Literal' + self._doc = self.__doc__ = doc def __repr__(self): return 'typing_extensions.' + self._name def __getitem__(self, parameters): - return typing._GenericAlias(self, parameters) + if not isinstance(parameters, tuple): + parameters = (parameters,) + + parameters = _flatten_literal_params(parameters) - Literal = _LiteralForm('Literal', - doc="""A type that can be used to indicate to type checkers + val_type_pairs = list(_value_and_type_iter(parameters)) + try: + deduped_pairs = set(val_type_pairs) + except TypeError: + # unhashable parameters + pass + else: + # similar logic to typing._deduplicate on Python 3.9+ + if len(deduped_pairs) < len(val_type_pairs): + new_parameters = [] + for pair in val_type_pairs: + if pair in deduped_pairs: + new_parameters.append(pair[0]) + deduped_pairs.remove(pair) + assert not deduped_pairs, deduped_pairs + parameters = tuple(new_parameters) + + return _LiteralGenericAlias(self, parameters) + + Literal = _LiteralForm(doc="""\ + A type that can be used to indicate to type checkers that the corresponding value has a value literally equivalent to the provided parameter. For example: From fb37b2ee0ab56baf2dfd5df27aec0430435ba17f Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 19 Apr 2023 21:56:13 -0600 Subject: [PATCH 21/46] Reimplement `Literal` on Python <=3.10.0 (#148) --- CHANGELOG.md | 4 ++++ README.md | 7 ++++--- src/test_typing_extensions.py | 10 ++++++---- src/typing_extensions.py | 4 ++-- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04f1a3f6..3a3f62b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ objects will now raise a `TypeError` if one of the `Literal` objects being compared has a mutable parameter. (Using mutable parameters with `Literal` is not supported by PEP 586 or by any major static type checkers.) +- `Literal` is now reimplemented on all Python versions <= 3.10.0. The + `typing_extensions` version does not suffer from the bug that was fixed in + https://github.com/python/cpython/pull/29334. (The CPython bugfix was + backported to CPython 3.10.1 and 3.9.8, but no earlier.) - Backport [CPython PR 26067](https://github.com/python/cpython/pull/26067) (originally by Yurii Karabas), ensuring that `isinstance()` calls on protocols raise `TypeError` when the protocol is not decorated with diff --git a/README.md b/README.md index 46678afa..59d5d3d0 100644 --- a/README.md +++ b/README.md @@ -143,9 +143,10 @@ Certain objects were changed after they were added to `typing`, and - `TypeVar` gains two additional parameters, `default=` and `infer_variance=`, in the draft PEPs [695](https://peps.python.org/pep-0695/) and [696](https://peps.python.org/pep-0696/), which are being considered for inclusion in Python 3.12. -- `Literal` does not flatten or deduplicate parameters on Python <3.9.1. The - `typing_extensions` version flattens and deduplicates parameters on all - Python versions. +- `Literal` does not flatten or deduplicate parameters on Python <3.9.1, and a + caching bug was fixed in 3.10.1/3.9.8. The `typing_extensions` version + flattens and deduplicates parameters on all Python versions, and the caching + bug is also fixed on all versions. There are a few types whose interface was modified between different versions of typing. For example, `typing.Sequence` was modified to diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 0dbc3301..9c89cfc1 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -615,8 +615,8 @@ def test_literals_inside_other_types(self): List[Literal[("foo", "bar", "baz")]] def test_repr(self): - # we backport various bugfixes that were added in 3.9.1 - if sys.version_info >= (3, 9, 1): + # we backport various bugfixes that were added in 3.10.1 and earlier + if sys.version_info >= (3, 10, 1): mod_name = 'typing' else: mod_name = 'typing_extensions' @@ -662,6 +662,8 @@ def test_equal(self): self.assertNotEqual(Literal[True], Literal[1]) self.assertNotEqual(Literal[1], Literal[2]) self.assertNotEqual(Literal[1, True], Literal[1]) + self.assertNotEqual(Literal[1, True], Literal[1, 1]) + self.assertNotEqual(Literal[1, 2], Literal[True, 2]) self.assertEqual(Literal[1], Literal[1]) self.assertEqual(Literal[1, 2], Literal[2, 1]) self.assertEqual(Literal[1, 2, 3], Literal[1, 2, 3, 3]) @@ -3601,10 +3603,10 @@ def test_typing_extensions_defers_when_possible(self): 'get_type_hints', 'is_typeddict', } - if sys.version_info < (3, 9, 1): - exclude |= {"Literal"} if sys.version_info < (3, 10): exclude |= {'get_args', 'get_origin'} + if sys.version_info < (3, 10, 1): + exclude |= {"Literal"} if sys.version_info < (3, 11): exclude |= {'final', 'NamedTuple', 'Any'} if sys.version_info < (3, 12): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index cd02b3f2..b6b6bd49 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -261,8 +261,8 @@ def IntVar(name): return typing.TypeVar(name) -# Various Literal bugs were fixed in 3.9.1, but not backported earlier than that -if sys.version_info >= (3, 9, 1): +# A Literal bug was fixed in 3.11.0, 3.10.1 and 3.9.8 +if sys.version_info >= (3, 10, 1): Literal = typing.Literal else: def _flatten_literal_params(parameters): From 41a828801a60e8e20bd2d9b172b2e3fbd9604d87 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 23 Apr 2023 15:57:36 -0600 Subject: [PATCH 22/46] Make tests pass on conda builds (#151) --- src/test_typing_extensions.py | 178 +++++++++++++++++++++++++++++----- 1 file changed, 153 insertions(+), 25 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 9c89cfc1..7fa310ce 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -8,19 +8,23 @@ import collections.abc import copy from functools import lru_cache +import importlib import inspect import pickle import subprocess +import tempfile import types +from pathlib import Path from unittest import TestCase, main, skipUnless, skipIf from unittest.mock import patch -from test import ann_module, ann_module2, ann_module3 import typing from typing import TypeVar, Optional, Union, AnyStr from typing import T, KT, VT # Not in __all__. from typing import Tuple, List, Dict, Iterable, Iterator, Callable from typing import Generic from typing import no_type_check +import warnings + import typing_extensions from typing_extensions import NoReturn, Any, ClassVar, Final, IntVar, Literal, Type, NewType, TypedDict, Self from typing_extensions import TypeAlias, ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs, TypeGuard @@ -32,7 +36,6 @@ from typing_extensions import NamedTuple from typing_extensions import override, deprecated, Buffer from _typed_dict_test_helper import Foo, FooGeneric -import warnings # Flags used to mark tests that only apply after a specific # version of the typing module. @@ -47,6 +50,112 @@ # versions, but not all HAS_FORWARD_MODULE = "module" in inspect.signature(typing._type_check).parameters +ANN_MODULE_SOURCE = '''\ +from typing import Optional +from functools import wraps + +__annotations__[1] = 2 + +class C: + + x = 5; y: Optional['C'] = None + +from typing import Tuple +x: int = 5; y: str = x; f: Tuple[int, int] + +class M(type): + + __annotations__['123'] = 123 + o: type = object + +(pars): bool = True + +class D(C): + j: str = 'hi'; k: str= 'bye' + +from types import new_class +h_class = new_class('H', (C,)) +j_class = new_class('J') + +class F(): + z: int = 5 + def __init__(self, x): + pass + +class Y(F): + def __init__(self): + super(F, self).__init__(123) + +class Meta(type): + def __new__(meta, name, bases, namespace): + return super().__new__(meta, name, bases, namespace) + +class S(metaclass = Meta): + x: str = 'something' + y: str = 'something else' + +def foo(x: int = 10): + def bar(y: List[str]): + x: str = 'yes' + bar() + +def dec(func): + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper +''' + +ANN_MODULE_2_SOURCE = '''\ +from typing import no_type_check, ClassVar + +i: int = 1 +j: int +x: float = i/10 + +def f(): + class C: ... + return C() + +f().new_attr: object = object() + +class C: + def __init__(self, x: int) -> None: + self.x = x + +c = C(5) +c.new_attr: int = 10 + +__annotations__ = {} + + +@no_type_check +class NTC: + def meth(self, param: complex) -> None: + ... + +class CV: + var: ClassVar['CV'] + +CV.var = CV() +''' + +ANN_MODULE_3_SOURCE = '''\ +def f_bad_ann(): + __annotations__[1] = 2 + +class C_OK: + def __init__(self, x: int) -> None: + self.x: no_such_name = x # This one is OK as proposed by Guido + +class D_bad_ann: + def __init__(self, x: int) -> None: + sfel.y: int = 0 + +def g_bad_ann(): + no_such_name.attr: int = 0 +''' + class BaseTestCase(TestCase): def assertIsSubclass(self, cls, class_or_tuple, msg=None): @@ -384,8 +493,13 @@ def test_repr(self): else: mod_name = 'typing_extensions' self.assertEqual(repr(Any), f"{mod_name}.Any") - if sys.version_info < (3, 11): # skip for now on 3.11+ see python/cpython#95987 - self.assertEqual(repr(self.SubclassesAny), "") + + @skipIf(sys.version_info[:3] == (3, 11, 0), "A bug was fixed in 3.11.1") + def test_repr_on_Any_subclass(self): + self.assertEqual( + repr(self.SubclassesAny), + f"" + ) def test_instantiation(self): with self.assertRaises(TypeError): @@ -944,28 +1058,42 @@ class AnnotatedMovie(TypedDict): class GetTypeHintTests(BaseTestCase): + @classmethod + def setUpClass(cls): + with tempfile.TemporaryDirectory() as tempdir: + sys.path.append(tempdir) + Path(tempdir, "ann_module.py").write_text(ANN_MODULE_SOURCE) + Path(tempdir, "ann_module2.py").write_text(ANN_MODULE_2_SOURCE) + Path(tempdir, "ann_module3.py").write_text(ANN_MODULE_3_SOURCE) + cls.ann_module = importlib.import_module("ann_module") + cls.ann_module2 = importlib.import_module("ann_module2") + cls.ann_module3 = importlib.import_module("ann_module3") + sys.path.pop() + + @classmethod + def tearDownClass(cls): + for modname in "ann_module", "ann_module2", "ann_module3": + delattr(cls, modname) + del sys.modules[modname] + def test_get_type_hints_modules(self): ann_module_type_hints = {1: 2, 'f': Tuple[int, int], 'x': int, 'y': str} - if (TYPING_3_11_0 - or (TYPING_3_10_0 and sys.version_info.releaselevel in {'candidate', 'final'})): - # More tests were added in 3.10rc1. - ann_module_type_hints['u'] = int | float - self.assertEqual(gth(ann_module), ann_module_type_hints) - self.assertEqual(gth(ann_module2), {}) - self.assertEqual(gth(ann_module3), {}) + self.assertEqual(gth(self.ann_module), ann_module_type_hints) + self.assertEqual(gth(self.ann_module2), {}) + self.assertEqual(gth(self.ann_module3), {}) def test_get_type_hints_classes(self): - self.assertEqual(gth(ann_module.C, ann_module.__dict__), - {'y': Optional[ann_module.C]}) - self.assertIsInstance(gth(ann_module.j_class), dict) - self.assertEqual(gth(ann_module.M), {'123': 123, 'o': type}) - self.assertEqual(gth(ann_module.D), - {'j': str, 'k': str, 'y': Optional[ann_module.C]}) - self.assertEqual(gth(ann_module.Y), {'z': int}) - self.assertEqual(gth(ann_module.h_class), - {'y': Optional[ann_module.C]}) - self.assertEqual(gth(ann_module.S), {'x': str, 'y': str}) - self.assertEqual(gth(ann_module.foo), {'x': int}) + self.assertEqual(gth(self.ann_module.C, self.ann_module.__dict__), + {'y': Optional[self.ann_module.C]}) + self.assertIsInstance(gth(self.ann_module.j_class), dict) + self.assertEqual(gth(self.ann_module.M), {'123': 123, 'o': type}) + self.assertEqual(gth(self.ann_module.D), + {'j': str, 'k': str, 'y': Optional[self.ann_module.C]}) + self.assertEqual(gth(self.ann_module.Y), {'z': int}) + self.assertEqual(gth(self.ann_module.h_class), + {'y': Optional[self.ann_module.C]}) + self.assertEqual(gth(self.ann_module.S), {'x': str, 'y': str}) + self.assertEqual(gth(self.ann_module.foo), {'x': int}) self.assertEqual(gth(NoneAndForward, globals()), {'parent': NoneAndForward, 'meaning': type(None)}) @@ -976,7 +1104,7 @@ class Inn: def __init__(self, x: 'not a type'): ... self.assertTrue(NoTpCheck.__no_type_check__) self.assertTrue(NoTpCheck.Inn.__init__.__no_type_check__) - self.assertEqual(gth(ann_module2.NTC.meth), {}) + self.assertEqual(gth(self.ann_module2.NTC.meth), {}) class ABase(Generic[T]): def meth(x: int): ... @no_type_check @@ -984,8 +1112,8 @@ class Der(ABase): ... self.assertEqual(gth(ABase.meth), {'x': int}) def test_get_type_hints_ClassVar(self): - self.assertEqual(gth(ann_module2.CV, ann_module2.__dict__), - {'var': ClassVar[ann_module2.CV]}) + self.assertEqual(gth(self.ann_module2.CV, self.ann_module2.__dict__), + {'var': ClassVar[self.ann_module2.CV]}) self.assertEqual(gth(B, globals()), {'y': int, 'x': ClassVar[Optional[B]], 'b': int}) self.assertEqual(gth(CSub, globals()), From 468a8415bd5fe284dd6440a1d4d3585c60d65e23 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Mon, 24 Apr 2023 14:40:17 +0300 Subject: [PATCH 23/46] Improve CI definitons (#153) --- .github/workflows/ci.yml | 6 ++++++ .github/workflows/package.yml | 11 +++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e813cb72..2b2fca67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,12 +2,18 @@ name: Test and lint on: push: + branches: + - main pull_request: workflow_dispatch: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + jobs: tests: name: Run tests diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 4e270719..ad2deee1 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -2,11 +2,18 @@ name: Test packaging on: push: + branches: + - main pull_request: + workflow_dispatch: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + jobs: wheel: name: Test wheel install @@ -23,7 +30,7 @@ jobs: - name: Install pypa/build run: | # Be wary of running `pip install` here, since it becomes easy for us to - # accidentally pick up typing_extensions as installed by a dependency + # accidentally pick up typing_extensions as installed by a dependency python -m pip install --upgrade build python -m pip list @@ -53,7 +60,7 @@ jobs: - name: Install pypa/build run: | # Be wary of running `pip install` here, since it becomes easy for us to - # accidentally pick up typing_extensions as installed by a dependency + # accidentally pick up typing_extensions as installed by a dependency python -m pip install --upgrade build python -m pip list From 0b8de38d8c9feda22550ea0d71c6cc0cffc89f42 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Mon, 24 Apr 2023 22:29:04 +0300 Subject: [PATCH 24/46] Backport tests of `Union` + `Literal` from CPython (#152) --- src/test_typing_extensions.py | 79 +++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 7fa310ce..62e5a4bc 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -714,6 +714,13 @@ def test_basics(self): Literal["x", "y", "z"] Literal[None] + def test_enum(self): + import enum + class My(enum.Enum): + A = 'A' + + self.assertEqual(Literal[My.A].__args__, (My.A,)) + def test_illegal_parameters_do_not_raise_runtime_errors(self): # Type checkers should reject these types, but we do not # raise errors at runtime to maintain maximum flexibility @@ -794,6 +801,64 @@ def test_args(self): # Mutable arguments will not be deduplicated self.assertEqual(Literal[[], []].__args__, ([], [])) + def test_union_of_literals(self): + self.assertEqual(Union[Literal[1], Literal[2]].__args__, + (Literal[1], Literal[2])) + self.assertEqual(Union[Literal[1], Literal[1]], + Literal[1]) + + self.assertEqual(Union[Literal[False], Literal[0]].__args__, + (Literal[False], Literal[0])) + self.assertEqual(Union[Literal[True], Literal[1]].__args__, + (Literal[True], Literal[1])) + + import enum + class Ints(enum.IntEnum): + A = 0 + B = 1 + + self.assertEqual(Union[Literal[Ints.A], Literal[Ints.B]].__args__, + (Literal[Ints.A], Literal[Ints.B])) + + self.assertEqual(Union[Literal[Ints.A], Literal[Ints.A]], + Literal[Ints.A]) + self.assertEqual(Union[Literal[Ints.B], Literal[Ints.B]], + Literal[Ints.B]) + + self.assertEqual(Union[Literal[0], Literal[Ints.A], Literal[False]].__args__, + (Literal[0], Literal[Ints.A], Literal[False])) + self.assertEqual(Union[Literal[1], Literal[Ints.B], Literal[True]].__args__, + (Literal[1], Literal[Ints.B], Literal[True])) + + @skipUnless(TYPING_3_10_0, "Python 3.10+ required") + def test_or_type_operator_with_Literal(self): + self.assertEqual((Literal[1] | Literal[2]).__args__, + (Literal[1], Literal[2])) + + self.assertEqual((Literal[0] | Literal[False]).__args__, + (Literal[0], Literal[False])) + self.assertEqual((Literal[1] | Literal[True]).__args__, + (Literal[1], Literal[True])) + + self.assertEqual(Literal[1] | Literal[1], Literal[1]) + self.assertEqual(Literal['a'] | Literal['a'], Literal['a']) + + import enum + class Ints(enum.IntEnum): + A = 0 + B = 1 + + self.assertEqual(Literal[Ints.A] | Literal[Ints.A], Literal[Ints.A]) + self.assertEqual(Literal[Ints.B] | Literal[Ints.B], Literal[Ints.B]) + + self.assertEqual((Literal[Ints.B] | Literal[Ints.A]).__args__, + (Literal[Ints.B], Literal[Ints.A])) + + self.assertEqual((Literal[0] | Literal[Ints.A]).__args__, + (Literal[0], Literal[Ints.A])) + self.assertEqual((Literal[1] | Literal[Ints.B]).__args__, + (Literal[1], Literal[Ints.B])) + def test_flatten(self): l1 = Literal[Literal[1], Literal[2], Literal[3]] l2 = Literal[Literal[1, 2], 3] @@ -802,6 +867,20 @@ def test_flatten(self): self.assertEqual(lit, Literal[1, 2, 3]) self.assertEqual(lit.__args__, (1, 2, 3)) + def test_does_not_flatten_enum(self): + import enum + class Ints(enum.IntEnum): + A = 1 + B = 2 + + literal = Literal[ + Literal[Ints.A], + Literal[Ints.B], + Literal[1], + Literal[2], + ] + self.assertEqual(literal.__args__, (Ints.A, Ints.B, 1, 2)) + def test_caching_of_Literal_respects_type(self): self.assertIs(type(Literal[1].__args__[0]), int) self.assertIs(type(Literal[True].__args__[0]), bool) From 1f98818ddc3a7d6f724fe2323f102835f52f0eb0 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Mon, 24 Apr 2023 13:33:52 -0600 Subject: [PATCH 25/46] Add __orig_bases__ to all TypedDict and NamedTuple (#150) Co-authored-by: AlexWaygood --- CHANGELOG.md | 6 +++ src/test_typing_extensions.py | 88 ++++++++++++++++++++++++++++------- src/typing_extensions.py | 31 +++++++++--- 3 files changed, 101 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a3f62b0..a95f31ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,12 @@ Patch by Alex Waygood. - Speedup `isinstance(3, typing_extensions.SupportsIndex)` by >10x on Python <3.12. Patch by Alex Waygood. +- Add `__orig_bases__` to non-generic TypedDicts, call-based TypedDicts, and + call-based NamedTuples. Other TypedDicts and NamedTuples already had the attribute. + Patch by Adrian Garcia Badaracco. +- Constructing a call-based `TypedDict` using keyword arguments for the fields + now causes a `DeprecationWarning` to be emitted. This matches the behaviour + of `typing.TypedDict` on 3.11 and 3.12. # Release 4.5.0 (February 14, 2023) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 62e5a4bc..77171100 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -172,13 +172,6 @@ def assertNotIsSubclass(self, cls, class_or_tuple, msg=None): message += f' : {msg}' raise self.failureException(message) - @contextlib.contextmanager - def assertWarnsIf(self, condition: bool, expected_warning: Type[Warning]): - with contextlib.ExitStack() as stack: - if condition: - stack.enter_context(self.assertWarns(expected_warning)) - yield - class Employee: pass @@ -2467,7 +2460,7 @@ def test_basics_iterable_syntax(self): self.assertEqual(Emp.__total__, True) def test_basics_keywords_syntax(self): - with self.assertWarnsIf(sys.version_info >= (3, 11), DeprecationWarning): + with self.assertWarns(DeprecationWarning): Emp = TypedDict('Emp', name=str, id=int) self.assertIsSubclass(Emp, dict) self.assertIsSubclass(Emp, typing.MutableMapping) @@ -2483,7 +2476,7 @@ def test_basics_keywords_syntax(self): self.assertEqual(Emp.__total__, True) def test_typeddict_special_keyword_names(self): - with self.assertWarnsIf(sys.version_info >= (3, 11), DeprecationWarning): + with self.assertWarns(DeprecationWarning): TD = TypedDict("TD", cls=type, self=object, typename=str, _typename=int, fields=list, _fields=dict) self.assertEqual(TD.__name__, 'TD') @@ -2519,7 +2512,7 @@ def test_typeddict_create_errors(self): def test_typeddict_errors(self): Emp = TypedDict('Emp', {'name': str, 'id': int}) - if hasattr(typing, "Required"): + if sys.version_info >= (3, 12): self.assertEqual(TypedDict.__module__, 'typing') else: self.assertEqual(TypedDict.__module__, 'typing_extensions') @@ -2532,7 +2525,7 @@ def test_typeddict_errors(self): issubclass(dict, Emp) if not TYPING_3_11_0: - with self.assertRaises(TypeError): + with self.assertRaises(TypeError), self.assertWarns(DeprecationWarning): TypedDict('Hi', x=1) with self.assertRaises(TypeError): TypedDict('Hi', [('x', int), ('y', 1)]) @@ -3036,6 +3029,49 @@ def test_get_type_hints_typeddict(self): 'year': NotRequired[Annotated[int, 2000]], } + def test_orig_bases(self): + T = TypeVar('T') + + class Parent(TypedDict): + pass + + class Child(Parent): + pass + + class OtherChild(Parent): + pass + + class MixedChild(Child, OtherChild, Parent): + pass + + class GenericParent(TypedDict, Generic[T]): + pass + + class GenericChild(GenericParent[int]): + pass + + class OtherGenericChild(GenericParent[str]): + pass + + class MixedGenericChild(GenericChild, OtherGenericChild, GenericParent[float]): + pass + + class MultipleGenericBases(GenericParent[int], GenericParent[float]): + pass + + CallTypedDict = TypedDict('CallTypedDict', {}) + + self.assertEqual(Parent.__orig_bases__, (TypedDict,)) + self.assertEqual(Child.__orig_bases__, (Parent,)) + self.assertEqual(OtherChild.__orig_bases__, (Parent,)) + self.assertEqual(MixedChild.__orig_bases__, (Child, OtherChild, Parent,)) + self.assertEqual(GenericParent.__orig_bases__, (TypedDict, Generic[T])) + self.assertEqual(GenericChild.__orig_bases__, (GenericParent[int],)) + self.assertEqual(OtherGenericChild.__orig_bases__, (GenericParent[str],)) + self.assertEqual(MixedGenericChild.__orig_bases__, (GenericChild, OtherGenericChild, GenericParent[float])) + self.assertEqual(MultipleGenericBases.__orig_bases__, (GenericParent[int], GenericParent[float])) + self.assertEqual(CallTypedDict.__orig_bases__, (TypedDict,)) + class TypeAliasTests(BaseTestCase): def test_canonical_usage_with_variable_annotation(self): @@ -3802,22 +3838,23 @@ def test_typing_extensions_defers_when_possible(self): 'overload', 'ParamSpec', 'Text', - 'TypedDict', 'TypeVar', 'TypeVarTuple', 'TYPE_CHECKING', 'Final', 'get_type_hints', - 'is_typeddict', } if sys.version_info < (3, 10): exclude |= {'get_args', 'get_origin'} if sys.version_info < (3, 10, 1): exclude |= {"Literal"} if sys.version_info < (3, 11): - exclude |= {'final', 'NamedTuple', 'Any'} + exclude |= {'final', 'Any'} if sys.version_info < (3, 12): - exclude |= {'Protocol', 'runtime_checkable', 'SupportsIndex'} + exclude |= { + 'Protocol', 'runtime_checkable', 'SupportsIndex', 'TypedDict', + 'is_typeddict', 'NamedTuple', + } for item in typing_extensions.__all__: if item not in exclude and hasattr(typing, item): self.assertIs( @@ -3863,7 +3900,6 @@ def __add__(self, other): return 0 -@skipIf(TYPING_3_11_0, "These invariants should all be tested upstream on 3.11+") class NamedTupleTests(BaseTestCase): class NestedEmployee(NamedTuple): name: str @@ -4003,7 +4039,9 @@ class Y(Generic[T], NamedTuple): self.assertIs(type(a), G) self.assertEqual(a.x, 3) - with self.assertRaisesRegex(TypeError, 'Too many parameters'): + things = "arguments" if sys.version_info >= (3, 11) else "parameters" + + with self.assertRaisesRegex(TypeError, f'Too many {things}'): G[int, str] @skipUnless(TYPING_3_9_0, "tuple.__class_getitem__ was added in 3.9") @@ -4134,6 +4172,22 @@ def test_same_as_typing_NamedTuple_38_minus(self): self.NestedEmployee._field_types ) + def test_orig_bases(self): + T = TypeVar('T') + + class SimpleNamedTuple(NamedTuple): + pass + + class GenericNamedTuple(NamedTuple, Generic[T]): + pass + + self.assertEqual(SimpleNamedTuple.__orig_bases__, (NamedTuple,)) + self.assertEqual(GenericNamedTuple.__orig_bases__, (NamedTuple, Generic[T])) + + CallNamedTuple = NamedTuple('CallNamedTuple', []) + + self.assertEqual(CallNamedTuple.__orig_bases__, (NamedTuple,)) + class TypeVarLikeDefaultsTests(BaseTestCase): def test_typevar(self): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index b6b6bd49..411ccd42 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -749,7 +749,7 @@ def __index__(self) -> int: pass -if hasattr(typing, "Required"): +if sys.version_info >= (3, 12): # The standard library TypedDict in Python 3.8 does not store runtime information # about which (if any) keys are optional. See https://bugs.python.org/issue38834 # The standard library TypedDict in Python 3.9.0/1 does not honour the "total" @@ -757,6 +757,8 @@ def __index__(self) -> int: # The standard library TypedDict below Python 3.11 does not store runtime # information about optional and required keys when using Required or NotRequired. # Generic TypedDicts are also impossible using typing.TypedDict on Python <3.11. + # Aaaand on 3.12 we add __orig_bases__ to TypedDict + # to enable better runtime introspection. TypedDict = typing.TypedDict _TypedDictMeta = typing._TypedDictMeta is_typeddict = typing.is_typeddict @@ -786,7 +788,6 @@ def _typeddict_new(*args, total=True, **kwargs): typename, args = args[0], args[1:] # allow the "_typename" keyword be passed elif '_typename' in kwargs: typename = kwargs.pop('_typename') - import warnings warnings.warn("Passing '_typename' as keyword argument is deprecated", DeprecationWarning, stacklevel=2) else: @@ -801,7 +802,6 @@ def _typeddict_new(*args, total=True, **kwargs): 'were given') elif '_fields' in kwargs and len(kwargs) == 1: fields = kwargs.pop('_fields') - import warnings warnings.warn("Passing '_fields' as keyword argument is deprecated", DeprecationWarning, stacklevel=2) else: @@ -813,6 +813,15 @@ def _typeddict_new(*args, total=True, **kwargs): raise TypeError("TypedDict takes either a dict or keyword arguments," " but not both") + if kwargs: + warnings.warn( + "The kwargs-based syntax for TypedDict definitions is deprecated, " + "may be removed in a future version, and may not be " + "understood by third-party type checkers.", + DeprecationWarning, + stacklevel=2, + ) + ns = {'__annotations__': dict(fields)} module = _caller() if module is not None: @@ -844,9 +853,14 @@ def __new__(cls, name, bases, ns, total=True): # Instead, monkey-patch __bases__ onto the class after it's been created. tp_dict = super().__new__(cls, name, (dict,), ns) - if any(issubclass(base, typing.Generic) for base in bases): + is_generic = any(issubclass(base, typing.Generic) for base in bases) + + if is_generic: tp_dict.__bases__ = (typing.Generic, dict) _maybe_adjust_parameters(tp_dict) + else: + # generic TypedDicts get __orig_bases__ from Generic + tp_dict.__orig_bases__ = bases or (TypedDict,) annotations = {} own_annotations = ns.get('__annotations__', {}) @@ -2313,10 +2327,11 @@ def wrapper(*args, **kwargs): typing._check_generic = _check_generic -# Backport typing.NamedTuple as it exists in Python 3.11. +# Backport typing.NamedTuple as it exists in Python 3.12. # In 3.11, the ability to define generic `NamedTuple`s was supported. # This was explicitly disallowed in 3.9-3.10, and only half-worked in <=3.8. -if sys.version_info >= (3, 11): +# On 3.12, we added __orig_bases__ to call-based NamedTuples +if sys.version_info >= (3, 12): NamedTuple = typing.NamedTuple else: def _make_nmtuple(name, types, module, defaults=()): @@ -2378,7 +2393,9 @@ def NamedTuple(__typename, __fields=None, **kwargs): elif kwargs: raise TypeError("Either list of fields or keywords" " can be provided to NamedTuple, not both") - return _make_nmtuple(__typename, __fields, module=_caller()) + nt = _make_nmtuple(__typename, __fields, module=_caller()) + nt.__orig_bases__ = (NamedTuple,) + return nt NamedTuple.__doc__ = typing.NamedTuple.__doc__ _NamedTuple = type.__new__(_NamedTupleMeta, 'NamedTuple', (), {}) From 0273a6e37dc5abba5e3ff8c039249894d321642e Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 24 Apr 2023 16:24:36 -0600 Subject: [PATCH 26/46] README: Updates re NamedTuple and TypedDict (#155) Followup to #150 --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 59d5d3d0..d98027e9 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,10 @@ Certain objects were changed after they were added to `typing`, and about which (if any) keys are non-required in Python 3.8, and does not honor the `total` keyword with old-style `TypedDict()` in Python 3.9.0 and 3.9.1. `TypedDict` also does not support multiple inheritance - with `typing.Generic` on Python <3.11. + with `typing.Generic` on Python <3.11, and `TypedDict` classes do not + consistently have the `__orig_bases__` attribute on Python <3.12. The + `typing_extensions` backport provides all of these features and bugfixes on + all Python versions. - `get_origin` and `get_args` lack support for `Annotated` in Python 3.8 and lack support for `ParamSpecArgs` and `ParamSpecKwargs` in 3.9. @@ -137,7 +140,9 @@ Certain objects were changed after they were added to `typing`, and `typing_extensions.get_overloads()`, you must use `@typing_extensions.overload`. - `NamedTuple` was changed in Python 3.11 to allow for multiple inheritance - with `typing.Generic`. + with `typing.Generic`. Call-based `NamedTuple`s were changed in Python 3.12 + so that they have an `__orig_bases__` attribute, the same as class-based + `NamedTuple`s. - Since Python 3.11, it has been possible to inherit from `Any` at runtime. `typing_extensions.Any` also provides this capability. - `TypeVar` gains two additional parameters, `default=` and `infer_variance=`, From 48b685557e7f79a8dd5847c5f482390786f9c028 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 26 Apr 2023 11:03:45 -0600 Subject: [PATCH 27/46] Add a backport of `types.get_original_bases` (#154) Co-authored-by: Jelle Zijlstra --- CHANGELOG.md | 10 ++++ README.md | 8 ++++ src/test_typing_extensions.py | 87 ++++++++++++++++++++++++++++++++++- src/typing_extensions.py | 37 +++++++++++++++ 4 files changed, 141 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a95f31ed..b8d8b628 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,16 @@ - Add `__orig_bases__` to non-generic TypedDicts, call-based TypedDicts, and call-based NamedTuples. Other TypedDicts and NamedTuples already had the attribute. Patch by Adrian Garcia Badaracco. +- Add `typing_extensions.get_original_bases`, a backport of + [`types.get_original_bases`](https://docs.python.org/3.12/library/types.html#types.get_original_bases), + introduced in Python 3.12 (CPython PR + https://github.com/python/cpython/pull/101827, originally by James + Hilton-Balfe). Patch by Alex Waygood. + + This function should always produce correct results when called on classes + constructed using features from `typing_extensions`. However, it may + produce incorrect results when called on some `NamedTuple` or `TypedDict` + classes that use `typing.{NamedTuple,TypedDict}` on Python <=3.11. - Constructing a call-based `TypedDict` using keyword arguments for the fields now causes a `DeprecationWarning` to be emitted. This matches the behaviour of `typing.TypedDict` on 3.11 and 3.12. diff --git a/README.md b/README.md index d98027e9..d23dbd4e 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,14 @@ This module currently contains the following: - `override` (equivalent to `typing.override`; see [PEP 698](https://peps.python.org/pep-0698/)) - `Buffer` (equivalent to `collections.abc.Buffer`; see [PEP 688](https://peps.python.org/pep-0688/)) + - `get_original_bases` (equivalent to + [`types.get_original_bases`](https://docs.python.org/3.12/library/types.html#types.get_original_bases) + on 3.12+). + + This function should always produce correct results when called on classes + constructed using features from `typing_extensions`. However, it may + produce incorrect results when called on some `NamedTuple` or `TypedDict` + classes that use `typing.{NamedTuple,TypedDict}` on Python <=3.11. - In `typing` since Python 3.11 diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 77171100..7f3c0ef2 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -31,7 +31,7 @@ from typing_extensions import Awaitable, AsyncIterator, AsyncContextManager, Required, NotRequired from typing_extensions import Protocol, runtime, runtime_checkable, Annotated, final, is_typeddict from typing_extensions import TypeVarTuple, Unpack, dataclass_transform, reveal_type, Never, assert_never, LiteralString -from typing_extensions import assert_type, get_type_hints, get_origin, get_args +from typing_extensions import assert_type, get_type_hints, get_origin, get_args, get_original_bases from typing_extensions import clear_overloads, get_overloads, overload from typing_extensions import NamedTuple from typing_extensions import override, deprecated, Buffer @@ -4286,5 +4286,90 @@ def __buffer__(self, flags: int) -> memoryview: self.assertIsSubclass(MySubclassedBuffer, Buffer) +class GetOriginalBasesTests(BaseTestCase): + def test_basics(self): + T = TypeVar('T') + class A: pass + class B(Generic[T]): pass + class C(B[int]): pass + class D(B[str], float): pass + self.assertEqual(get_original_bases(A), (object,)) + self.assertEqual(get_original_bases(B), (Generic[T],)) + self.assertEqual(get_original_bases(C), (B[int],)) + self.assertEqual(get_original_bases(int), (object,)) + self.assertEqual(get_original_bases(D), (B[str], float)) + + with self.assertRaisesRegex(TypeError, "Expected an instance of type"): + get_original_bases(object()) + + @skipUnless(TYPING_3_9_0, "PEP 585 is yet to be") + def test_builtin_generics(self): + class E(list[T]): pass + class F(list[int]): pass + + self.assertEqual(get_original_bases(E), (list[T],)) + self.assertEqual(get_original_bases(F), (list[int],)) + + def test_namedtuples(self): + # On 3.12, this should work well with typing.NamedTuple and typing_extensions.NamedTuple + # On lower versions, it will only work fully with typing_extensions.NamedTuple + if sys.version_info >= (3, 12): + namedtuple_classes = (typing.NamedTuple, typing_extensions.NamedTuple) + else: + namedtuple_classes = (typing_extensions.NamedTuple,) + + for NamedTuple in namedtuple_classes: # noqa: F402 + with self.subTest(cls=NamedTuple): + class ClassBasedNamedTuple(NamedTuple): + x: int + + class GenericNamedTuple(NamedTuple, Generic[T]): + x: T + + CallBasedNamedTuple = NamedTuple("CallBasedNamedTuple", [("x", int)]) + + self.assertIs( + get_original_bases(ClassBasedNamedTuple)[0], NamedTuple + ) + self.assertEqual( + get_original_bases(GenericNamedTuple), + (NamedTuple, Generic[T]) + ) + self.assertIs( + get_original_bases(CallBasedNamedTuple)[0], NamedTuple + ) + + def test_typeddicts(self): + # On 3.12, this should work well with typing.TypedDict and typing_extensions.TypedDict + # On lower versions, it will only work fully with typing_extensions.TypedDict + if sys.version_info >= (3, 12): + typeddict_classes = (typing.TypedDict, typing_extensions.TypedDict) + else: + typeddict_classes = (typing_extensions.TypedDict,) + + for TypedDict in typeddict_classes: # noqa: F402 + with self.subTest(cls=TypedDict): + class ClassBasedTypedDict(TypedDict): + x: int + + class GenericTypedDict(TypedDict, Generic[T]): + x: T + + CallBasedTypedDict = TypedDict("CallBasedTypedDict", {"x": int}) + + self.assertIs( + get_original_bases(ClassBasedTypedDict)[0], + TypedDict + ) + self.assertEqual( + get_original_bases(GenericTypedDict), + (TypedDict, Generic[T]) + ) + self.assertIs( + get_original_bases(CallBasedTypedDict)[0], + TypedDict + ) + + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 411ccd42..2ee36eb3 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -59,6 +59,7 @@ 'final', 'get_args', 'get_origin', + 'get_original_bases', 'get_type_hints', 'IntVar', 'is_typeddict', @@ -2440,3 +2441,39 @@ class Buffer(abc.ABC): Buffer.register(memoryview) Buffer.register(bytearray) Buffer.register(bytes) + + +# Backport of types.get_original_bases, available on 3.12+ in CPython +if hasattr(_types, "get_original_bases"): + get_original_bases = _types.get_original_bases +else: + def get_original_bases(__cls): + """Return the class's "original" bases prior to modification by `__mro_entries__`. + + Examples:: + + from typing import TypeVar, Generic + from typing_extensions import NamedTuple, TypedDict + + T = TypeVar("T") + class Foo(Generic[T]): ... + class Bar(Foo[int], float): ... + class Baz(list[str]): ... + Eggs = NamedTuple("Eggs", [("a", int), ("b", str)]) + Spam = TypedDict("Spam", {"a": int, "b": str}) + + assert get_original_bases(Bar) == (Foo[int], float) + assert get_original_bases(Baz) == (list[str],) + assert get_original_bases(Eggs) == (NamedTuple,) + assert get_original_bases(Spam) == (TypedDict,) + assert get_original_bases(int) == (object,) + """ + try: + return __cls.__orig_bases__ + except AttributeError: + try: + return __cls.__bases__ + except AttributeError: + raise TypeError( + f'Expected an instance of type, not {type(__cls).__name__!r}' + ) from None From d3719ac92b9f3045688e4b3d08f37c369a7b436c Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 26 Apr 2023 11:17:30 -0600 Subject: [PATCH 28/46] Add faster versions of various runtime-checkable protocols (#146) --- CHANGELOG.md | 5 +++ src/test_typing_extensions.py | 5 ++- src/typing_extensions.py | 69 +++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8d8b628..f117f390 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,11 @@ Patch by Alex Waygood. - Speedup `isinstance(3, typing_extensions.SupportsIndex)` by >10x on Python <3.12. Patch by Alex Waygood. +- Add `typing_extensions` versions of `SupportsInt`, `SupportsFloat`, + `SupportsComplex`, `SupportsBytes`, `SupportsAbs` and `SupportsRound`. These + have the same semantics as the versions from the `typing` module, but + `isinstance()` checks against the `typing_extensions` versions are >10x faster + at runtime on Python <3.12. Patch by Alex Waygood. - Add `__orig_bases__` to non-generic TypedDicts, call-based TypedDicts, and call-based NamedTuples. Other TypedDicts and NamedTuples already had the attribute. Patch by Adrian Garcia Badaracco. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 7f3c0ef2..4a5d3a14 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -3852,8 +3852,9 @@ def test_typing_extensions_defers_when_possible(self): exclude |= {'final', 'Any'} if sys.version_info < (3, 12): exclude |= { - 'Protocol', 'runtime_checkable', 'SupportsIndex', 'TypedDict', - 'is_typeddict', 'NamedTuple', + 'Protocol', 'runtime_checkable', 'SupportsAbs', 'SupportsBytes', + 'SupportsComplex', 'SupportsFloat', 'SupportsIndex', 'SupportsInt', + 'SupportsRound', 'TypedDict', 'is_typeddict', 'NamedTuple', } for item in typing_extensions.__all__: if item not in exclude and hasattr(typing, item): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 2ee36eb3..cce31f84 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -46,7 +46,13 @@ 'TypedDict', # Structural checks, a.k.a. protocols. + 'SupportsAbs', + 'SupportsBytes', + 'SupportsComplex', + 'SupportsFloat', 'SupportsIndex', + 'SupportsInt', + 'SupportsRound', # One-off things. 'Annotated', @@ -739,8 +745,49 @@ def runtime_checkable(cls): # Our version of runtime-checkable protocols is faster on Python 3.7-3.11 if sys.version_info >= (3, 12): + SupportsInt = typing.SupportsInt + SupportsFloat = typing.SupportsFloat + SupportsComplex = typing.SupportsComplex SupportsIndex = typing.SupportsIndex + SupportsAbs = typing.SupportsAbs + SupportsRound = typing.SupportsRound else: + @runtime_checkable + class SupportsInt(Protocol): + """An ABC with one abstract method __int__.""" + __slots__ = () + + @abc.abstractmethod + def __int__(self) -> int: + pass + + @runtime_checkable + class SupportsFloat(Protocol): + """An ABC with one abstract method __float__.""" + __slots__ = () + + @abc.abstractmethod + def __float__(self) -> float: + pass + + @runtime_checkable + class SupportsComplex(Protocol): + """An ABC with one abstract method __complex__.""" + __slots__ = () + + @abc.abstractmethod + def __complex__(self) -> complex: + pass + + @runtime_checkable + class SupportsBytes(Protocol): + """An ABC with one abstract method __bytes__.""" + __slots__ = () + + @abc.abstractmethod + def __bytes__(self) -> bytes: + pass + @runtime_checkable class SupportsIndex(Protocol): __slots__ = () @@ -749,6 +796,28 @@ class SupportsIndex(Protocol): def __index__(self) -> int: pass + @runtime_checkable + class SupportsAbs(Protocol[T_co]): + """ + An ABC with one abstract method __abs__ that is covariant in its return type. + """ + __slots__ = () + + @abc.abstractmethod + def __abs__(self) -> T_co: + pass + + @runtime_checkable + class SupportsRound(Protocol[T_co]): + """ + An ABC with one abstract method __round__ that is covariant in its return type. + """ + __slots__ = () + + @abc.abstractmethod + def __round__(self, ndigits: int = 0) -> T_co: + pass + if sys.version_info >= (3, 12): # The standard library TypedDict in Python 3.8 does not store runtime information From 962936a9e4583d4d424ff85745e9aeb6cf4460ee Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 26 Apr 2023 14:34:45 -0600 Subject: [PATCH 29/46] State in README that we backport the 3.12 version of `Protocol`, `runtime_checkable` and various runtime-checkable protocols (#143) --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index d23dbd4e..b7e6a7a6 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ This module currently contains the following: - `TypedDict` (see [PEP 589](https://peps.python.org/pep-0589/)) - `get_origin` (`typing_extensions` provides this function only in Python 3.7+) - `get_args` (`typing_extensions` provides this function only in Python 3.7+) + - `SupportsIndex` - In `typing` since Python 3.7 @@ -126,6 +127,17 @@ This module currently contains the following: - `NamedTuple` (supports multiple inheritance with `Generic` since Python 3.11) - `TypeVar` (see PEPs [695](https://peps.python.org/pep-0695/) and [696](https://peps.python.org/pep-0696/)) +The following runtime-checkable protocols have always been present in `typing`, +but the `isinstance()` checks against the `typing_extensions` versions are much +faster on Python <3.12: + + - `SupportsInt` + - `SupportsFloat` + - `SupportsComplex` + - `SupportsBytes` + - `SupportsAbs` + - `SupportsRound` + # Other Notes and Limitations Certain objects were changed after they were added to `typing`, and @@ -156,6 +168,16 @@ Certain objects were changed after they were added to `typing`, and - `TypeVar` gains two additional parameters, `default=` and `infer_variance=`, in the draft PEPs [695](https://peps.python.org/pep-0695/) and [696](https://peps.python.org/pep-0696/), which are being considered for inclusion in Python 3.12. +- `Protocol` was added in Python 3.8, but several bugfixes have been made in + subsequent releases, as well as significant performance improvements to + runtime-checkable protocols in Python 3.12. `typing_extensions` backports the + 3.12+ version to Python 3.7+. +- `SupportsInt`, `SupportsFloat`, `SupportsComplex`, `SupportsBytes`, + `SupportsAbs` and `SupportsRound` have always been present in the `typing` + module. Meanwhile, `SupportsIndex` was added in Python 3.8. However, + `isinstance()` checks against all these protocols were sped up significantly + on Python 3.12. `typing_extensions` backports the faster versions to Python + 3.7+. - `Literal` does not flatten or deduplicate parameters on Python <3.9.1, and a caching bug was fixed in 3.10.1/3.9.8. The `typing_extensions` version flattens and deduplicates parameters on all Python versions, and the caching From 7e6a4c00175d382c33e14ac1724ef314cbd1e2ba Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 8 May 2023 14:57:04 +0100 Subject: [PATCH 30/46] Backport `NewType` as it exists on py310+ (#157) --- CHANGELOG.md | 3 ++ README.md | 3 ++ src/test_typing_extensions.py | 82 ++++++++++++++++++++++++++++++++--- src/typing_extensions.py | 66 +++++++++++++++++++++++++++- 4 files changed, 146 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f117f390..bc5abe74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,9 @@ - Constructing a call-based `TypedDict` using keyword arguments for the fields now causes a `DeprecationWarning` to be emitted. This matches the behaviour of `typing.TypedDict` on 3.11 and 3.12. +- Backport the implementation of `NewType` from 3.10 (where it is implemented + as a class rather than a function). This allows user-defined `NewType`s to be + pickled. Patch by Alex Waygood. # Release 4.5.0 (February 14, 2023) diff --git a/README.md b/README.md index b7e6a7a6..11434d18 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,9 @@ Certain objects were changed after they were added to `typing`, and caching bug was fixed in 3.10.1/3.9.8. The `typing_extensions` version flattens and deduplicates parameters on all Python versions, and the caching bug is also fixed on all versions. +- `NewType` has been in the `typing` module since Python 3.5.2, but + user-defined `NewType`s are only pickleable on Python 3.10+. + `typing_extensions.NewType` backports this feature to all Python versions. There are a few types whose interface was modified between different versions of typing. For example, `typing.Sequence` was modified to diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 4a5d3a14..469c31b6 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -11,6 +11,7 @@ import importlib import inspect import pickle +import re import subprocess import tempfile import types @@ -1539,23 +1540,90 @@ def foo(a: A) -> Optional[BaseException]: class NewTypeTests(BaseTestCase): + @classmethod + def setUpClass(cls): + global UserId + UserId = NewType('UserId', int) + cls.UserName = NewType(cls.__qualname__ + '.UserName', str) + + @classmethod + def tearDownClass(cls): + global UserId + del UserId + del cls.UserName def test_basic(self): - UserId = NewType('UserId', int) - UserName = NewType('UserName', str) self.assertIsInstance(UserId(5), int) - self.assertIsInstance(UserName('Joe'), str) + self.assertIsInstance(self.UserName('Joe'), str) self.assertEqual(UserId(5) + 1, 6) def test_errors(self): - UserId = NewType('UserId', int) - UserName = NewType('UserName', str) with self.assertRaises(TypeError): issubclass(UserId, int) with self.assertRaises(TypeError): - class D(UserName): + class D(UserId): pass + @skipUnless(TYPING_3_10_0, "PEP 604 has yet to be") + def test_or(self): + for cls in (int, self.UserName): + with self.subTest(cls=cls): + self.assertEqual(UserId | cls, Union[UserId, cls]) + self.assertEqual(cls | UserId, Union[cls, UserId]) + + self.assertEqual(get_args(UserId | cls), (UserId, cls)) + self.assertEqual(get_args(cls | UserId), (cls, UserId)) + + def test_special_attrs(self): + self.assertEqual(UserId.__name__, 'UserId') + self.assertEqual(UserId.__qualname__, 'UserId') + self.assertEqual(UserId.__module__, __name__) + self.assertEqual(UserId.__supertype__, int) + + UserName = self.UserName + self.assertEqual(UserName.__name__, 'UserName') + self.assertEqual(UserName.__qualname__, + self.__class__.__qualname__ + '.UserName') + self.assertEqual(UserName.__module__, __name__) + self.assertEqual(UserName.__supertype__, str) + + def test_repr(self): + self.assertEqual(repr(UserId), f'{__name__}.UserId') + self.assertEqual(repr(self.UserName), + f'{__name__}.{self.__class__.__qualname__}.UserName') + + def test_pickle(self): + UserAge = NewType('UserAge', float) + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(proto=proto): + pickled = pickle.dumps(UserId, proto) + loaded = pickle.loads(pickled) + self.assertIs(loaded, UserId) + + pickled = pickle.dumps(self.UserName, proto) + loaded = pickle.loads(pickled) + self.assertIs(loaded, self.UserName) + + with self.assertRaises(pickle.PicklingError): + pickle.dumps(UserAge, proto) + + def test_missing__name__(self): + code = ("import typing_extensions\n" + "NT = typing_extensions.NewType('NT', int)\n" + ) + exec(code, {}) + + def test_error_message_when_subclassing(self): + with self.assertRaisesRegex( + TypeError, + re.escape( + "Cannot subclass an instance of NewType. Perhaps you were looking for: " + "`ProUserId = NewType('ProUserId', UserId)`" + ) + ): + class ProUserId(UserId): + ... + class Coordinate(Protocol): x: int @@ -3849,7 +3917,7 @@ def test_typing_extensions_defers_when_possible(self): if sys.version_info < (3, 10, 1): exclude |= {"Literal"} if sys.version_info < (3, 11): - exclude |= {'final', 'Any'} + exclude |= {'final', 'Any', 'NewType'} if sys.version_info < (3, 12): exclude |= { 'Protocol', 'runtime_checkable', 'SupportsAbs', 'SupportsBytes', diff --git a/src/typing_extensions.py b/src/typing_extensions.py index cce31f84..dd12cfb8 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -440,7 +440,6 @@ def clear_overloads(): Counter = typing.Counter ChainMap = typing.ChainMap AsyncGenerator = typing.AsyncGenerator -NewType = typing.NewType Text = typing.Text TYPE_CHECKING = typing.TYPE_CHECKING @@ -2546,3 +2545,68 @@ class Baz(list[str]): ... raise TypeError( f'Expected an instance of type, not {type(__cls).__name__!r}' ) from None + + +# NewType is a class on Python 3.10+, making it pickleable +# The error message for subclassing instances of NewType was improved on 3.11+ +if sys.version_info >= (3, 11): + NewType = typing.NewType +else: + class NewType: + """NewType creates simple unique types with almost zero + runtime overhead. NewType(name, tp) is considered a subtype of tp + by static type checkers. At runtime, NewType(name, tp) returns + a dummy callable that simply returns its argument. Usage:: + UserId = NewType('UserId', int) + def name_by_id(user_id: UserId) -> str: + ... + UserId('user') # Fails type check + name_by_id(42) # Fails type check + name_by_id(UserId(42)) # OK + num = UserId(5) + 1 # type: int + """ + + def __call__(self, obj): + return obj + + def __init__(self, name, tp): + self.__qualname__ = name + if '.' in name: + name = name.rpartition('.')[-1] + self.__name__ = name + self.__supertype__ = tp + def_mod = _caller() + if def_mod != 'typing_extensions': + self.__module__ = def_mod + + def __mro_entries__(self, bases): + # We defined __mro_entries__ to get a better error message + # if a user attempts to subclass a NewType instance. bpo-46170 + supercls_name = self.__name__ + + class Dummy: + def __init_subclass__(cls): + subcls_name = cls.__name__ + raise TypeError( + f"Cannot subclass an instance of NewType. " + f"Perhaps you were looking for: " + f"`{subcls_name} = NewType({subcls_name!r}, {supercls_name})`" + ) + + return (Dummy,) + + def __repr__(self): + return f'{self.__module__}.{self.__qualname__}' + + def __reduce__(self): + return self.__qualname__ + + if sys.version_info >= (3, 10): + # PEP 604 methods + # It doesn't make sense to have these methods on Python <3.10 + + def __or__(self, other): + return typing.Union[self, other] + + def __ror__(self, other): + return typing.Union[other, self] From dfe4889858cea9503797229a9dea1524f8f1e767 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 19 May 2023 23:09:24 +0100 Subject: [PATCH 31/46] Backport some recent `Protocol` fixes from 3.12 (#161) --- src/test_typing_extensions.py | 119 +++++++++++++++++++++++++++++++++- src/typing_extensions.py | 61 ++++++++++------- 2 files changed, 152 insertions(+), 28 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 469c31b6..d12c5de6 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1907,6 +1907,63 @@ class D(PNonCall): ... with self.assertRaises(TypeError): issubclass(D, PNonCall) + def test_no_weird_caching_with_issubclass_after_isinstance(self): + @runtime_checkable + class Spam(Protocol): + x: int + + class Eggs: + def __init__(self) -> None: + self.x = 42 + + self.assertIsInstance(Eggs(), Spam) + + # gh-104555: If we didn't override ABCMeta.__subclasscheck__ in _ProtocolMeta, + # TypeError wouldn't be raised here, + # as the cached result of the isinstance() check immediately above + # would mean the issubclass() call would short-circuit + # before we got to the "raise TypeError" line + with self.assertRaises(TypeError): + issubclass(Eggs, Spam) + + def test_no_weird_caching_with_issubclass_after_isinstance_2(self): + @runtime_checkable + class Spam(Protocol): + x: int + + class Eggs: ... + + self.assertNotIsInstance(Eggs(), Spam) + + # gh-104555: If we didn't override ABCMeta.__subclasscheck__ in _ProtocolMeta, + # TypeError wouldn't be raised here, + # as the cached result of the isinstance() check immediately above + # would mean the issubclass() call would short-circuit + # before we got to the "raise TypeError" line + with self.assertRaises(TypeError): + issubclass(Eggs, Spam) + + def test_no_weird_caching_with_issubclass_after_isinstance_3(self): + @runtime_checkable + class Spam(Protocol): + x: int + + class Eggs: + def __getattr__(self, attr): + if attr == "x": + return 42 + raise AttributeError(attr) + + self.assertNotIsInstance(Eggs(), Spam) + + # gh-104555: If we didn't override ABCMeta.__subclasscheck__ in _ProtocolMeta, + # TypeError wouldn't be raised here, + # as the cached result of the isinstance() check immediately above + # would mean the issubclass() call would short-circuit + # before we got to the "raise TypeError" line + with self.assertRaises(TypeError): + issubclass(Eggs, Spam) + def test_protocols_isinstance(self): T = TypeVar('T') @runtime_checkable @@ -2235,10 +2292,10 @@ def meth(self): pass class NonP(P): x = 1 class NonPR(PR): pass - class C: + class C(metaclass=abc.ABCMeta): x = 1 - class D: - def meth(self): pass + class D(metaclass=abc.ABCMeta): # noqa: B024 + def meth(self): pass # noqa: B027 self.assertNotIsInstance(C(), NonP) self.assertNotIsInstance(D(), NonPR) self.assertNotIsSubclass(C, NonP) @@ -2246,6 +2303,20 @@ def meth(self): pass self.assertIsInstance(NonPR(), PR) self.assertIsSubclass(NonPR, PR) + self.assertNotIn("__protocol_attrs__", vars(NonP)) + self.assertNotIn("__protocol_attrs__", vars(NonPR)) + self.assertNotIn("__callable_proto_members_only__", vars(NonP)) + self.assertNotIn("__callable_proto_members_only__", vars(NonPR)) + + acceptable_extra_attrs = { + '_is_protocol', '_is_runtime_protocol', '__parameters__', + '__init__', '__annotations__', '__subclasshook__', + } + self.assertLessEqual(vars(NonP).keys(), vars(C).keys() | acceptable_extra_attrs) + self.assertLessEqual( + vars(NonPR).keys(), vars(D).keys() | acceptable_extra_attrs + ) + def test_custom_subclasshook(self): class P(Protocol): x = 1 @@ -2325,6 +2396,48 @@ def bar(self, x: str) -> str: with self.assertRaises(TypeError): PR[int, ClassVar] + if sys.version_info >= (3, 12): + exec(textwrap.dedent( + """ + def test_pep695_generic_protocol_callable_members(self): + @runtime_checkable + class Foo[T](Protocol): + def meth(self, x: T) -> None: ... + + class Bar[T]: + def meth(self, x: T) -> None: ... + + self.assertIsInstance(Bar(), Foo) + self.assertIsSubclass(Bar, Foo) + + @runtime_checkable + class SupportsTrunc[T](Protocol): + def __trunc__(self) -> T: ... + + self.assertIsInstance(0.0, SupportsTrunc) + self.assertIsSubclass(float, SupportsTrunc) + + def test_no_weird_caching_with_issubclass_after_isinstance_pep695(self): + @runtime_checkable + class Spam[T](Protocol): + x: T + + class Eggs[T]: + def __init__(self, x: T) -> None: + self.x = x + + self.assertIsInstance(Eggs(42), Spam) + + # gh-104555: If we didn't override ABCMeta.__subclasscheck__ in _ProtocolMeta, + # TypeError wouldn't be raised here, + # as the cached result of the isinstance() check immediately above + # would mean the issubclass() call would short-circuit + # before we got to the "raise TypeError" line + with self.assertRaises(TypeError): + issubclass(Eggs, Spam) + """ + )) + def test_init_called(self): T = TypeVar('T') class P(Protocol[T]): pass diff --git a/src/typing_extensions.py b/src/typing_extensions.py index dd12cfb8..b74bf135 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -470,6 +470,9 @@ def clear_overloads(): if sys.version_info >= (3, 9): _EXCLUDED_ATTRS.add("__class_getitem__") +if sys.version_info >= (3, 12): + _EXCLUDED_ATTRS.add("__type_params__") + _EXCLUDED_ATTRS = frozenset(_EXCLUDED_ATTRS) @@ -550,23 +553,37 @@ def _no_init(self, *args, **kwargs): raise TypeError('Protocols cannot be instantiated') class _ProtocolMeta(abc.ABCMeta): - # This metaclass is a bit unfortunate and exists only because of the lack - # of __instancehook__. + # This metaclass is somewhat unfortunate, + # but is necessary for several reasons... def __init__(cls, *args, **kwargs): super().__init__(*args, **kwargs) - cls.__protocol_attrs__ = _get_protocol_attrs(cls) - # PEP 544 prohibits using issubclass() - # with protocols that have non-method members. - cls.__callable_proto_members_only__ = all( - callable(getattr(cls, attr, None)) for attr in cls.__protocol_attrs__ - ) + if getattr(cls, "_is_protocol", False): + cls.__protocol_attrs__ = _get_protocol_attrs(cls) + # PEP 544 prohibits using issubclass() + # with protocols that have non-method members. + cls.__callable_proto_members_only__ = all( + callable(getattr(cls, attr, None)) for attr in cls.__protocol_attrs__ + ) + + def __subclasscheck__(cls, other): + if ( + getattr(cls, '_is_protocol', False) + and not cls.__callable_proto_members_only__ + and not _allow_reckless_class_checks(depth=3) + ): + raise TypeError( + "Protocols with non-method members don't support issubclass()" + ) + return super().__subclasscheck__(other) def __instancecheck__(cls, instance): # We need this method for situations where attributes are # assigned in __init__. - is_protocol_cls = getattr(cls, "_is_protocol", False) + if not getattr(cls, "_is_protocol", False): + # i.e., it's a concrete subclass of a protocol + return super().__instancecheck__(instance) + if ( - is_protocol_cls and not getattr(cls, '_is_runtime_protocol', False) and not _allow_reckless_class_checks(depth=2) ): @@ -576,16 +593,15 @@ def __instancecheck__(cls, instance): if super().__instancecheck__(instance): return True - if is_protocol_cls: - for attr in cls.__protocol_attrs__: - try: - val = inspect.getattr_static(instance, attr) - except AttributeError: - break - if val is None and callable(getattr(cls, attr, None)): - break - else: - return True + for attr in cls.__protocol_attrs__: + try: + val = inspect.getattr_static(instance, attr) + except AttributeError: + break + if val is None and callable(getattr(cls, attr, None)): + break + else: + return True return False @@ -679,11 +695,6 @@ def _proto_hook(other): return NotImplemented raise TypeError("Instance and class checks can only be used with" " @runtime protocols") - if not cls.__callable_proto_members_only__: - if _allow_reckless_class_checks(): - return NotImplemented - raise TypeError("Protocols with non-method members" - " don't support issubclass()") if not isinstance(other, type): # Same error as for issubclass(1, int) raise TypeError('issubclass() arg 1 must be a class') From 40dbc09dbb01982f3ef58db05f38ba9d61cd19e9 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 19 May 2023 23:09:48 +0100 Subject: [PATCH 32/46] Backport changes to the repr of `typing.Unpack` that were made in Python 3.12 (#163) --- CHANGELOG.md | 3 ++ README.md | 2 + src/test_typing_extensions.py | 12 +++--- src/typing_extensions.py | 71 +++++++++++++++++++++++------------ 4 files changed, 59 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc5abe74..4e24f90b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,9 @@ - Backport the implementation of `NewType` from 3.10 (where it is implemented as a class rather than a function). This allows user-defined `NewType`s to be pickled. Patch by Alex Waygood. +- Backport changes to the repr of `typing.Unpack` that were made in order to + implement [PEP 692](https://peps.python.org/pep-0692/) (backport of + https://github.com/python/cpython/pull/104048). Patch by Alex Waygood. # Release 4.5.0 (February 14, 2023) diff --git a/README.md b/README.md index 11434d18..0b888e90 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,8 @@ Certain objects were changed after they were added to `typing`, and - `NewType` has been in the `typing` module since Python 3.5.2, but user-defined `NewType`s are only pickleable on Python 3.10+. `typing_extensions.NewType` backports this feature to all Python versions. +- `Unpack` was added in Python 3.11, but the repr was changed in Python 3.12; + `typing_extensions.Unpack` has the newer repr on all versions. There are a few types whose interface was modified between different versions of typing. For example, `typing.Sequence` was modified to diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index d12c5de6..48a5e1ab 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -3644,10 +3644,7 @@ def test_basic_plain(self): def test_repr(self): Ts = TypeVarTuple('Ts') - if TYPING_3_11_0: - self.assertEqual(repr(Unpack[Ts]), '*Ts') - else: - self.assertEqual(repr(Unpack[Ts]), 'typing_extensions.Unpack[Ts]') + self.assertEqual(repr(Unpack[Ts]), f'{Unpack.__module__}.Unpack[Ts]') def test_cannot_subclass_vars(self): with self.assertRaises(TypeError): @@ -3769,7 +3766,10 @@ def test_args_and_parameters(self): Ts = TypeVarTuple('Ts') t = Tuple[tuple(Ts)] - self.assertEqual(t.__args__, (Unpack[Ts],)) + if sys.version_info >= (3, 11): + self.assertEqual(t.__args__, (typing.Unpack[Ts],)) + else: + self.assertEqual(t.__args__, (Unpack[Ts],)) self.assertEqual(t.__parameters__, (Ts,)) def test_pickle(self): @@ -4035,7 +4035,7 @@ def test_typing_extensions_defers_when_possible(self): exclude |= { 'Protocol', 'runtime_checkable', 'SupportsAbs', 'SupportsBytes', 'SupportsComplex', 'SupportsFloat', 'SupportsIndex', 'SupportsInt', - 'SupportsRound', 'TypedDict', 'is_typeddict', 'NamedTuple', + 'SupportsRound', 'TypedDict', 'is_typeddict', 'NamedTuple', 'Unpack', } for item in typing_extensions.__all__: if item not in exclude and hasattr(typing, item): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index b74bf135..43c6da5f 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1971,7 +1971,49 @@ class Movie(TypedDict): """) -if hasattr(typing, "Unpack"): # 3.11+ +_UNPACK_DOC = """\ +Type unpack operator. + +The type unpack operator takes the child types from some container type, +such as `tuple[int, str]` or a `TypeVarTuple`, and 'pulls them out'. For +example: + + # For some generic class `Foo`: + Foo[Unpack[tuple[int, str]]] # Equivalent to Foo[int, str] + + Ts = TypeVarTuple('Ts') + # Specifies that `Bar` is generic in an arbitrary number of types. + # (Think of `Ts` as a tuple of an arbitrary number of individual + # `TypeVar`s, which the `Unpack` is 'pulling out' directly into the + # `Generic[]`.) + class Bar(Generic[Unpack[Ts]]): ... + Bar[int] # Valid + Bar[int, str] # Also valid + +From Python 3.11, this can also be done using the `*` operator: + + Foo[*tuple[int, str]] + class Bar(Generic[*Ts]): ... + +The operator can also be used along with a `TypedDict` to annotate +`**kwargs` in a function signature. For instance: + + class Movie(TypedDict): + name: str + year: int + + # This function expects two keyword arguments - *name* of type `str` and + # *year* of type `int`. + def foo(**kwargs: Unpack[Movie]): ... + +Note that there is only some runtime checking of this operator. Not +everything the runtime allows may be accepted by static type checkers. + +For more information, see PEP 646 and PEP 692. +""" + + +if sys.version_info >= (3, 12): # PEP 692 changed the repr of Unpack[] Unpack = typing.Unpack def _is_unpack(obj): @@ -1979,6 +2021,10 @@ def _is_unpack(obj): elif sys.version_info[:2] >= (3, 9): class _UnpackSpecialForm(typing._SpecialForm, _root=True): + def __init__(self, getitem): + super().__init__(getitem) + self.__doc__ = _UNPACK_DOC + def __repr__(self): return 'typing_extensions.' + self._name @@ -1987,16 +2033,6 @@ class _UnpackAlias(typing._GenericAlias, _root=True): @_UnpackSpecialForm def Unpack(self, parameters): - """A special typing construct to unpack a variadic type. For example: - - Shape = TypeVarTuple('Shape') - Batch = NewType('Batch', int) - - def add_batch_axis( - x: Array[Unpack[Shape]] - ) -> Array[Batch, Unpack[Shape]]: ... - - """ item = typing._type_check(parameters, f'{self._name} accepts only a single type.') return _UnpackAlias(self, (item,)) @@ -2016,18 +2052,7 @@ def __getitem__(self, parameters): f'{self._name} accepts only a single type.') return _UnpackAlias(self, (item,)) - Unpack = _UnpackForm( - 'Unpack', - doc="""A special typing construct to unpack a variadic type. For example: - - Shape = TypeVarTuple('Shape') - Batch = NewType('Batch', int) - - def add_batch_axis( - x: Array[Unpack[Shape]] - ) -> Array[Batch, Unpack[Shape]]: ... - - """) + Unpack = _UnpackForm('Unpack', doc=_UNPACK_DOC) def _is_unpack(obj): return isinstance(obj, _UnpackAlias) From 09c1ed49509d3c0a963e5bfed50e4b941954810c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 20 May 2023 16:24:05 -0700 Subject: [PATCH 33/46] Add TypeAliasType (#160) Co-authored-by: Sebastian Rittau Co-authored-by: Alex Waygood --- CHANGELOG.md | 4 +- README.md | 1 + src/test_typing_extensions.py | 81 ++++++++++++++++++++++++++++- src/typing_extensions.py | 98 +++++++++++++++++++++++++++++++++++ 4 files changed, 181 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e24f90b..c6d0233a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,7 @@ using the new release, and vice versa. Most users are unlikely to be affected by this change. Patch by Alex Waygood. - Backport the ability to define `__init__` methods on Protocol classes, a - change made in Python 3.11 (originally implemented in + change made in Python 3.11 (originally implemented in https://github.com/python/cpython/pull/31628 by Adrian Garcia Badaracco). Patch by Alex Waygood. - Speedup `isinstance(3, typing_extensions.SupportsIndex)` by >10x on Python @@ -73,6 +73,8 @@ - Backport the implementation of `NewType` from 3.10 (where it is implemented as a class rather than a function). This allows user-defined `NewType`s to be pickled. Patch by Alex Waygood. +- Add `typing_extensions.TypeAliasType`, a backport of `typing.TypeAliasType` + from PEP 695. Patch by Jelle Zijlstra. - Backport changes to the repr of `typing.Unpack` that were made in order to implement [PEP 692](https://peps.python.org/pep-0692/) (backport of https://github.com/python/cpython/pull/104048). Patch by Alex Waygood. diff --git a/README.md b/README.md index 0b888e90..aad814ef 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ This module currently contains the following: - In the standard library since Python 3.12 - `override` (equivalent to `typing.override`; see [PEP 698](https://peps.python.org/pep-0698/)) + - `TypeAliasType` (equivalent to `typing.TypeAliasType`; see [PEP 695](https://peps.python.org/pep-0695/)) - `Buffer` (equivalent to `collections.abc.Buffer`; see [PEP 688](https://peps.python.org/pep-0688/)) - `get_original_bases` (equivalent to [`types.get_original_bases`](https://docs.python.org/3.12/library/types.html#types.get_original_bases) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 48a5e1ab..dadf6e3c 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -21,7 +21,7 @@ import typing from typing import TypeVar, Optional, Union, AnyStr from typing import T, KT, VT # Not in __all__. -from typing import Tuple, List, Dict, Iterable, Iterator, Callable +from typing import Tuple, List, Set, Dict, Iterable, Iterator, Callable from typing import Generic from typing import no_type_check import warnings @@ -35,7 +35,7 @@ from typing_extensions import assert_type, get_type_hints, get_origin, get_args, get_original_bases from typing_extensions import clear_overloads, get_overloads, overload from typing_extensions import NamedTuple -from typing_extensions import override, deprecated, Buffer +from typing_extensions import override, deprecated, Buffer, TypeAliasType from _typed_dict_test_helper import Foo, FooGeneric # Flags used to mark tests that only apply after a specific @@ -4553,5 +4553,82 @@ class GenericTypedDict(TypedDict, Generic[T]): ) +class TypeAliasTypeTests(BaseTestCase): + def test_attributes(self): + Simple = TypeAliasType("Simple", int) + self.assertEqual(Simple.__name__, "Simple") + self.assertIs(Simple.__value__, int) + self.assertEqual(Simple.__type_params__, ()) + self.assertEqual(Simple.__parameters__, ()) + + T = TypeVar("T") + ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,)) + self.assertEqual(ListOrSetT.__name__, "ListOrSetT") + self.assertEqual(ListOrSetT.__value__, Union[List[T], Set[T]]) + self.assertEqual(ListOrSetT.__type_params__, (T,)) + self.assertEqual(ListOrSetT.__parameters__, (T,)) + + Ts = TypeVarTuple("Ts") + Variadic = TypeAliasType("Variadic", Tuple[int, Unpack[Ts]], type_params=(Ts,)) + self.assertEqual(Variadic.__name__, "Variadic") + self.assertEqual(Variadic.__value__, Tuple[int, Unpack[Ts]]) + self.assertEqual(Variadic.__type_params__, (Ts,)) + self.assertEqual(Variadic.__parameters__, tuple(iter(Ts))) + + def test_immutable(self): + Simple = TypeAliasType("Simple", int) + with self.assertRaisesRegex(AttributeError, "Can't set attribute"): + Simple.__name__ = "NewName" + with self.assertRaisesRegex(AttributeError, "Can't set attribute"): + Simple.__value__ = str + with self.assertRaisesRegex(AttributeError, "Can't set attribute"): + Simple.__type_params__ = (T,) + with self.assertRaisesRegex(AttributeError, "Can't set attribute"): + Simple.__parameters__ = (T,) + with self.assertRaisesRegex(AttributeError, "Can't set attribute"): + Simple.some_attribute = "not allowed" + with self.assertRaisesRegex(AttributeError, "Can't delete attribute"): + del Simple.__name__ + with self.assertRaisesRegex(AttributeError, "Can't delete attribute"): + del Simple.nonexistent_attribute + + def test_or(self): + Alias = TypeAliasType("Alias", int) + if sys.version_info >= (3, 10): + self.assertEqual(Alias | "Ref", Union[Alias, typing.ForwardRef("Ref")]) + else: + with self.assertRaises(TypeError): + Alias | "Ref" + + def test_getitem(self): + ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,)) + subscripted = ListOrSetT[int] + self.assertEqual(get_args(subscripted), (int,)) + self.assertIs(get_origin(subscripted), ListOrSetT) + with self.assertRaises(TypeError): + subscripted[str] + + still_generic = ListOrSetT[Iterable[T]] + self.assertEqual(get_args(still_generic), (Iterable[T],)) + self.assertIs(get_origin(still_generic), ListOrSetT) + fully_subscripted = still_generic[float] + self.assertEqual(get_args(fully_subscripted), (Iterable[float],)) + self.assertIs(get_origin(fully_subscripted), ListOrSetT) + + def test_pickle(self): + global Alias + Alias = TypeAliasType("Alias", int) + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(proto=proto): + pickled = pickle.dumps(Alias, proto) + unpickled = pickle.loads(pickled) + self.assertIs(unpickled, Alias) + + def test_no_instance_subclassing(self): + with self.assertRaises(TypeError): + class MyAlias(TypeAliasType): + pass + + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 43c6da5f..82e4ba76 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -79,6 +79,7 @@ 'runtime_checkable', 'Text', 'TypeAlias', + 'TypeAliasType', 'TypeGuard', 'TYPE_CHECKING', 'Never', @@ -2646,3 +2647,100 @@ def __or__(self, other): def __ror__(self, other): return typing.Union[other, self] + + +if hasattr(typing, "TypeAliasType"): + TypeAliasType = typing.TypeAliasType +else: + class TypeAliasType: + """Create named, parameterized type aliases. + + This provides a backport of the new `type` statement in Python 3.12: + + type ListOrSet[T] = list[T] | set[T] + + is equivalent to: + + T = TypeVar("T") + ListOrSet = TypeAliasType("ListOrSet", list[T] | set[T], type_params=(T,)) + + The name ListOrSet can then be used as an alias for the type it refers to. + + The type_params argument should contain all the type parameters used + in the value of the type alias. If the alias is not generic, this + argument is omitted. + + Static type checkers should only support type aliases declared using + TypeAliasType that follow these rules: + + - The first argument (the name) must be a string literal. + - The TypeAliasType instance must be immediately assigned to a variable + of the same name. (For example, 'X = TypeAliasType("Y", int)' is invalid, + as is 'X, Y = TypeAliasType("X", int), TypeAliasType("Y", int)'). + + """ + + def __init__(self, name: str, value, *, type_params=()): + if not isinstance(name, str): + raise TypeError("TypeAliasType name must be a string") + self.__value__ = value + self.__type_params__ = type_params + + parameters = [] + for type_param in type_params: + if isinstance(type_param, TypeVarTuple): + parameters.extend(type_param) + else: + parameters.append(type_param) + self.__parameters__ = tuple(parameters) + def_mod = _caller() + if def_mod != 'typing_extensions': + self.__module__ = def_mod + # Setting this attribute closes the TypeAliasType from further modification + self.__name__ = name + + def __setattr__(self, __name: str, __value: object) -> None: + if hasattr(self, "__name__"): + raise AttributeError( + f"Can't set attribute {__name!r} on an instance of TypeAliasType" + ) + super().__setattr__(__name, __value) + + def __delattr__(self, __name: str) -> None: + raise AttributeError( + f"Can't delete attribute {__name!r} on an instance of TypeAliasType" + ) + + def __repr__(self) -> str: + return self.__name__ + + def __getitem__(self, parameters): + if not isinstance(parameters, tuple): + parameters = (parameters,) + parameters = [ + typing._type_check( + item, f'Subscripting {self.__name__} requires a type.' + ) + for item in parameters + ] + return typing._GenericAlias(self, tuple(parameters)) + + def __reduce__(self): + return self.__name__ + + def __init_subclass__(cls, *args, **kwargs): + raise TypeError( + "type 'typing_extensions.TypeAliasType' is not an acceptable base type" + ) + + # The presence of this method convinces typing._type_check + # that TypeAliasTypes are types. + def __call__(self): + raise TypeError("Type alias is not callable") + + if sys.version_info >= (3, 10): + def __or__(self, right): + return typing.Union[self, right] + + def __ror__(self, left): + return typing.Union[left, self] From 8b6582e26822ed4bd51f8ac8bd5ac0383626d137 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 20 May 2023 16:29:41 -0700 Subject: [PATCH 34/46] Fix tests on Python 3.12 (#162) Co-authored-by: Alex Waygood --- CHANGELOG.md | 2 + src/test_typing_extensions.py | 36 +++++++++-- src/typing_extensions.py | 109 ++++++++++++++++++++++------------ 3 files changed, 103 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6d0233a..e3247b46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,8 @@ - Backport the implementation of `NewType` from 3.10 (where it is implemented as a class rather than a function). This allows user-defined `NewType`s to be pickled. Patch by Alex Waygood. +- Fix tests and import on Python 3.12, where `typing.TypeVar` can no longer be + subclassed. Patch by Jelle Zijlstra. - Add `typing_extensions.TypeAliasType`, a backport of `typing.TypeAliasType` from PEP 695. Patch by Jelle Zijlstra. - Backport changes to the repr of `typing.Unpack` that were made in order to diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index dadf6e3c..7c940678 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -14,6 +14,7 @@ import re import subprocess import tempfile +import textwrap import types from pathlib import Path from unittest import TestCase, main, skipUnless, skipIf @@ -47,6 +48,9 @@ # 3.11 makes runtime type checks (_type_check) more lenient. TYPING_3_11_0 = sys.version_info[:3] >= (3, 11, 0) +# 3.12 changes the representation of Unpack[] (PEP 692) +TYPING_3_12_0 = sys.version_info[:3] >= (3, 12, 0) + # https://github.com/python/cpython/pull/27017 was backported into some 3.9 and 3.10 # versions, but not all HAS_FORWARD_MODULE = "module" in inspect.signature(typing._type_check).parameters @@ -2396,7 +2400,7 @@ def bar(self, x: str) -> str: with self.assertRaises(TypeError): PR[int, ClassVar] - if sys.version_info >= (3, 12): + if hasattr(typing, "TypeAliasType"): exec(textwrap.dedent( """ def test_pep695_generic_protocol_callable_members(self): @@ -4342,8 +4346,8 @@ def test_signature_on_37(self): @skipUnless(TYPING_3_9_0, "NamedTuple was a class on 3.8 and lower") def test_same_as_typing_NamedTuple_39_plus(self): self.assertEqual( - set(dir(NamedTuple)), - set(dir(typing.NamedTuple)) | {"__text_signature__"} + set(dir(NamedTuple)) - {"__text_signature__"}, + set(dir(typing.NamedTuple)) ) self.assertIs(type(NamedTuple), type(typing.NamedTuple)) @@ -4374,7 +4378,12 @@ class GenericNamedTuple(NamedTuple, Generic[T]): class TypeVarLikeDefaultsTests(BaseTestCase): def test_typevar(self): T = typing_extensions.TypeVar('T', default=int) + typing_T = TypeVar('T') self.assertEqual(T.__default__, int) + self.assertIsInstance(T, typing_extensions.TypeVar) + self.assertIsInstance(T, typing.TypeVar) + self.assertIsInstance(typing_T, typing.TypeVar) + self.assertIsInstance(typing_T, typing_extensions.TypeVar) class A(Generic[T]): ... Alias = Optional[T] @@ -4388,6 +4397,12 @@ def test_typevar_none(self): def test_paramspec(self): P = ParamSpec('P', default=(str, int)) self.assertEqual(P.__default__, (str, int)) + self.assertIsInstance(P, ParamSpec) + if hasattr(typing, "ParamSpec"): + self.assertIsInstance(P, typing.ParamSpec) + typing_P = typing.ParamSpec('P') + self.assertIsInstance(typing_P, typing.ParamSpec) + self.assertIsInstance(typing_P, ParamSpec) class A(Generic[P]): ... Alias = typing.Callable[P, None] @@ -4395,6 +4410,12 @@ class A(Generic[P]): ... def test_typevartuple(self): Ts = TypeVarTuple('Ts', default=Unpack[Tuple[str, int]]) self.assertEqual(Ts.__default__, Unpack[Tuple[str, int]]) + self.assertIsInstance(Ts, TypeVarTuple) + if hasattr(typing, "TypeVarTuple"): + self.assertIsInstance(Ts, typing.TypeVarTuple) + typing_Ts = typing.TypeVarTuple('Ts') + self.assertIsInstance(typing_Ts, typing.TypeVarTuple) + self.assertIsInstance(typing_Ts, TypeVarTuple) class A(Generic[Unpack[Ts]]): ... Alias = Optional[Unpack[Ts]] @@ -4454,8 +4475,13 @@ class MyRegisteredBuffer: def __buffer__(self, flags: int) -> memoryview: return memoryview(b'') - self.assertNotIsInstance(MyRegisteredBuffer(), Buffer) - self.assertNotIsSubclass(MyRegisteredBuffer, Buffer) + # On 3.12, collections.abc.Buffer does a structural compatibility check + if TYPING_3_12_0: + self.assertIsInstance(MyRegisteredBuffer(), Buffer) + self.assertIsSubclass(MyRegisteredBuffer, Buffer) + else: + self.assertNotIsInstance(MyRegisteredBuffer(), Buffer) + self.assertNotIsSubclass(MyRegisteredBuffer, Buffer) Buffer.register(MyRegisteredBuffer) self.assertIsInstance(MyRegisteredBuffer(), Buffer) self.assertIsSubclass(MyRegisteredBuffer, Buffer) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 82e4ba76..6fd0f241 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -759,6 +759,7 @@ def runtime_checkable(cls): SupportsInt = typing.SupportsInt SupportsFloat = typing.SupportsFloat SupportsComplex = typing.SupportsComplex + SupportsBytes = typing.SupportsBytes SupportsIndex = typing.SupportsIndex SupportsAbs = typing.SupportsAbs SupportsRound = typing.SupportsRound @@ -1343,39 +1344,53 @@ def __repr__(self): above.""") +def _set_default(type_param, default): + 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: + type_param.__default__ = typing._type_check(default, "Default must be a type") + else: + type_param.__default__ = None + + class _DefaultMixin: """Mixin for TypeVarLike defaults.""" __slots__ = () - - def __init__(self, default): - if isinstance(default, (tuple, list)): - self.__default__ = tuple((typing._type_check(d, "Default must be a type") - for d in default)) - elif default != _marker: - self.__default__ = typing._type_check(default, "Default must be a type") - else: - self.__default__ = None + __init__ = _set_default # Add default and infer_variance parameters from PEP 696 and 695 -class TypeVar(typing.TypeVar, _DefaultMixin, _root=True): - """Type variable.""" - - __module__ = 'typing' - - def __init__(self, name, *constraints, bound=None, +class _TypeVarMeta(type): + def __call__(self, name, *constraints, bound=None, covariant=False, contravariant=False, default=_marker, infer_variance=False): - super().__init__(name, *constraints, bound=bound, covariant=covariant, - contravariant=contravariant) - _DefaultMixin.__init__(self, default) - self.__infer_variance__ = infer_variance + if hasattr(typing, "TypeAliasType"): + # PEP 695 implemented, 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) + typevar.__infer_variance__ = infer_variance + _set_default(typevar, default) # for pickling: def_mod = _caller() if def_mod != 'typing_extensions': - self.__module__ = def_mod + typevar.__module__ = def_mod + return typevar + + def __instancecheck__(self, __instance: Any) -> bool: + return isinstance(__instance, typing.TypeVar) + + +class TypeVar(metaclass=_TypeVarMeta): + """Type variable.""" + + __module__ = 'typing' # Python 3.10+ has PEP 612 @@ -1443,22 +1458,28 @@ def __eq__(self, other): # 3.10+ if hasattr(typing, 'ParamSpec'): - # Add default Parameter - PEP 696 - class ParamSpec(typing.ParamSpec, _DefaultMixin, _root=True): - """Parameter specification variable.""" - - __module__ = 'typing' - - def __init__(self, name, *, bound=None, covariant=False, contravariant=False, + # Add default parameter - PEP 696 + class _ParamSpecMeta(type): + def __call__(self, name, *, bound=None, + covariant=False, contravariant=False, default=_marker): - super().__init__(name, bound=bound, covariant=covariant, - contravariant=contravariant) - _DefaultMixin.__init__(self, default) + paramspec = typing.ParamSpec(name, bound=bound, + covariant=covariant, contravariant=contravariant) + _set_default(paramspec, default) # for pickling: def_mod = _caller() if def_mod != 'typing_extensions': - self.__module__ = def_mod + paramspec.__module__ = def_mod + return paramspec + + def __instancecheck__(self, __instance: Any) -> bool: + return isinstance(__instance, typing.ParamSpec) + + class ParamSpec(metaclass=_ParamSpecMeta): + """Parameter specification.""" + + __module__ = 'typing' # 3.7-3.9 else: @@ -2061,18 +2082,28 @@ def _is_unpack(obj): if hasattr(typing, "TypeVarTuple"): # 3.11+ - # Add default Parameter - PEP 696 - class TypeVarTuple(typing.TypeVarTuple, _DefaultMixin, _root=True): - """Type variable tuple.""" - - def __init__(self, name, *, default=_marker): - super().__init__(name) - _DefaultMixin.__init__(self, default) + # Add default parameter - PEP 696 + class _TypeVarTupleMeta(type): + def __call__(self, name, *, default=_marker): + tvt = typing.TypeVarTuple(name) + _set_default(tvt, default) # for pickling: def_mod = _caller() if def_mod != 'typing_extensions': - self.__module__ = def_mod + tvt.__module__ = def_mod + return tvt + + def __instancecheck__(self, __instance: Any) -> bool: + return isinstance(__instance, typing.TypeVarTuple) + + class TypeVarTuple(metaclass=_TypeVarTupleMeta): + """Type variable tuple.""" + + __module__ = 'typing' + + def __init_subclass__(self, *args, **kwds): + raise TypeError("Cannot subclass special typing classes") else: class TypeVarTuple(_DefaultMixin): From cca17ebfd641fc3b681912789ad4cfe131daf2c5 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 21 May 2023 05:37:07 -0700 Subject: [PATCH 35/46] Bring over TypeVarTests from CPython (#165) I started out trying to backport python/cpython#104571, but realized that it makes sense to backport CPython's whole TypeVarTests class since we now have our own implementation of TypeVar. I dropped test_var_substitution and test_bad_var_substitution since they rely on the internal __typing_subst__ method, and the type substitution logic is generally very hard to get precisely the same across versions. --- src/test_typing_extensions.py | 150 +++++++++++++++++++++++++++++++++- src/typing_extensions.py | 8 ++ 2 files changed, 155 insertions(+), 3 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 7c940678..f7820ec0 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -20,7 +20,7 @@ from unittest import TestCase, main, skipUnless, skipIf from unittest.mock import patch import typing -from typing import TypeVar, Optional, Union, AnyStr +from typing import Optional, Union, AnyStr from typing import T, KT, VT # Not in __all__. from typing import Tuple, List, Set, Dict, Iterable, Iterator, Callable from typing import Generic @@ -36,7 +36,7 @@ from typing_extensions import assert_type, get_type_hints, get_origin, get_args, get_original_bases from typing_extensions import clear_overloads, get_overloads, overload from typing_extensions import NamedTuple -from typing_extensions import override, deprecated, Buffer, TypeAliasType +from typing_extensions import override, deprecated, Buffer, TypeAliasType, TypeVar from _typed_dict_test_helper import Foo, FooGeneric # Flags used to mark tests that only apply after a specific @@ -3306,6 +3306,7 @@ def test_basic_plain(self): P = ParamSpec('P') self.assertEqual(P, P) self.assertIsInstance(P, ParamSpec) + self.assertEqual(P.__name__, 'P') # Should be hashable hash(P) @@ -4375,10 +4376,153 @@ class GenericNamedTuple(NamedTuple, Generic[T]): self.assertEqual(CallNamedTuple.__orig_bases__, (NamedTuple,)) +class TypeVarTests(BaseTestCase): + def test_basic_plain(self): + T = TypeVar('T') + # T equals itself. + self.assertEqual(T, T) + # T is an instance of TypeVar + self.assertIsInstance(T, TypeVar) + self.assertEqual(T.__name__, 'T') + self.assertEqual(T.__constraints__, ()) + self.assertIs(T.__bound__, None) + self.assertIs(T.__covariant__, False) + self.assertIs(T.__contravariant__, False) + self.assertIs(T.__infer_variance__, False) + + def test_attributes(self): + T_bound = TypeVar('T_bound', bound=int) + self.assertEqual(T_bound.__name__, 'T_bound') + self.assertEqual(T_bound.__constraints__, ()) + self.assertIs(T_bound.__bound__, int) + + T_constraints = TypeVar('T_constraints', int, str) + self.assertEqual(T_constraints.__name__, 'T_constraints') + self.assertEqual(T_constraints.__constraints__, (int, str)) + self.assertIs(T_constraints.__bound__, None) + + T_co = TypeVar('T_co', covariant=True) + self.assertEqual(T_co.__name__, 'T_co') + self.assertIs(T_co.__covariant__, True) + self.assertIs(T_co.__contravariant__, False) + self.assertIs(T_co.__infer_variance__, False) + + T_contra = TypeVar('T_contra', contravariant=True) + self.assertEqual(T_contra.__name__, 'T_contra') + self.assertIs(T_contra.__covariant__, False) + self.assertIs(T_contra.__contravariant__, True) + self.assertIs(T_contra.__infer_variance__, False) + + T_infer = TypeVar('T_infer', infer_variance=True) + self.assertEqual(T_infer.__name__, 'T_infer') + self.assertIs(T_infer.__covariant__, False) + self.assertIs(T_infer.__contravariant__, False) + self.assertIs(T_infer.__infer_variance__, True) + + def test_typevar_instance_type_error(self): + T = TypeVar('T') + with self.assertRaises(TypeError): + isinstance(42, T) + + def test_typevar_subclass_type_error(self): + T = TypeVar('T') + with self.assertRaises(TypeError): + issubclass(int, T) + with self.assertRaises(TypeError): + issubclass(T, int) + + def test_constrained_error(self): + with self.assertRaises(TypeError): + X = TypeVar('X', int) + X + + def test_union_unique(self): + X = TypeVar('X') + Y = TypeVar('Y') + self.assertNotEqual(X, Y) + self.assertEqual(Union[X], X) + self.assertNotEqual(Union[X], Union[X, Y]) + self.assertEqual(Union[X, X], X) + self.assertNotEqual(Union[X, int], Union[X]) + self.assertNotEqual(Union[X, int], Union[int]) + self.assertEqual(Union[X, int].__args__, (X, int)) + self.assertEqual(Union[X, int].__parameters__, (X,)) + self.assertIs(Union[X, int].__origin__, Union) + + if hasattr(types, "UnionType"): + def test_or(self): + X = TypeVar('X') + # use a string because str doesn't implement + # __or__/__ror__ itself + self.assertEqual(X | "x", Union[X, "x"]) + self.assertEqual("x" | X, Union["x", X]) + # make sure the order is correct + self.assertEqual(get_args(X | "x"), (X, typing.ForwardRef("x"))) + self.assertEqual(get_args("x" | X), (typing.ForwardRef("x"), X)) + + def test_union_constrained(self): + A = TypeVar('A', str, bytes) + self.assertNotEqual(Union[A, str], Union[A]) + + def test_repr(self): + self.assertEqual(repr(T), '~T') + self.assertEqual(repr(KT), '~KT') + self.assertEqual(repr(VT), '~VT') + self.assertEqual(repr(AnyStr), '~AnyStr') + T_co = TypeVar('T_co', covariant=True) + self.assertEqual(repr(T_co), '+T_co') + T_contra = TypeVar('T_contra', contravariant=True) + self.assertEqual(repr(T_contra), '-T_contra') + + def test_no_redefinition(self): + self.assertNotEqual(TypeVar('T'), TypeVar('T')) + self.assertNotEqual(TypeVar('T', int, str), TypeVar('T', int, str)) + + def test_cannot_subclass(self): + with self.assertRaises(TypeError): + class V(TypeVar): pass + T = TypeVar("T") + with self.assertRaises(TypeError): + class V(T): pass + + def test_cannot_instantiate_vars(self): + with self.assertRaises(TypeError): + TypeVar('A')() + + def test_bound_errors(self): + with self.assertRaises(TypeError): + TypeVar('X', bound=Union) + with self.assertRaises(TypeError): + TypeVar('X', str, float, bound=Employee) + with self.assertRaisesRegex(TypeError, + r"Bound must be a type\. Got \(1, 2\)\."): + TypeVar('X', bound=(1, 2)) + + # Technically we could run it on later versions of 3.7 and 3.8, + # but that's not worth the effort. + @skipUnless(TYPING_3_9_0, "Fix was not backported") + def test_missing__name__(self): + # See bpo-39942 + code = ("import typing\n" + "T = typing.TypeVar('T')\n" + ) + exec(code, {}) + + def test_no_bivariant(self): + with self.assertRaises(ValueError): + TypeVar('T', covariant=True, contravariant=True) + + def test_cannot_combine_explicit_and_infer(self): + with self.assertRaises(ValueError): + TypeVar('T', covariant=True, infer_variance=True) + with self.assertRaises(ValueError): + TypeVar('T', contravariant=True, infer_variance=True) + + class TypeVarLikeDefaultsTests(BaseTestCase): def test_typevar(self): T = typing_extensions.TypeVar('T', default=int) - typing_T = TypeVar('T') + typing_T = typing.TypeVar('T') self.assertEqual(T.__default__, int) self.assertIsInstance(T, typing_extensions.TypeVar) self.assertIsInstance(T, typing.TypeVar) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 6fd0f241..ff5aefea 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1374,6 +1374,8 @@ def __call__(self, name, *constraints, bound=None, 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) @@ -1392,6 +1394,9 @@ class TypeVar(metaclass=_TypeVarMeta): __module__ = 'typing' + def __init_subclass__(cls) -> None: + raise TypeError(f"type '{__name__}.TypeVar' is not an acceptable base type") + # Python 3.10+ has PEP 612 if hasattr(typing, 'ParamSpecArgs'): @@ -1481,6 +1486,9 @@ class ParamSpec(metaclass=_ParamSpecMeta): __module__ = 'typing' + def __init_subclass__(cls) -> None: + raise TypeError(f"type '{__name__}.ParamSpec' is not an acceptable base type") + # 3.7-3.9 else: From d03ea9b73f7b748a4c7d3c8d220220500070f354 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 21 May 2023 05:40:28 -0700 Subject: [PATCH 36/46] Further 3.12 compatibility fixes (#164) Make our TypeAliasType behave exactly like the 3.12 one --- src/test_typing_extensions.py | 56 ++++++++++++++++++++++++++++------- src/typing_extensions.py | 48 ++++++++++++++++++++++-------- 2 files changed, 80 insertions(+), 24 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index f7820ec0..8d0ed9df 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -4745,30 +4745,64 @@ def test_attributes(self): self.assertEqual(Variadic.__type_params__, (Ts,)) self.assertEqual(Variadic.__parameters__, tuple(iter(Ts))) - def test_immutable(self): + def test_cannot_set_attributes(self): Simple = TypeAliasType("Simple", int) - with self.assertRaisesRegex(AttributeError, "Can't set attribute"): + with self.assertRaisesRegex(AttributeError, "readonly attribute"): Simple.__name__ = "NewName" - with self.assertRaisesRegex(AttributeError, "Can't set attribute"): + with self.assertRaisesRegex( + AttributeError, + "attribute '__value__' of 'typing.TypeAliasType' objects is not writable", + ): Simple.__value__ = str - with self.assertRaisesRegex(AttributeError, "Can't set attribute"): + with self.assertRaisesRegex( + AttributeError, + "attribute '__type_params__' of 'typing.TypeAliasType' objects is not writable", + ): Simple.__type_params__ = (T,) - with self.assertRaisesRegex(AttributeError, "Can't set attribute"): + with self.assertRaisesRegex( + AttributeError, + "attribute '__parameters__' of 'typing.TypeAliasType' objects is not writable", + ): Simple.__parameters__ = (T,) - with self.assertRaisesRegex(AttributeError, "Can't set attribute"): + with self.assertRaisesRegex( + AttributeError, + "attribute '__module__' of 'typing.TypeAliasType' objects is not writable", + ): + Simple.__module__ = 42 + with self.assertRaisesRegex( + AttributeError, + "'typing.TypeAliasType' object has no attribute 'some_attribute'", + ): Simple.some_attribute = "not allowed" - with self.assertRaisesRegex(AttributeError, "Can't delete attribute"): + + def test_cannot_delete_attributes(self): + Simple = TypeAliasType("Simple", int) + with self.assertRaisesRegex(AttributeError, "readonly attribute"): del Simple.__name__ - with self.assertRaisesRegex(AttributeError, "Can't delete attribute"): - del Simple.nonexistent_attribute + with self.assertRaisesRegex( + AttributeError, + "attribute '__value__' of 'typing.TypeAliasType' objects is not writable", + ): + del Simple.__value__ + with self.assertRaisesRegex( + AttributeError, + "'typing.TypeAliasType' object has no attribute 'some_attribute'", + ): + del Simple.some_attribute def test_or(self): Alias = TypeAliasType("Alias", int) if sys.version_info >= (3, 10): - self.assertEqual(Alias | "Ref", Union[Alias, typing.ForwardRef("Ref")]) + self.assertEqual(Alias | int, Union[Alias, int]) + self.assertEqual(Alias | None, Union[Alias, None]) + self.assertEqual(Alias | (int | str), Union[Alias, int | str]) + self.assertEqual(Alias | list[float], Union[Alias, list[float]]) else: with self.assertRaises(TypeError): - Alias | "Ref" + Alias | int + # Rejected on all versions + with self.assertRaises(TypeError): + Alias | "Ref" def test_getitem(self): ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,)) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index ff5aefea..2a635ba7 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1061,9 +1061,6 @@ def greet(name: str) -> None: if hasattr(typing, "Required"): get_type_hints = typing.get_type_hints else: - import functools - import types - # replaces _strip_annotations() def _strip_extras(t): """Strips Annotated, Required and NotRequired from a given type.""" @@ -1076,12 +1073,12 @@ def _strip_extras(t): if stripped_args == t.__args__: return t return t.copy_with(stripped_args) - if hasattr(types, "GenericAlias") and isinstance(t, types.GenericAlias): + if hasattr(_types, "GenericAlias") and isinstance(t, _types.GenericAlias): stripped_args = tuple(_strip_extras(a) for a in t.__args__) if stripped_args == t.__args__: return t - return types.GenericAlias(t.__origin__, stripped_args) - if hasattr(types, "UnionType") and isinstance(t, types.UnionType): + return _types.GenericAlias(t.__origin__, stripped_args) + if hasattr(_types, "UnionType") and isinstance(t, _types.UnionType): stripped_args = tuple(_strip_extras(a) for a in t.__args__) if stripped_args == t.__args__: return t @@ -2691,6 +2688,15 @@ def __ror__(self, other): if hasattr(typing, "TypeAliasType"): TypeAliasType = typing.TypeAliasType else: + def _is_unionable(obj): + """Corresponds to is_unionable() in unionobject.c in CPython.""" + return obj is None or isinstance(obj, ( + type, + _types.GenericAlias, + _types.UnionType, + TypeAliasType, + )) + class TypeAliasType: """Create named, parameterized type aliases. @@ -2740,15 +2746,25 @@ def __init__(self, name: str, value, *, type_params=()): def __setattr__(self, __name: str, __value: object) -> None: if hasattr(self, "__name__"): - raise AttributeError( - f"Can't set attribute {__name!r} on an instance of TypeAliasType" - ) + self._raise_attribute_error(__name) super().__setattr__(__name, __value) - def __delattr__(self, __name: str) -> None: - raise AttributeError( - f"Can't delete attribute {__name!r} on an instance of TypeAliasType" - ) + def __delattr__(self, __name: str) -> Never: + self._raise_attribute_error(__name) + + def _raise_attribute_error(self, name: str) -> Never: + # Match the Python 3.12 error messages exactly + if name == "__name__": + raise AttributeError("readonly attribute") + elif name in {"__value__", "__type_params__", "__parameters__", "__module__"}: + raise AttributeError( + f"attribute '{name}' of 'typing.TypeAliasType' objects " + "is not writable" + ) + else: + raise AttributeError( + f"'typing.TypeAliasType' object has no attribute '{name}'" + ) def __repr__(self) -> str: return self.__name__ @@ -2779,7 +2795,13 @@ def __call__(self): if sys.version_info >= (3, 10): def __or__(self, right): + # For forward compatibility with 3.12, reject Unions + # that are not accepted by the built-in Union. + if not _is_unionable(right): + return NotImplemented return typing.Union[self, right] def __ror__(self, left): + if not _is_unionable(left): + return NotImplemented return typing.Union[left, self] From 024d465a38572e55a4174a7d7c0325c68f28f794 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 21 May 2023 15:41:20 -0700 Subject: [PATCH 37/46] Add documentation page for typing-extensions (#166) Co-authored-by: Alex Waygood --- doc/.gitignore | 1 + doc/Makefile | 20 ++ doc/_extensions/__init__.py | 0 doc/_extensions/gh_link.py | 29 ++ doc/conf.py | 34 +++ doc/index.rst | 590 ++++++++++++++++++++++++++++++++++++ doc/make.bat | 35 +++ 7 files changed, 709 insertions(+) create mode 100644 doc/.gitignore create mode 100644 doc/Makefile create mode 100644 doc/_extensions/__init__.py create mode 100644 doc/_extensions/gh_link.py create mode 100644 doc/conf.py create mode 100644 doc/index.rst create mode 100644 doc/make.bat diff --git a/doc/.gitignore b/doc/.gitignore new file mode 100644 index 00000000..69fa449d --- /dev/null +++ b/doc/.gitignore @@ -0,0 +1 @@ +_build/ diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 00000000..d4bb2cbb --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/doc/_extensions/__init__.py b/doc/_extensions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/doc/_extensions/gh_link.py b/doc/_extensions/gh_link.py new file mode 100644 index 00000000..3442dbd3 --- /dev/null +++ b/doc/_extensions/gh_link.py @@ -0,0 +1,29 @@ +from docutils import nodes + + +def setup(app): + app.add_role( + "pr", autolink("https://github.com/python/typing_extensions/pull/{}", "PR #") + ) + app.add_role( + "pr-cpy", autolink("https://github.com/python/cpython/pull/{}", "CPython PR #") + ) + app.add_role( + "issue", + autolink("https://github.com/python/typing_extensions/issues/{}", "issue #"), + ) + app.add_role( + "issue-cpy", + autolink("https://github.com/python/cpython/issues/{}", "CPython issue #"), + ) + + +def autolink(pattern: str, prefix: str): + def role(name, rawtext, text: str, lineno, inliner, options=None, content=None): + if options is None: + options = {} + url = pattern.format(text) + node = nodes.reference(rawtext, f"{prefix}{text}", refuri=url, **options) + return [node], [] + + return role diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 00000000..7984bc22 --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,34 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +import os.path +import sys + +sys.path.insert(0, os.path.abspath('.')) + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'typing_extensions' +copyright = '2023, Guido van Rossum and others' +author = 'Guido van Rossum and others' +release = '4.6.0' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ['sphinx.ext.intersphinx', '_extensions.gh_link'] + +templates_path = ['_templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +intersphinx_mapping = {'py': ('https://docs.python.org/3.12', None)} + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'alabaster' +html_static_path = ['_static'] diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 00000000..4334414a --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,590 @@ + +Welcome to typing_extensions's documentation! +============================================= + +``typing_extensions`` complements the standard-library :py:mod:`typing` module, +providing runtime support for type hints as specified by :pep:`484` and subsequent +PEPs. The module serves two related purposes: + +- Enable use of new type system features on older Python versions. For example, + :py:data:`typing.TypeGuard` is new in Python 3.10, but ``typing_extensions`` allows + users on previous Python versions to use it too. +- Enable experimentation with type system features proposed in new PEPs before they are accepted and + added to the :py:mod:`typing` module. + +New features may be added to ``typing_extensions`` as soon as they are specified +in a PEP that has been added to the `python/peps `_ +repository. If the PEP is accepted, the feature will then be added to the +:py:mod:`typing` module for the next CPython release. No typing PEP that +affected ``typing_extensions`` has been rejected so far, so we haven't yet +figured out how to deal with that possibility. + +Starting with version 4.0.0, ``typing_extensions`` uses +`Semantic Versioning `_. The +major version is incremented for all backwards-incompatible changes. +Therefore, it's safe to depend +on ``typing_extensions`` like this: ``typing_extensions >=x.y, <(x+1)``, +where ``x.y`` is the first version that includes all features you need. +In view of the wide usage of ``typing_extensions`` across the ecosystem, +we are highly hesitant to break backwards compatibility, and we do not +expect to increase the major version number in the foreseeable future. + +``typing_extensions`` supports Python versions 3.7 and higher. In the future, +support for older Python versions will be dropped some time after that version +reaches end of life. + +Module contents +--------------- + +As most of the features in ``typing_extensions`` exist in :py:mod:`typing` +in newer versions of Python, the documentation here is brief and focuses +on aspects that are specific to ``typing_extensions``, such as limitations +on specific Python versions. + +Special typing primitives +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. data:: Annotated + + See :py:data:`typing.Annotated` and :pep:`593`. In ``typing`` since 3.9. + + .. versionchanged:: 4.1.0 + + ``Annotated`` can now wrap :data:`ClassVar` and :data:`Final`. + +.. data:: Any + + See :py:data:`typing.Any`. + + Since Python 3.11, ``typing.Any`` can be used as a base class. + ``typing_extensions.Any`` supports this feature on older versions. + + .. versionadded:: 4.4.0 + + Added to support inheritance from ``Any``. + +.. data:: ClassVar + + See :py:data:`typing.ClassVar` and :pep:`526`. In ``typing`` since 3.5.3. + +.. data:: Concatenate + + See :py:data:`typing.Concatenate` and :pep:`612`. In ``typing`` since 3.10. + + The backport does not support certain operations involving ``...`` as + a parameter; see :issue:`48` and :issue:`110` for details. + +.. data:: Final + + See :py:data:`typing.Final` and :pep:`591`. In ``typing`` since 3.8. + +.. data:: Literal + + See :py:data:`typing.Literal` and :pep:`586`. In ``typing`` since 3.8. + + :py:data:`typing.Literal` does not flatten or deduplicate parameters on Python <3.9.1, and a + caching bug was fixed in 3.10.1/3.9.8. The ``typing_extensions`` version + flattens and deduplicates parameters on all Python versions, and the caching + bug is also fixed on all versions. + + .. versionchanged:: 4.6.0 + + Backported the bug fixes from :pr-cpy:`29334`, :pr-cpy:`23294`, and :pr-cpy:`23383`. + +.. data:: LiteralString + + See :py:data:`typing.LiteralString` and :pep:`675`. In ``typing`` since 3.11. + + .. versionadded:: 4.1.0 + +.. class:: NamedTuple + + See :py:class:`typing.NamedTuple`. + + ``typing_extensions`` backports several changes + to ``NamedTuple`` on Python 3.11 and lower: in 3.11, + support for generic ``NamedTuple``\ s was added, and + in 3.12, the ``__orig_bases__`` attribute was added. + + .. versionadded:: 4.3.0 + + Added to provide support for generic ``NamedTuple``\ s. + + .. versionchanged:: 4.6.0 + + Support for the ``__orig_bases__`` attribute was added. + +.. data:: Never + + See :py:data:`typing.Never`. In ``typing`` since 3.11. + + .. versionadded:: 4.1.0 + +.. class:: NewType(name, tp) + + See :py:class:`typing.NewType`. In ``typing`` since 3.5.2. + + Instances of ``NewType`` were made picklable in 3.10 and an error message was + improved in 3.11; ``typing_extensions`` backports these changes. + + .. versionchanged:: 4.6.0 + + The improvements from Python 3.10 and 3.11 were backported. + +.. data:: NoReturn + + See :py:data:`typing.NoReturn`. In ``typing`` since 3.5.4 and 3.6.2. + +.. data:: NotRequired + + See :py:data:`typing.NotRequired` and :pep:`655`. In ``typing`` since 3.11. + + .. versionadded:: 4.0.0 + +.. class:: ParamSpec(name, *, default=...) + + See :py:class:`typing.ParamSpec` and :pep:`612`. In ``typing`` since 3.10. + + The ``typing_extensions`` version adds support for the + ``default=`` argument from :pep:`696`. + + On older Python versions, ``typing_extensions.ParamSpec`` may not work + correctly with introspection tools like :func:`get_args` and + :func:`get_origin`. Certain special cases in user-defined + :py:class:`typing.Generic`\ s are also not available. + + .. versionchanged:: 4.4.0 + + Added support for the ``default=`` argument. + + .. versionchanged:: 4.6.0 + + The implementation was changed for compatibility with Python 3.12. + +.. class:: ParamSpecArgs + +.. class:: ParamSpecKwargs + + See :py:class:`typing.ParamSpecArgs` and :py:class:`typing.ParamSpecKwargs`. + In ``typing`` since 3.10. + +.. class:: Protocol + + See :py:class:`typing.Protocol` and :pep:`544`. In ``typing`` since 3.8. + + Python 3.12 improves the performance of runtime-checkable protocols; + ``typing_extensions`` backports this improvement. + + .. versionchanged:: 4.6.0 + + Backported the ability to define ``__init__`` methods on Protocol classes. + + .. versionchanged:: 4.6.0 + + Backported changes to runtime-checkable protocols from Python 3.12, + including :pr-cpy:`103034` and :pr-cpy:`26067`. + +.. data:: Required + + See :py:data:`typing.Required` and :pep:`655`. In ``typing`` since 3.11. + + .. versionadded:: 4.0.0 + +.. data:: Self + + See :py:data:`typing.Self` and :pep:`673`. In ``typing`` since 3.11. + + .. versionadded:: 4.0.0 + +.. class:: Type + + See :py:class:`typing.Type`. In ``typing`` since 3.5.2. + +.. data:: TypeAlias + + See :py:data:`typing.TypeAlias` and :pep:`613`. In ``typing`` since 3.10. + +.. class:: TypeAliasType(name, value, *, type_params=()) + + See :py:class:`typing.TypeAliasType` and :pep:`695`. In ``typing`` since 3.12. + + .. versionadded:: 4.6.0 + +.. data:: TypeGuard + + See :py:data:`typing.TypeGuard` and :pep:`647`. In ``typing`` since 3.10. + +.. class:: TypedDict + + See :py:class:`typing.TypedDict` and :pep:`589`. In ``typing`` since 3.8. + + ``typing_extensions`` backports various bug fixes and improvements + to ``TypedDict`` on Python 3.11 and lower. + :py:class:`TypedDict` does not store runtime information + about which (if any) keys are non-required in Python 3.8, and does not + honor the ``total`` keyword with old-style ``TypedDict()`` in Python + 3.9.0 and 3.9.1. :py:class:`typing.TypedDict` also does not support multiple inheritance + with :py:class:`typing.Generic` on Python <3.11, and :py:class:`typing.TypedDict` classes do not + consistently have the ``__orig_bases__`` attribute on Python <3.12. The + ``typing_extensions`` backport provides all of these features and bugfixes on + all Python versions. + + .. versionchanged:: 4.3.0 + + Added support for generic ``TypedDict``\ s. + + .. versionchanged:: 4.6.0 + + A :py:exc:`DeprecationWarning` is now emitted when a call-based + ``TypedDict`` is constructed using keyword arguments. + + .. versionchanged:: 4.6.0 + + Support for the ``__orig_bases__`` attribute was added. + +.. class:: TypeVar(name, *constraints, bound=None, covariant=False, + contravariant=False, infer_variance=False, default=...) + + See :py:class:`typing.TypeVar`. + + The ``typing_extensions`` version adds support for the + ``default=`` argument from :pep:`696`, as well as the + ``infer_variance=`` argument from :pep:`695` (also available + in Python 3.12). + + .. versionadded:: 4.4.0 + + Added in order to support the new ``default=`` and + ``infer_variance=`` arguments. + + .. versionchanged:: 4.6.0 + + The implementation was changed for compatibility with Python 3.12. + +.. class:: TypeVarTuple(name, *, default=...) + + See :py:class:`typing.TypeVarTuple` and :pep:`646`. In ``typing`` since 3.11. + + The ``typing_extensions`` version adds support for the + ``default=`` argument from :pep:`696`. + + .. versionadded:: 4.1.0 + + .. versionchanged:: 4.4.0 + + Added support for the ``default=`` argument. + + .. versionchanged:: 4.6.0 + + The implementation was changed for compatibility with Python 3.12. + +.. data:: Unpack + + See :py:data:`typing.Unpack` and :pep:`646`. In ``typing`` since 3.11. + + In Python 3.12, the ``repr()`` was changed as a result of :pep:`692`. + ``typing_extensions`` backports this change. + + .. versionadded:: 4.1.0 + + .. versionchanged:: 4.6.0 + + Backport ``repr()`` changes from Python 3.12. + +Generic concrete collections +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. class:: ChainMap + + See :py:class:`typing.ChainMap`. In ``typing`` since 3.5.4 and 3.6.1. + +.. class:: Counter + + See :py:class:`typing.Counter`. In ``typing`` since 3.5.4 and 3.6.1. + +.. class:: DefaultDict + + See :py:class:`typing.DefaultDict`. In ``typing`` since 3.5.2. + +.. class:: Deque + + See :py:class:`typing.Deque`. In ``typing`` since 3.5.4 and 3.6.1. + +.. class:: OrderedDict + + See :py:class:`typing.OrderedDict`. In ``typing`` since 3.7.2. + +Abstract Base Classes +~~~~~~~~~~~~~~~~~~~~~ + +.. class:: AsyncContextManager + + See :py:class:`typing.AsyncContextManager`. In ``typing`` since 3.5.4 and 3.6.2. + +.. class:: AsyncGenerator + + See :py:class:`typing.AsyncGenerator`. In ``typing`` since 3.6.1. + +.. class:: AsyncIterable + + See :py:class:`typing.AsyncIterable`. In ``typing`` since 3.5.2. + +.. class:: AsyncIterator + + See :py:class:`typing.AsyncIterator`. In ``typing`` since 3.5.2. + +.. class:: Awaitable + + See :py:class:`typing.Awaitable`. In ``typing`` since 3.5.2. + +.. class:: Buffer + + See :py:class:`collections.abc.Buffer`. Added to the standard library + in Python 3.12. + + .. versionadded:: 4.6.0 + +.. class:: ContextManager + + See :py:class:`typing.ContextManager`. In ``typing`` since 3.5.4. + +.. class:: Coroutine + + See :py:class:`typing.Coroutine`. In ``typing`` since 3.5.3. + +Protocols +~~~~~~~~~ + +.. class:: SupportsAbs + + See :py:class:`typing.SupportsAbs`. + + ``typing_extensions`` backports a more performant version of this + protocol on Python 3.11 and lower. + + .. versionadded:: 4.6.0 + +.. class:: SupportsBytes + + See :py:class:`typing.SupportsBytes`. + + ``typing_extensions`` backports a more performant version of this + protocol on Python 3.11 and lower. + + .. versionadded:: 4.6.0 + +.. class:: SupportsComplex + + See :py:class:`typing.SupportsComplex`. + + ``typing_extensions`` backports a more performant version of this + protocol on Python 3.11 and lower. + + .. versionadded:: 4.6.0 + +.. class:: SupportsFloat + + See :py:class:`typing.SupportsFloat`. + + ``typing_extensions`` backports a more performant version of this + protocol on Python 3.11 and lower. + + .. versionadded:: 4.6.0 + +.. class:: SupportsIndex + + See :py:class:`typing.SupportsIndex`. In ``typing`` since 3.8. + + ``typing_extensions`` backports a more performant version of this + protocol on Python 3.11 and lower. + + .. versionchanged:: 4.6.0 + + Backported the performance improvements from Python 3.12. + +.. class:: SupportsInt + + See :py:class:`typing.SupportsInt`. + + ``typing_extensions`` backports a more performant version of this + protocol on Python 3.11 and lower. + + .. versionadded:: 4.6.0 + +.. class:: SupportsRound + + See :py:class:`typing.SupportsRound`. + + ``typing_extensions`` backports a more performant version of this + protocol on Python 3.11 and lower. + + .. versionadded:: 4.6.0 + +Decorators +~~~~~~~~~~ + +.. decorator:: dataclass_transform(*, eq_default=False, order_default=False, + kw_only_default=False, frozen_default=False, + field_specifiers=(), **kwargs) + + See :py:func:`typing.dataclass_transform` and :pep:`681`. In ``typing`` since 3.11. + + Python 3.12 adds the ``frozen_default`` parameter; ``typing_extensions`` + backports this parameter. + + .. versionadded:: 4.1.0 + + .. versionchanged:: 4.2.0 + + The ``field_descriptors`` parameter was renamed to ``field_specifiers``. + For compatibility, the decorator now accepts arbitrary keyword arguments. + + .. versionchanged:: 4.5.0 + + The ``frozen_default`` parameter was added. + +.. decorator:: deprecated(msg, *, category=DeprecationWarning, stacklevel=1) + + See :pep:`702`. Experimental; not yet part of the standard library. + + .. versionadded:: 4.5.0 + +.. decorator:: final + + See :py:func:`typing.final` and :pep:`591`. In ``typing`` since 3.8. + + Since Python 3.11, this decorator supports runtime introspection + by setting the ``__final__`` attribute wherever possible; ``typing_extensions.final`` + backports this feature. + + .. versionchanged:: 4.1.0 + + The decorator now attempts to set the ``__final__`` attribute on decorated objects. + +.. decorator:: overload + + See :py:func:`typing.overload`. + + Since Python 3.11, this decorator supports runtime introspection + through :func:`get_overloads`; ``typing_extensions.overload`` + backports this feature. + + .. versionchanged:: 4.2.0 + + Introspection support via :func:`get_overloads` was added. + +.. decorator:: override + + See :py:func:`typing.override` and :pep:`698`. In ``typing`` since 3.12. + + .. versionadded:: 4.4.0 + + .. versionchanged:: 4.5.0 + + The decorator now attempts to set the ``__override__`` attribute on the decorated + object. + +.. decorator:: runtime_checkable + + See :py:func:`typing.runtime_checkable`. In ``typing`` since 3.8. + + In Python 3.12, the performance of runtime-checkable protocols was + improved, and ``typing_extensions`` backports these performance + improvements. + +Functions +~~~~~~~~~ + +.. function:: assert_never(arg) + + See :py:func:`typing.assert_never`. In ``typing`` since 3.11. + + .. versionadded:: 4.1.0 + +.. function:: assert_type(val, typ) + + See :py:func:`typing.assert_type`. In ``typing`` since 3.11. + + .. versionadded:: 4.2.0 + +.. function:: clear_overloads() + + See :py:func:`typing.clear_overloads`. In ``typing`` since 3.11. + + .. versionadded:: 4.2.0 + +.. function:: get_args(tp) + + See :py:func:`typing.get_args`. In ``typing`` since 3.8. + + This function was changed in 3.9 and 3.10 to deal with :data:`Annotated` + and :class:`ParamSpec` correctly; ``typing_extensions`` backports these + fixes. + +.. function:: get_origin(tp) + + See :py:func:`typing.get_origin`. In ``typing`` since 3.8. + + This function was changed in 3.9 and 3.10 to deal with :data:`Annotated` + and :class:`ParamSpec` correctly; ``typing_extensions`` backports these + fixes. + +.. function:: get_original_bases(cls) + + See :py:func:`types.get_original_bases`. Added to the standard library + in Python 3.12. + + This function should always produce correct results when called on classes + constructed using features from ``typing_extensions``. However, it may + produce incorrect results when called on some :py:class:`NamedTuple` or + :py:class:`TypedDict` classes on Python <=3.11. + + .. versionadded:: 4.6.0 + +.. function:: get_overloads(func) + + See :py:func:`typing.get_overloads`. In ``typing`` since 3.11. + + Before Python 3.11, this works only with overloads created through + :func:`overload`, not with :py:func:`typing.overload`. + + .. versionadded:: 4.2.0 + +.. function:: get_type_hints(obj, globalns=None, localns=None, include_extras=False) + + See :py:func:`typing.get_type_hints`. + + In Python 3.11, this function was changed to support the new + :py:data:`typing.Required` and :py:data:`typing.NotRequired`. + ``typing_extensions`` backports these fixes. + + .. versionchanged:: 4.1.0 + + Interaction with :data:`Required` and :data:`NotRequired`. + +.. function:: is_typeddict(tp) + + See :py:func:`typing.is_typeddict`. In ``typing`` since 3.10. + + On versions where :class:`TypedDict` is not the same as + :py:class:`typing.TypedDict`, this function recognizes + ``TypedDict`` classes created through either mechanism. + + .. versionadded:: 4.1.0 + +.. function:: reveal_type(obj) + + See :py:func:`typing.reveal_type`. In ``typing`` since 3.11. + + .. versionadded:: 4.1.0 + +Other +~~~~~ + +.. class:: Text + + See :py:class:`typing.Text`. In ``typing`` since 3.5.2. + +.. data:: TYPE_CHECKING + + See :py:data:`typing.TYPE_CHECKING`. In ``typing`` since 3.5.2. diff --git a/doc/make.bat b/doc/make.bat new file mode 100644 index 00000000..32bb2452 --- /dev/null +++ b/doc/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd From 52c53f83ffc94989d2e2b537047dd88b46de2279 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 22 May 2023 00:14:48 +0100 Subject: [PATCH 38/46] Slightly cleanup implementation of typevarlikes (#170) --- src/typing_extensions.py | 61 ++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 2a635ba7..93a0dca6 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1351,6 +1351,13 @@ def _set_default(type_param, default): type_param.__default__ = None +def _set_module(typevarlike): + # for pickling: + def_mod = _caller(depth=3) + if def_mod != 'typing_extensions': + typevarlike.__module__ = def_mod + + class _DefaultMixin: """Mixin for TypeVarLike defaults.""" @@ -1358,8 +1365,19 @@ class _DefaultMixin: __init__ = _set_default +class _TypeVarLikeMeta(type): + def __init__(cls, *args, **kwargs): + super().__init__(*args, **kwargs) + cls.__module__ = 'typing' + + def __instancecheck__(cls, __instance: Any) -> bool: + return isinstance(__instance, cls._backported_typevarlike) + + # Add default and infer_variance parameters from PEP 696 and 695 -class _TypeVarMeta(type): +class _TypeVarMeta(_TypeVarLikeMeta): + _backported_typevarlike = typing.TypeVar + def __call__(self, name, *constraints, bound=None, covariant=False, contravariant=False, default=_marker, infer_variance=False): @@ -1375,22 +1393,13 @@ def __call__(self, name, *constraints, bound=None, raise ValueError("Variance cannot be specified with infer_variance.") typevar.__infer_variance__ = infer_variance _set_default(typevar, default) - - # for pickling: - def_mod = _caller() - if def_mod != 'typing_extensions': - typevar.__module__ = def_mod + _set_module(typevar) return typevar - def __instancecheck__(self, __instance: Any) -> bool: - return isinstance(__instance, typing.TypeVar) - class TypeVar(metaclass=_TypeVarMeta): """Type variable.""" - __module__ = 'typing' - def __init_subclass__(cls) -> None: raise TypeError(f"type '{__name__}.TypeVar' is not an acceptable base type") @@ -1461,28 +1470,21 @@ def __eq__(self, other): if hasattr(typing, 'ParamSpec'): # Add default parameter - PEP 696 - class _ParamSpecMeta(type): + class _ParamSpecMeta(_TypeVarLikeMeta): + _backported_typevarlike = typing.ParamSpec + def __call__(self, name, *, bound=None, covariant=False, contravariant=False, default=_marker): paramspec = typing.ParamSpec(name, bound=bound, covariant=covariant, contravariant=contravariant) _set_default(paramspec, default) - - # for pickling: - def_mod = _caller() - if def_mod != 'typing_extensions': - paramspec.__module__ = def_mod + _set_module(paramspec) return paramspec - def __instancecheck__(self, __instance: Any) -> bool: - return isinstance(__instance, typing.ParamSpec) - class ParamSpec(metaclass=_ParamSpecMeta): """Parameter specification.""" - __module__ = 'typing' - def __init_subclass__(cls) -> None: raise TypeError(f"type '{__name__}.ParamSpec' is not an acceptable base type") @@ -2088,25 +2090,18 @@ def _is_unpack(obj): if hasattr(typing, "TypeVarTuple"): # 3.11+ # Add default parameter - PEP 696 - class _TypeVarTupleMeta(type): + class _TypeVarTupleMeta(_TypeVarLikeMeta): + _backported_typevarlike = typing.TypeVarTuple + def __call__(self, name, *, default=_marker): tvt = typing.TypeVarTuple(name) _set_default(tvt, default) - - # for pickling: - def_mod = _caller() - if def_mod != 'typing_extensions': - tvt.__module__ = def_mod + _set_module(tvt) return tvt - def __instancecheck__(self, __instance: Any) -> bool: - return isinstance(__instance, typing.TypeVarTuple) - class TypeVarTuple(metaclass=_TypeVarTupleMeta): """Type variable tuple.""" - __module__ = 'typing' - def __init_subclass__(self, *args, **kwds): raise TypeError("Cannot subclass special typing classes") From 3534900201ed1c49c76633a73beea1a71c20900e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 21 May 2023 16:15:02 -0700 Subject: [PATCH 39/46] Shorten README, link to docs page (#169) --- README.md | 194 +++---------------------------------------------- pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 186 deletions(-) diff --git a/README.md b/README.md index aad814ef..ddc11882 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ [![Chat at https://gitter.im/python/typing](https://badges.gitter.im/python/typing.svg)](https://gitter.im/python/typing) +[Documentation](https://typing-extensions.readthedocs.io/en/latest/#) – +[PyPI](https://pypi.org/project/typing-extensions/) + ## Overview The `typing_extensions` module serves two related purposes: @@ -12,200 +15,21 @@ The `typing_extensions` module serves two related purposes: - Enable experimentation with new type system PEPs before they are accepted and added to the `typing` module. -New features may be added to `typing_extensions` as soon as they are specified -in a PEP that has been added to the [python/peps](https://github.com/python/peps) -repository. If the PEP is accepted, the feature will then be added to `typing` -for the next CPython release. No typing PEP has been rejected so far, so we -haven't yet figured out how to deal with that possibility. - -Starting with version 4.0.0, `typing_extensions` uses +`typing_extensions` uses [Semantic Versioning](https://semver.org/). The -major version is incremented for all backwards-incompatible changes. +major version will be incremented only for backwards-incompatible changes. Therefore, it's safe to depend on `typing_extensions` like this: `typing_extensions >=x.y, <(x+1)`, where `x.y` is the first version that includes all features you need. -`typing_extensions` supports Python versions 3.7 and higher. In the future, -support for older Python versions will be dropped some time after that version -reaches end of life. +`typing_extensions` supports Python versions 3.7 and higher. ## Included items -This module currently contains the following: - -- Experimental features - - - The `default=` argument to `TypeVar`, `ParamSpec`, and `TypeVarTuple` (see [PEP 696](https://peps.python.org/pep-0696/)) - - The `infer_variance=` argument to `TypeVar` (see [PEP 695](https://peps.python.org/pep-0695/)) - - The `@deprecated` decorator (see [PEP 702](https://peps.python.org/pep-0702/)) - -- In the standard library since Python 3.12 - - - `override` (equivalent to `typing.override`; see [PEP 698](https://peps.python.org/pep-0698/)) - - `TypeAliasType` (equivalent to `typing.TypeAliasType`; see [PEP 695](https://peps.python.org/pep-0695/)) - - `Buffer` (equivalent to `collections.abc.Buffer`; see [PEP 688](https://peps.python.org/pep-0688/)) - - `get_original_bases` (equivalent to - [`types.get_original_bases`](https://docs.python.org/3.12/library/types.html#types.get_original_bases) - on 3.12+). - - This function should always produce correct results when called on classes - constructed using features from `typing_extensions`. However, it may - produce incorrect results when called on some `NamedTuple` or `TypedDict` - classes that use `typing.{NamedTuple,TypedDict}` on Python <=3.11. - -- In `typing` since Python 3.11 - - - `assert_never` - - `assert_type` - - `clear_overloads` - - `@dataclass_transform()` (see [PEP 681](https://peps.python.org/pep-0681/)) - - `get_overloads` - - `LiteralString` (see [PEP 675](https://peps.python.org/pep-0675/)) - - `Never` - - `NotRequired` (see [PEP 655](https://peps.python.org/pep-0655/)) - - `reveal_type` - - `Required` (see [PEP 655](https://peps.python.org/pep-0655/)) - - `Self` (see [PEP 673](https://peps.python.org/pep-0673/)) - - `TypeVarTuple` (see [PEP 646](https://peps.python.org/pep-0646/); the `typing_extensions` version supports the `default=` argument from [PEP 696](https://peps.python.org/pep-0696/)) - - `Unpack` (see [PEP 646](https://peps.python.org/pep-0646/)) - -- In `typing` since Python 3.10 - - - `Concatenate` (see [PEP 612](https://peps.python.org/pep-0612/)) - - `ParamSpec` (see [PEP 612](https://peps.python.org/pep-0612/); the `typing_extensions` version supports the `default=` argument from [PEP 696](https://peps.python.org/pep-0696/)) - - `ParamSpecArgs` (see [PEP 612](https://peps.python.org/pep-0612/)) - - `ParamSpecKwargs` (see [PEP 612](https://peps.python.org/pep-0612/)) - - `TypeAlias` (see [PEP 613](https://peps.python.org/pep-0613/)) - - `TypeGuard` (see [PEP 647](https://peps.python.org/pep-0647/)) - - `is_typeddict` - -- In `typing` since Python 3.9 - - - `Annotated` (see [PEP 593](https://peps.python.org/pep-0593/)) - -- In `typing` since Python 3.8 - - - `final` (see [PEP 591](https://peps.python.org/pep-0591/)) - - `Final` (see [PEP 591](https://peps.python.org/pep-0591/)) - - `Literal` (see [PEP 586](https://peps.python.org/pep-0586/)) - - `Protocol` (see [PEP 544](https://peps.python.org/pep-0544/)) - - `runtime_checkable` (see [PEP 544](https://peps.python.org/pep-0544/)) - - `TypedDict` (see [PEP 589](https://peps.python.org/pep-0589/)) - - `get_origin` (`typing_extensions` provides this function only in Python 3.7+) - - `get_args` (`typing_extensions` provides this function only in Python 3.7+) - - `SupportsIndex` - -- In `typing` since Python 3.7 - - - `OrderedDict` - -- In `typing` since Python 3.5 or 3.6 (see [the typing documentation](https://docs.python.org/3.10/library/typing.html) for details) - - - `AsyncContextManager` - - `AsyncGenerator` - - `AsyncIterable` - - `AsyncIterator` - - `Awaitable` - - `ChainMap` - - `ClassVar` (see [PEP 526](https://peps.python.org/pep-0526/)) - - `ContextManager` - - `Coroutine` - - `Counter` - - `DefaultDict` - - `Deque` - - `NewType` - - `NoReturn` - - `overload` - - `Text` - - `Type` - - `TYPE_CHECKING` - - `get_type_hints` - -- The following have always been present in `typing`, but the `typing_extensions` versions provide - additional features: - - - `Any` (supports inheritance since Python 3.11) - - `NamedTuple` (supports multiple inheritance with `Generic` since Python 3.11) - - `TypeVar` (see PEPs [695](https://peps.python.org/pep-0695/) and [696](https://peps.python.org/pep-0696/)) - -The following runtime-checkable protocols have always been present in `typing`, -but the `isinstance()` checks against the `typing_extensions` versions are much -faster on Python <3.12: - - - `SupportsInt` - - `SupportsFloat` - - `SupportsComplex` - - `SupportsBytes` - - `SupportsAbs` - - `SupportsRound` - -# Other Notes and Limitations - -Certain objects were changed after they were added to `typing`, and -`typing_extensions` provides a backport even on newer Python versions: - -- `TypedDict` does not store runtime information - about which (if any) keys are non-required in Python 3.8, and does not - honor the `total` keyword with old-style `TypedDict()` in Python - 3.9.0 and 3.9.1. `TypedDict` also does not support multiple inheritance - with `typing.Generic` on Python <3.11, and `TypedDict` classes do not - consistently have the `__orig_bases__` attribute on Python <3.12. The - `typing_extensions` backport provides all of these features and bugfixes on - all Python versions. -- `get_origin` and `get_args` lack support for `Annotated` in - Python 3.8 and lack support for `ParamSpecArgs` and `ParamSpecKwargs` - in 3.9. -- `@final` was changed in Python 3.11 to set the `.__final__` attribute. -- `@overload` was changed in Python 3.11 to make function overloads - introspectable at runtime. In order to access overloads with - `typing_extensions.get_overloads()`, you must use - `@typing_extensions.overload`. -- `NamedTuple` was changed in Python 3.11 to allow for multiple inheritance - with `typing.Generic`. Call-based `NamedTuple`s were changed in Python 3.12 - so that they have an `__orig_bases__` attribute, the same as class-based - `NamedTuple`s. -- Since Python 3.11, it has been possible to inherit from `Any` at - runtime. `typing_extensions.Any` also provides this capability. -- `TypeVar` gains two additional parameters, `default=` and `infer_variance=`, - in the draft PEPs [695](https://peps.python.org/pep-0695/) and [696](https://peps.python.org/pep-0696/), which are being considered for inclusion - in Python 3.12. -- `Protocol` was added in Python 3.8, but several bugfixes have been made in - subsequent releases, as well as significant performance improvements to - runtime-checkable protocols in Python 3.12. `typing_extensions` backports the - 3.12+ version to Python 3.7+. -- `SupportsInt`, `SupportsFloat`, `SupportsComplex`, `SupportsBytes`, - `SupportsAbs` and `SupportsRound` have always been present in the `typing` - module. Meanwhile, `SupportsIndex` was added in Python 3.8. However, - `isinstance()` checks against all these protocols were sped up significantly - on Python 3.12. `typing_extensions` backports the faster versions to Python - 3.7+. -- `Literal` does not flatten or deduplicate parameters on Python <3.9.1, and a - caching bug was fixed in 3.10.1/3.9.8. The `typing_extensions` version - flattens and deduplicates parameters on all Python versions, and the caching - bug is also fixed on all versions. -- `NewType` has been in the `typing` module since Python 3.5.2, but - user-defined `NewType`s are only pickleable on Python 3.10+. - `typing_extensions.NewType` backports this feature to all Python versions. -- `Unpack` was added in Python 3.11, but the repr was changed in Python 3.12; - `typing_extensions.Unpack` has the newer repr on all versions. - -There are a few types whose interface was modified between different -versions of typing. For example, `typing.Sequence` was modified to -subclass `typing.Reversible` as of Python 3.5.3. - -These changes are _not_ backported to prevent subtle compatibility -issues when mixing the differing implementations of modified classes. - -Certain types have incorrect runtime behavior due to limitations of older -versions of the typing module: - -- `ParamSpec` and `Concatenate` will not work with `get_args` and - `get_origin`. Certain [PEP 612](https://peps.python.org/pep-0612/) special cases in user-defined - `Generic`s are also not available. - -These types are only guaranteed to work for static type checking. +See [the documentation](https://typing-extensions.readthedocs.io/en/latest/#) for a +complete listing of module contents. ## Running tests -To run tests, navigate into the appropriate source directory and run +To run tests, navigate into the `src/` directory and run `test_typing_extensions.py`. diff --git a/pyproject.toml b/pyproject.toml index 67d81a17..aec54d3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ classifiers = [ Home = "https://github.com/python/typing_extensions" Repository = "https://github.com/python/typing_extensions" Changes = "https://github.com/python/typing_extensions/blob/main/CHANGELOG.md" -Documentation = "https://typing.readthedocs.io/" +Documentation = "https://typing-extensions.readthedocs.io/" "Bug Tracker" = "https://github.com/python/typing_extensions/issues" "Q & A" = "https://github.com/python/typing/discussions" From f2fc4cb99a2980a228b99e3b447da09be0d31942 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 21 May 2023 17:34:56 -0700 Subject: [PATCH 40/46] Add references to additional known limitations (#171) --- doc/index.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/index.rst b/doc/index.rst index 4334414a..dc482515 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -151,7 +151,7 @@ Special typing primitives On older Python versions, ``typing_extensions.ParamSpec`` may not work correctly with introspection tools like :func:`get_args` and :func:`get_origin`. Certain special cases in user-defined - :py:class:`typing.Generic`\ s are also not available. + :py:class:`typing.Generic`\ s are also not available (e.g., see :issue:`126`). .. versionchanged:: 4.4.0 @@ -285,6 +285,9 @@ Special typing primitives In Python 3.12, the ``repr()`` was changed as a result of :pep:`692`. ``typing_extensions`` backports this change. + Generic type aliases involving ``Unpack`` may not work correctly on + Python 3.10 and lower; see :issue:`103` for details. + .. versionadded:: 4.1.0 .. versionchanged:: 4.6.0 From 9648c6ffb2a3fa07907d853c4d74d4894e9dd1ad Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 21 May 2023 17:49:09 -0700 Subject: [PATCH 41/46] add infer_variance for ParamSpec (#172) For compatibility with Python 3.12, which adds this undocumented parameter along with the existing covariant/contravariant ones. Intentionally leaving this undocumented: it's here in case we want to make variance on ParamSpec mean something later, but it currently has no supported use. --- src/test_typing_extensions.py | 26 +++++++++++++++++++++++++- src/typing_extensions.py | 23 ++++++++++++++++++----- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 8d0ed9df..882b4500 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -3314,6 +3314,7 @@ def test_repr(self): P = ParamSpec('P') P_co = ParamSpec('P_co', covariant=True) P_contra = ParamSpec('P_contra', contravariant=True) + P_infer = ParamSpec('P_infer', infer_variance=True) P_2 = ParamSpec('P_2') self.assertEqual(repr(P), '~P') self.assertEqual(repr(P_2), '~P_2') @@ -3322,6 +3323,30 @@ def test_repr(self): # just follow CPython. self.assertEqual(repr(P_co), '+P_co') self.assertEqual(repr(P_contra), '-P_contra') + # On other versions we use typing.ParamSpec, but it is not aware of + # infer_variance=. Not worth creating our own version of ParamSpec + # for this. + if hasattr(typing, 'TypeAliasType') or not hasattr(typing, 'ParamSpec'): + self.assertEqual(repr(P_infer), 'P_infer') + else: + self.assertEqual(repr(P_infer), '~P_infer') + + def test_variance(self): + P_co = ParamSpec('P_co', covariant=True) + P_contra = ParamSpec('P_contra', contravariant=True) + P_infer = ParamSpec('P_infer', infer_variance=True) + + self.assertIs(P_co.__covariant__, True) + self.assertIs(P_co.__contravariant__, False) + self.assertIs(P_co.__infer_variance__, False) + + self.assertIs(P_contra.__covariant__, False) + self.assertIs(P_contra.__contravariant__, True) + self.assertIs(P_contra.__infer_variance__, False) + + self.assertIs(P_infer.__covariant__, False) + self.assertIs(P_infer.__contravariant__, False) + self.assertIs(P_infer.__infer_variance__, True) def test_valid_uses(self): P = ParamSpec('P') @@ -3333,7 +3358,6 @@ def test_valid_uses(self): self.assertEqual(C2.__args__, (P, T)) self.assertEqual(C2.__parameters__, (P, T)) - # Test collections.abc.Callable too. if sys.version_info[:2] >= (3, 9): # Note: no tests for Callable.__parameters__ here diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 93a0dca6..30255a00 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1475,9 +1475,19 @@ class _ParamSpecMeta(_TypeVarLikeMeta): def __call__(self, name, *, bound=None, covariant=False, contravariant=False, - default=_marker): - paramspec = typing.ParamSpec(name, bound=bound, - covariant=covariant, contravariant=contravariant) + infer_variance=False, default=_marker): + if hasattr(typing, "TypeAliasType"): + # PEP 695 implemented, can pass infer_variance to typing.TypeVar + paramspec = typing.ParamSpec(name, bound=bound, + covariant=covariant, + contravariant=contravariant, + infer_variance=infer_variance) + else: + paramspec = typing.ParamSpec(name, bound=bound, + covariant=covariant, + contravariant=contravariant) + paramspec.__infer_variance__ = infer_variance + _set_default(paramspec, default) _set_module(paramspec) return paramspec @@ -1551,11 +1561,12 @@ def kwargs(self): return ParamSpecKwargs(self) def __init__(self, name, *, bound=None, covariant=False, contravariant=False, - default=_marker): + infer_variance=False, default=_marker): super().__init__([self]) self.__name__ = name self.__covariant__ = bool(covariant) self.__contravariant__ = bool(contravariant) + self.__infer_variance__ = bool(infer_variance) if bound: self.__bound__ = typing._type_check(bound, 'Bound must be a type.') else: @@ -1568,7 +1579,9 @@ def __init__(self, name, *, bound=None, covariant=False, contravariant=False, self.__module__ = def_mod def __repr__(self): - if self.__covariant__: + if self.__infer_variance__: + prefix = '' + elif self.__covariant__: prefix = '+' elif self.__contravariant__: prefix = '-' From 88be907eebd0dcfb21f82254c9d80ae165278b73 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 22 May 2023 13:45:06 +0100 Subject: [PATCH 42/46] Improve the repr() of `_marker` (#174) --- src/typing_extensions.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 30255a00..a30c01b1 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -95,7 +95,13 @@ # The functions below are modified copies of typing internal helpers. # They are needed by _ProtocolMeta and they provide support for PEP 646. -_marker = object() + +class _Sentinel: + def __repr__(self): + return "" + + +_marker = _Sentinel() def _check_generic(cls, parameters, elen=_marker): From bbfd0ccbe2265d771c6e7ec8454e5bbe051efd79 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 22 May 2023 08:01:09 -0700 Subject: [PATCH 43/46] Extend docs intro (#168) --- doc/index.rst | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index dc482515..b38e6477 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -19,9 +19,17 @@ repository. If the PEP is accepted, the feature will then be added to the affected ``typing_extensions`` has been rejected so far, so we haven't yet figured out how to deal with that possibility. +Bugfixes and new typing features that don't require a PEP may be added to +``typing_extensions`` once they are merged into CPython's main branch. + +Versioning and backwards compatibility +-------------------------------------- + Starting with version 4.0.0, ``typing_extensions`` uses -`Semantic Versioning `_. The -major version is incremented for all backwards-incompatible changes. +`Semantic Versioning `_. A changelog is +maintained `on GitHub `_. + +The major version is incremented for all backwards-incompatible changes. Therefore, it's safe to depend on ``typing_extensions`` like this: ``typing_extensions >=x.y, <(x+1)``, where ``x.y`` is the first version that includes all features you need. @@ -29,7 +37,15 @@ In view of the wide usage of ``typing_extensions`` across the ecosystem, we are highly hesitant to break backwards compatibility, and we do not expect to increase the major version number in the foreseeable future. -``typing_extensions`` supports Python versions 3.7 and higher. In the future, +Before version 4.0.0, the versioning scheme loosely followed the Python +version from which features were backported; for example, +``typing_extensions`` 3.10.0.0 was meant to reflect ``typing`` as of +Python 3.10.0. During this period, no changelog was maintained. + +Python version support +---------------------- + +``typing_extensions`` currently supports Python versions 3.7 and higher. In the future, support for older Python versions will be dropped some time after that version reaches end of life. From 773090f43cb4684fda67e3fcd0badf7abcd10ba9 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 22 May 2023 15:48:00 -0700 Subject: [PATCH 44/46] Remove __module__ assignment for TypeVar and friends (#175) --- src/typing_extensions.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index a30c01b1..00644d58 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1372,10 +1372,6 @@ class _DefaultMixin: class _TypeVarLikeMeta(type): - def __init__(cls, *args, **kwargs): - super().__init__(*args, **kwargs) - cls.__module__ = 'typing' - def __instancecheck__(cls, __instance: Any) -> bool: return isinstance(__instance, cls._backported_typevarlike) From 8054a2945e48fc84263190d29c2b49b1e096b7ce Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 23 May 2023 00:21:02 +0100 Subject: [PATCH 45/46] Further simplify the implementations of the TypeVarLikes (#176) --- src/typing_extensions.py | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 00644d58..78665556 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1371,18 +1371,21 @@ class _DefaultMixin: __init__ = _set_default +# Classes using this metaclass must provide a _backported_typevarlike ClassVar class _TypeVarLikeMeta(type): def __instancecheck__(cls, __instance: Any) -> bool: return isinstance(__instance, cls._backported_typevarlike) # Add default and infer_variance parameters from PEP 696 and 695 -class _TypeVarMeta(_TypeVarLikeMeta): +class TypeVar(metaclass=_TypeVarLikeMeta): + """Type variable.""" + _backported_typevarlike = typing.TypeVar - def __call__(self, name, *constraints, bound=None, - covariant=False, contravariant=False, - default=_marker, infer_variance=False): + def __new__(cls, name, *constraints, bound=None, + covariant=False, contravariant=False, + default=_marker, infer_variance=False): if hasattr(typing, "TypeAliasType"): # PEP 695 implemented, can pass infer_variance to typing.TypeVar typevar = typing.TypeVar(name, *constraints, bound=bound, @@ -1398,10 +1401,6 @@ def __call__(self, name, *constraints, bound=None, _set_module(typevar) return typevar - -class TypeVar(metaclass=_TypeVarMeta): - """Type variable.""" - def __init_subclass__(cls) -> None: raise TypeError(f"type '{__name__}.TypeVar' is not an acceptable base type") @@ -1472,12 +1471,14 @@ def __eq__(self, other): if hasattr(typing, 'ParamSpec'): # Add default parameter - PEP 696 - class _ParamSpecMeta(_TypeVarLikeMeta): + class ParamSpec(metaclass=_TypeVarLikeMeta): + """Parameter specification.""" + _backported_typevarlike = typing.ParamSpec - def __call__(self, name, *, bound=None, - covariant=False, contravariant=False, - infer_variance=False, default=_marker): + def __new__(cls, name, *, bound=None, + covariant=False, contravariant=False, + infer_variance=False, default=_marker): if hasattr(typing, "TypeAliasType"): # PEP 695 implemented, can pass infer_variance to typing.TypeVar paramspec = typing.ParamSpec(name, bound=bound, @@ -1494,9 +1495,6 @@ def __call__(self, name, *, bound=None, _set_module(paramspec) return paramspec - class ParamSpec(metaclass=_ParamSpecMeta): - """Parameter specification.""" - def __init_subclass__(cls) -> None: raise TypeError(f"type '{__name__}.ParamSpec' is not an acceptable base type") @@ -2105,18 +2103,17 @@ def _is_unpack(obj): if hasattr(typing, "TypeVarTuple"): # 3.11+ # Add default parameter - PEP 696 - class _TypeVarTupleMeta(_TypeVarLikeMeta): + class TypeVarTuple(metaclass=_TypeVarLikeMeta): + """Type variable tuple.""" + _backported_typevarlike = typing.TypeVarTuple - def __call__(self, name, *, default=_marker): + def __new__(cls, name, *, default=_marker): tvt = typing.TypeVarTuple(name) _set_default(tvt, default) _set_module(tvt) return tvt - class TypeVarTuple(metaclass=_TypeVarTupleMeta): - """Type variable tuple.""" - def __init_subclass__(self, *args, **kwds): raise TypeError("Cannot subclass special typing classes") From 356934ca69a223416a199c2b26c19315382738db Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 22 May 2023 17:06:45 -0700 Subject: [PATCH 46/46] Prepare release 4.6.0 (#177) --- CHANGELOG.md | 4 +++- CONTRIBUTING.md | 4 ++-- pyproject.toml | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3247b46..74381f8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ -# Unreleased +# Release 4.6.0 (May 22, 2023) +- `typing_extensions` is now documented at + https://typing-extensions.readthedocs.io/en/latest/. Patch by Jelle Zijlstra. - Add `typing_extensions.Buffer`, a marker class for buffer types, as proposed by PEP 688. Equivalent to `collections.abc.Buffer` in Python 3.12. Patch by Jelle Zijlstra. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a65feb4f..2585ac70 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,8 +15,8 @@ time, this may require different code for some older Python versions. `typing_extensions` may also include experimental features that are not yet part of the standard library, so that users can experiment with them before they are added to the -standard library. Such features should ideally already be specified in a PEP or draft -PEP. +standard library. Such features should already be specified in a PEP or merged into +CPython's `main` branch. `typing_extensions` supports Python versions 3.7 and up. diff --git a/pyproject.toml b/pyproject.toml index aec54d3a..03cdf746 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.5.0" +version = "4.6.0" description = "Backported and Experimental Type Hints for Python 3.7+" readme = "README.md" requires-python = ">=3.7"