From b408fa0a3e8c4dde863c0eb182a2f938271b6c38 Mon Sep 17 00:00:00 2001 From: youknowdot Date: Sun, 18 Oct 2026 12:41:06 +0900 Subject: [PATCH] Align regex, dataclasses and typing with Python 3.15 Expose Pattern.prefixmatch and Scanner.prefixmatch and refresh their canonical re consumers, dataclasses annotation handling, typing APIs and regression fixtures from CPython d220409a13b77de7a6c30abe16ea90db01528edc. Assisted-by: OpenAI Codex:model version unavailable --- Lib/dataclasses.py | 169 ++--- Lib/re/__init__.py | 54 +- Lib/re/_compiler.py | 2 +- Lib/re/_parser.py | 1 - Lib/test/test_dataclasses/__init__.py | 73 +- Lib/test/test_dataclasses/_types_proxy.py | 8 + .../test_dataclasses/dataclass_module_3.py | 32 + .../dataclass_module_3_str.py | 32 + .../test_dataclasses/dataclass_module_4.py | 38 ++ .../dataclass_module_4_str.py | 38 ++ Lib/test/test_re.py | 79 ++- Lib/test/test_type_aliases.py | 58 +- Lib/test/test_typing.py | 645 +++++++++++++----- Lib/typing.py | 439 +++++++----- crates/vm/src/stdlib/_sre.rs | 2 + 15 files changed, 1176 insertions(+), 494 deletions(-) create mode 100644 Lib/test/test_dataclasses/_types_proxy.py create mode 100644 Lib/test/test_dataclasses/dataclass_module_3.py create mode 100644 Lib/test/test_dataclasses/dataclass_module_3_str.py create mode 100644 Lib/test/test_dataclasses/dataclass_module_4.py create mode 100644 Lib/test/test_dataclasses/dataclass_module_4_str.py diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 242a6d2ffc2..863e4101f98 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1,13 +1,12 @@ -import re import sys -import copy import types -import inspect import keyword import itertools import annotationlib import abc from reprlib import recursive_repr +lazy import copy +lazy import re __all__ = ['dataclass', @@ -178,21 +177,16 @@ def __repr__(self): return '' _HAS_DEFAULT_FACTORY = _HAS_DEFAULT_FACTORY_CLASS() -# A sentinel object to detect if a parameter is supplied or not. Use -# a class to give it a better repr. -class _MISSING_TYPE: - pass -MISSING = _MISSING_TYPE() +# A sentinel object to detect if a parameter is supplied or not. +MISSING = sentinel("MISSING") # A sentinel object to indicate that following fields are keyword-only by -# default. Use a class to give it a better repr. -class _KW_ONLY_TYPE: - pass -KW_ONLY = _KW_ONLY_TYPE() +# default. +KW_ONLY = sentinel("KW_ONLY") # Since most per-field metadata will be unused, create an empty -# read-only proxy that can be shared among all fields. -_EMPTY_METADATA = types.MappingProxyType({}) +# read-only dictionary that can be shared among all fields. +_EMPTY_METADATA = frozendict() # Markers for the various kinds of fields and pseudo-fields. class _FIELD_BASE: @@ -217,9 +211,8 @@ def __repr__(self): _POST_INIT_NAME = '__post_init__' # String regex that string annotations for ClassVar or InitVar must match. -# Allows "identifier.identifier[" or "identifier[". -# https://bugs.python.org/issue33453 for details. -_MODULE_IDENTIFIER_RE = re.compile(r'^(?:\s*(\w+)\s*\.)?\s*(\w+)') +# This regular expression is compiled on demand so that 're' module can be imported lazily +_MODULE_IDENTIFIER_RE = None # Atomic immutable types which don't require any recursive handling and for which deepcopy # returns the same object. We can provide a fast-path for these types in asdict and astuple. @@ -763,22 +756,16 @@ def _is_kw_only(a_type, dataclasses): return a_type is dataclasses.KW_ONLY -def _is_type(annotation, cls, a_module, a_type, is_type_predicate): - # Given a type annotation string, does it refer to a_type in - # a_module? For example, when checking that annotation denotes a - # ClassVar, then a_module is typing, and a_type is - # typing.ClassVar. +def _get_type_from_annotation(annotation, cls): + # Loosely parse a string annotation and return its type. - # It's possible to look up a_module given a_type, but it involves - # looking in sys.modules (again!), and seems like a waste since - # the caller already knows a_module. + # We can't perform a full type hint evaluation at the point where @dataclass + # was invoked because class's module is not fully initialized yet. So we resort + # to parsing string annotation using regexp, and extracting a type before + # the first square bracket. # - annotation is a string type annotation # - cls is the class that this annotation was found in - # - a_module is the module we want to match - # - a_type is the type in that module we want to match - # - is_type_predicate is a function called with (obj, a_module) - # that determines if obj is of the desired type. # Since this test does not do a local namespace lookup (and # instead only a module (global) lookup), there are some things it @@ -804,22 +791,26 @@ def _is_type(annotation, cls, a_module, a_type, is_type_predicate): # a eval() penalty for every single field of every dataclass # that's defined. It was judged not worth it. - match = _MODULE_IDENTIFIER_RE.match(annotation) - if match: - ns = None - module_name = match.group(1) - if not module_name: - # No module name, assume the class's module did - # "from dataclasses import InitVar". - ns = sys.modules.get(cls.__module__).__dict__ - else: - # Look up module_name in the class's module. - module = sys.modules.get(cls.__module__) - if module and module.__dict__.get(module_name) is a_module: - ns = sys.modules.get(a_type.__module__).__dict__ - if ns and is_type_predicate(ns.get(match.group(2)), a_module): - return True - return False + # String regex that string annotations for ClassVar or InitVar must match. + # Allows "identifier.identifier[" or "identifier[". + # https://github.com/python/cpython/issues/77634 for details. + global _MODULE_IDENTIFIER_RE + if _MODULE_IDENTIFIER_RE is None: + _MODULE_IDENTIFIER_RE = re.compile(r'^\s*(\w+(?:\s*\.\s*\w+)*)') + + match = _MODULE_IDENTIFIER_RE.prefixmatch(annotation) + if not match: + return None + + # Note: _MODULE_IDENTIFIER_RE guarantees that path is non-empty + path = match[1].split(".") + root = sys.modules.get(cls.__module__) + for path_item in path: + root = getattr(root, path_item.strip(), None) + if root is None: + return None + + return root def _get_field(cls, a_name, a_type, default_kw_only): @@ -857,6 +848,10 @@ def _get_field(cls, a_name, a_type, default_kw_only): # is actually of the correct type. # For the complete discussion, see https://bugs.python.org/issue33453 + if isinstance(a_type, str): + a_type_annotation = _get_type_from_annotation(a_type, cls) + else: + a_type_annotation = a_type # If typing has not been imported, then it's impossible for any # annotation to be a ClassVar. So, only look for ClassVar if @@ -864,10 +859,7 @@ def _get_field(cls, a_name, a_type, default_kw_only): # module). typing = sys.modules.get('typing') if typing: - if (_is_classvar(a_type, typing) - or (isinstance(f.type, str) - and _is_type(f.type, cls, typing, typing.ClassVar, - _is_classvar))): + if _is_classvar(a_type_annotation, typing): f._field_type = _FIELD_CLASSVAR # If the type is InitVar, or if it's a matching string annotation, @@ -876,10 +868,7 @@ def _get_field(cls, a_name, a_type, default_kw_only): # The module we're checking against is the module we're # currently in (dataclasses.py). dataclasses = sys.modules[__name__] - if (_is_initvar(a_type, dataclasses) - or (isinstance(f.type, str) - and _is_type(f.type, cls, dataclasses, dataclasses.InitVar, - _is_initvar))): + if _is_initvar(a_type_annotation, dataclasses): f._field_type = _FIELD_INITVAR # Validations for individual fields. This is delayed until now, @@ -982,6 +971,29 @@ def _hash_exception(cls, fields, func_builder): # See https://bugs.python.org/issue32929#msg312829 for an if-statement # version of this table. +# A non-data descriptor to autogenerate class docstring +# from the signature of its __init__ method on demand. +# The primary reason is to be able to lazy import `inspect` module. +class _AutoDocstring: + + def __get__(self, _obj, cls): + try: + # In some cases fetching a signature is not possible. + # But, we surely should not fail in this case. + import inspect + text_sig = str(inspect.signature( + cls, + annotation_format=annotationlib.Format.FORWARDREF, + )).replace(' -> None', '') + except TypeError, ValueError: + text_sig = '' + + doc = cls.__name__ + text_sig + setattr(cls, '__doc__', doc) + return doc + +_auto_docstring = _AutoDocstring() + def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, match_args, kw_only, slots, weakref_slot): @@ -1050,10 +1062,11 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, dataclasses = sys.modules[__name__] for name, type in cls_annotations.items(): # See if this is a marker to change the value of kw_only. - if (_is_kw_only(type, dataclasses) - or (isinstance(type, str) - and _is_type(type, cls, dataclasses, dataclasses.KW_ONLY, - _is_kw_only))): + if isinstance(type, str): + a_type_annotation = _get_type_from_annotation(type, cls) + else: + a_type_annotation = type + if _is_kw_only(a_type_annotation, dataclasses): # Switch the default to kw_only=True, and ignore this # annotation: it's not a real field. if KW_ONLY_seen: @@ -1209,23 +1222,13 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, if hash_action: cls.__hash__ = hash_action(cls, field_list, func_builder) - # Generate the methods and add them to the class. This needs to be done - # before the __doc__ logic below, since inspect will look at the __init__ - # signature. + # Generate the methods and add them to the class. func_builder.add_fns_to_class(cls) if not getattr(cls, '__doc__'): - # Create a class doc-string. - try: - # In some cases fetching a signature is not possible. - # But, we surely should not fail in this case. - text_sig = str(inspect.signature( - cls, - annotation_format=annotationlib.Format.FORWARDREF, - )).replace(' -> None', '') - except (TypeError, ValueError): - text_sig = '' - cls.__doc__ = (cls.__name__ + text_sig) + # Create a class doc-string lazily via descriptor protocol + # to avoid importing `inspect` module. + cls.__doc__ = _auto_docstring if match_args: # I could probably compute this once. @@ -1344,10 +1347,6 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields): if '__slots__' in cls.__dict__: raise TypeError(f'{cls.__name__} already specifies __slots__') - # gh-102069: Remove existing __weakref__ descriptor. - # gh-135228: Make sure the original class can be garbage collected. - sys._clear_type_descriptors(cls) - # Create a new dict for our new class. cls_dict = dict(cls.__dict__) field_names = tuple(f.name for f in fields(cls)) @@ -1365,6 +1364,11 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields): # available in _MARKER. cls_dict.pop(field_name, None) + # Remove __dict__ and `__weakref__` descriptors. + # They'll be added back if applicable. + cls_dict.pop('__dict__', None) + cls_dict.pop('__weakref__', None) # gh-102069 + # And finally create the class. qualname = getattr(cls, '__qualname__', None) newcls = type(cls)(cls.__name__, cls.__bases__, cls_dict) @@ -1385,8 +1389,11 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields): # make an update, since all closures for a class will share a # given cell. for member in newcls.__dict__.values(): + # If this is a wrapped function, unwrap it. - member = inspect.unwrap(member) + if not isinstance(member, type) and hasattr(member, '__wrapped__'): + import inspect + member = inspect.unwrap(member) if isinstance(member, types.FunctionType): if _update_func_cell_for__class__(member, cls, newcls): @@ -1498,7 +1505,8 @@ class C: If given, 'dict_factory' will be used instead of built-in dict. The function applies recursively to field values that are dataclass instances. This will also look into built-in containers: - tuples, lists, and dicts. Other objects are copied with 'copy.deepcopy()'. + tuples, lists, dicts, and frozendicts. Other objects are copied + with 'copy.deepcopy()'. """ if not _is_dataclass_instance(obj): raise TypeError("asdict() should be called on dataclass instances") @@ -1554,7 +1562,7 @@ def _asdict_inner(obj, dict_factory): return obj_type(*[_asdict_inner(v, dict_factory) for v in obj]) else: return obj_type(_asdict_inner(v, dict_factory) for v in obj) - elif issubclass(obj_type, dict): + elif issubclass(obj_type, (dict, frozendict)): if hasattr(obj_type, 'default_factory'): # obj is a defaultdict, which has a different constructor from # dict as it requires the default_factory as its first arg. @@ -1589,7 +1597,8 @@ class C: If given, 'tuple_factory' will be used instead of built-in tuple. The function applies recursively to field values that are dataclass instances. This will also look into built-in containers: - tuples, lists, and dicts. Other objects are copied with 'copy.deepcopy()'. + tuples, lists, dicts, and frozendicts. Other objects are copied + with 'copy.deepcopy()'. """ if not _is_dataclass_instance(obj): @@ -1618,7 +1627,7 @@ def _astuple_inner(obj, tuple_factory): # generator (which is not true for namedtuples, handled # above). return type(obj)(_astuple_inner(v, tuple_factory) for v in obj) - elif isinstance(obj, dict): + elif isinstance(obj, (dict, frozendict)): obj_type = type(obj) if hasattr(obj_type, 'default_factory'): # obj is a defaultdict, which has a different constructor from diff --git a/Lib/re/__init__.py b/Lib/re/__init__.py index af2808a77da..e6d29fd7a40 100644 --- a/Lib/re/__init__.py +++ b/Lib/re/__init__.py @@ -85,17 +85,18 @@ \\ Matches a literal backslash. This module exports the following functions: - match Match a regular expression pattern to the beginning of a string. - fullmatch Match a regular expression pattern to all of a string. - search Search a string for the presence of a pattern. - sub Substitute occurrences of a pattern found in a string. - subn Same as sub, but also return the number of substitutions made. - split Split a string by the occurrences of a pattern. - findall Find all occurrences of a pattern in a string. - finditer Return an iterator yielding a Match object for each match. - compile Compile a pattern into a Pattern object. - purge Clear the regular expression cache. - escape Backslash all non-alphanumerics in a string. + prefixmatch Match a regular expression pattern to the beginning of a str. + match The original name of prefixmatch prior to 3.15. + fullmatch Match a regular expression pattern to all of a string. + search Search a string for the presence of a pattern. + sub Substitute occurrences of a pattern found in a string. + subn Same as sub, but also return the number of substitutions made. + split Split a string by the occurrences of a pattern. + findall Find all occurrences of a pattern in a string. + finditer Return an iterator yielding a Match object for each match. + compile Compile a pattern into a Pattern object. + purge Clear the regular expression cache. + escape Backslash all non-alphanumerics in a string. Each function other than purge and escape can take an optional 'flags' argument consisting of one or more of the following module constants, joined by "|". @@ -130,15 +131,13 @@ # public symbols __all__ = [ - "match", "fullmatch", "search", "sub", "subn", "split", + "prefixmatch", "match", "fullmatch", "search", "sub", "subn", "split", "findall", "finditer", "compile", "purge", "escape", "error", "Pattern", "Match", "A", "I", "L", "M", "S", "X", "U", "ASCII", "IGNORECASE", "LOCALE", "MULTILINE", "DOTALL", "VERBOSE", "UNICODE", "NOFLAG", "RegexFlag", "PatternError" ] -__version__ = "2.2.1" - @enum.global_enum @enum._simple_enum(enum.IntFlag, boundary=enum.KEEP) class RegexFlag: @@ -161,10 +160,13 @@ class RegexFlag: # -------------------------------------------------------------------- # public interface -def match(pattern, string, flags=0): +def prefixmatch(pattern, string, flags=0): """Try to apply the pattern at the start of the string, returning a Match object, or None if no match was found.""" - return _compile(pattern, flags).match(string) + return _compile(pattern, flags).prefixmatch(string) + +# Our original name which was less explicitly clear about the behavior for prefixmatch. +match = prefixmatch def fullmatch(pattern, string, flags=0): """Try to apply the pattern to all of the string, returning @@ -313,7 +315,7 @@ def escape(pattern): return pattern.translate(_special_chars_map).encode('latin1') Pattern = type(_compiler.compile('', 0)) -Match = type(_compiler.compile('', 0).match('')) +Match = type(_compiler.compile('', 0).prefixmatch('')) # -------------------------------------------------------------------- # internals @@ -399,9 +401,12 @@ def __init__(self, lexicon, flags=0): s = _parser.State() s.flags = flags for phrase, action in lexicon: + sub_pattern = _parser.parse(phrase, flags) + if sub_pattern.state.groups != 1: + raise ValueError("Cannot use capturing groups in re.Scanner") gid = s.opengroup() p.append(_parser.SubPattern(s, [ - (SUBPATTERN, (gid, 0, 0, _parser.parse(phrase, flags))), + (SUBPATTERN, (gid, 0, 0, sub_pattern)), ])) s.closegroup(gid, p[-1]) p = _parser.SubPattern(s, [(BRANCH, (None, p))]) @@ -409,10 +414,10 @@ def __init__(self, lexicon, flags=0): def scan(self, string): result = [] append = result.append - match = self.scanner.scanner(string).match + _match = self.scanner.scanner(string).prefixmatch i = 0 while True: - m = match() + m = _match() if not m: break j = m.end() @@ -426,3 +431,12 @@ def scan(self, string): append(action) i = j return result, string[i:] + + +def __getattr__(name): + if name == "__version__": + from warnings import _deprecated + + _deprecated("__version__", remove=(3, 20)) + return "2.2.1" # Do not change + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/Lib/re/_compiler.py b/Lib/re/_compiler.py index 20dd561d1c1..c2ca8e25abe 100644 --- a/Lib/re/_compiler.py +++ b/Lib/re/_compiler.py @@ -375,7 +375,7 @@ def _optimize_charset(charset, iscased=None, fixup=None, fixes=None): # less significant byte is a bit index in the chunk (just like the # CHARSET matching). - charmap = bytes(charmap) # should be hashable + charmap = charmap.take_bytes() # should be hashable comps = {} mapping = bytearray(256) block = 0 diff --git a/Lib/re/_parser.py b/Lib/re/_parser.py index 35ab7ede2a7..bd189fe0695 100644 --- a/Lib/re/_parser.py +++ b/Lib/re/_parser.py @@ -455,7 +455,6 @@ def _parse_sub(source, state, verbose, nested): items = [] itemsappend = items.append sourcematch = source.match - start = source.tell() while True: itemsappend(_parse(source, state, verbose, nested + 1, not nested and not items)) diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index acbbf60ef37..17dee875991 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -27,11 +27,21 @@ import dataclasses # Needed for the string "dataclasses.InitVar[int]" to work as an annotation. from test import support -from test.support import import_helper +from test.support import cpython_only, import_helper # Just any custom exception we can catch. class CustomError(Exception): pass + +class TestImportTime(unittest.TestCase): + + @cpython_only + def test_lazy_import(self): + import_helper.ensure_lazy_imports( + "dataclasses", {"inspect", "re", "copy"} + ) + + class TestCase(unittest.TestCase): def test_no_fields(self): @dataclass @@ -71,7 +81,7 @@ def test_field_repr(self): expected_output = "Field(name='id',type=None," \ f"default=1,default_factory={MISSING!r}," \ "init=True,repr=False,hash=None," \ - "compare=True,metadata=mappingproxy({})," \ + "compare=True,metadata=frozendict()," \ f"kw_only={MISSING!r}," \ "doc='Docstring'," \ "_field_type=None)" @@ -985,7 +995,7 @@ class D: self.assertNotIn('x', D.__dict__) def test_missing_repr(self): - self.assertIn('MISSING_TYPE object', repr(MISSING)) + self.assertEqual(repr(MISSING), 'MISSING') def test_dont_include_other_annotations(self): @dataclass @@ -1693,17 +1703,24 @@ class GroupTuple: class GroupDict: id: int users: Dict[str, User] + @dataclass + class GroupFrozenDict: + id: int + users: frozendict[str, User] a = User('Alice', 1) b = User('Bob', 2) gl = GroupList(0, [a, b]) gt = GroupTuple(0, (a, b)) gd = GroupDict(0, {'first': a, 'second': b}) + gfd = GroupFrozenDict(0, frozendict({'first': a, 'second': b})) self.assertEqual(asdict(gl), {'id': 0, 'users': [{'name': 'Alice', 'id': 1}, {'name': 'Bob', 'id': 2}]}) self.assertEqual(asdict(gt), {'id': 0, 'users': ({'name': 'Alice', 'id': 1}, {'name': 'Bob', 'id': 2})}) - self.assertEqual(asdict(gd), {'id': 0, 'users': {'first': {'name': 'Alice', 'id': 1}, - 'second': {'name': 'Bob', 'id': 2}}}) + expected_dict = {'id': 0, 'users': {'first': {'name': 'Alice', 'id': 1}, + 'second': {'name': 'Bob', 'id': 2}}} + self.assertEqual(asdict(gd), expected_dict) + self.assertEqual(asdict(gfd), expected_dict) def test_helper_asdict_builtin_object_containers(self): @dataclass @@ -1884,14 +1901,21 @@ class GroupTuple: class GroupDict: id: int users: Dict[str, User] + @dataclass + class GroupFrozenDict: + id: int + users: frozendict[str, User] a = User('Alice', 1) b = User('Bob', 2) gl = GroupList(0, [a, b]) gt = GroupTuple(0, (a, b)) gd = GroupDict(0, {'first': a, 'second': b}) + gfd = GroupFrozenDict(0, frozendict({'first': a, 'second': b})) self.assertEqual(astuple(gl), (0, [('Alice', 1), ('Bob', 2)])) self.assertEqual(astuple(gt), (0, (('Alice', 1), ('Bob', 2)))) - self.assertEqual(astuple(gd), (0, {'first': ('Alice', 1), 'second': ('Bob', 2)})) + d = {'first': ('Alice', 1), 'second': ('Bob', 2)} + self.assertEqual(astuple(gd), (0, d)) + self.assertEqual(astuple(gfd), (0, frozendict(d))) def test_helper_astuple_builtin_object_containers(self): @dataclass @@ -2295,6 +2319,20 @@ class C: self.assertDocStrEqual(C.__doc__, "C()") + def test_docstring_slotted(self): + @dataclass(slots=True) + class C: + x: int + + self.assertDocStrEqual(C.__doc__, "C(x:int)") + + def test_docstring_recursive(self): + @dataclass() + class C: + x: list[C] + + self.assertDocStrEqual(C.__doc__, "C(x:list[test.test_dataclasses.TestDocString.test_docstring_recursive..C])") + def test_docstring_one_field(self): @dataclass class C: @@ -4444,10 +4482,17 @@ def test_classvar_module_level_import(self): from test.test_dataclasses import dataclass_module_1_str from test.test_dataclasses import dataclass_module_2 from test.test_dataclasses import dataclass_module_2_str - - for m in (dataclass_module_1, dataclass_module_1_str, - dataclass_module_2, dataclass_module_2_str, - ): + from test.test_dataclasses import dataclass_module_3 + from test.test_dataclasses import dataclass_module_3_str + from test.test_dataclasses import dataclass_module_4 + from test.test_dataclasses import dataclass_module_4_str + + for m in ( + dataclass_module_1, dataclass_module_1_str, + dataclass_module_2, dataclass_module_2_str, + dataclass_module_3, dataclass_module_3_str, + dataclass_module_4, dataclass_module_4_str, + ): with self.subTest(m=m): # There's a difference in how the ClassVars are # interpreted when using string annotations or @@ -4751,6 +4796,14 @@ def custom_dataclass(cls, *args, **kwargs): self.assertEqual(c.x, 10) self.assertEqual(c.__custom__, True) + def test_empty_annotation_string(self): + @dataclass + class DataclassWithEmptyTypeAnnotation: + x: "" + + c = DataclassWithEmptyTypeAnnotation(10) + self.assertEqual(c.x, 10) + class TestReplace(unittest.TestCase): def test(self): diff --git a/Lib/test/test_dataclasses/_types_proxy.py b/Lib/test/test_dataclasses/_types_proxy.py new file mode 100644 index 00000000000..f4aaeef7aec --- /dev/null +++ b/Lib/test/test_dataclasses/_types_proxy.py @@ -0,0 +1,8 @@ +# We need this to test a case when a type +# is imported via some other package, +# like ClassVar from typing_extensions instead of typing. +# https://github.com/python/cpython/issues/133956 +from typing import ClassVar +from dataclasses import InitVar + +__all__ = ["ClassVar", "InitVar"] diff --git a/Lib/test/test_dataclasses/dataclass_module_3.py b/Lib/test/test_dataclasses/dataclass_module_3.py new file mode 100644 index 00000000000..74abc091f35 --- /dev/null +++ b/Lib/test/test_dataclasses/dataclass_module_3.py @@ -0,0 +1,32 @@ +#from __future__ import annotations +USING_STRINGS = False + +# dataclass_module_3.py and dataclass_module_3_str.py are identical +# except only the latter uses string annotations. + +from dataclasses import dataclass +import test.test_dataclasses._types_proxy as tp + +T_CV2 = tp.ClassVar[int] +T_CV3 = tp.ClassVar + +T_IV2 = tp.InitVar[int] +T_IV3 = tp.InitVar + +@dataclass +class CV: + T_CV4 = tp.ClassVar + cv0: tp.ClassVar[int] = 20 + cv1: tp.ClassVar = 30 + cv2: T_CV2 + cv3: T_CV3 + not_cv4: T_CV4 # When using string annotations, this field is not recognized as a ClassVar. + +@dataclass +class IV: + T_IV4 = tp.InitVar + iv0: tp.InitVar[int] + iv1: tp.InitVar + iv2: T_IV2 + iv3: T_IV3 + not_iv4: T_IV4 # When using string annotations, this field is not recognized as an InitVar. diff --git a/Lib/test/test_dataclasses/dataclass_module_3_str.py b/Lib/test/test_dataclasses/dataclass_module_3_str.py new file mode 100644 index 00000000000..49e5fca6183 --- /dev/null +++ b/Lib/test/test_dataclasses/dataclass_module_3_str.py @@ -0,0 +1,32 @@ +from __future__ import annotations +USING_STRINGS = True + +# dataclass_module_3.py and dataclass_module_3_str.py are identical +# except only the latter uses string annotations. + +from dataclasses import dataclass +import test.test_dataclasses._types_proxy as tp + +T_CV2 = tp.ClassVar[int] +T_CV3 = tp.ClassVar + +T_IV2 = tp.InitVar[int] +T_IV3 = tp.InitVar + +@dataclass +class CV: + T_CV4 = tp.ClassVar + cv0: tp.ClassVar[int] = 20 + cv1: tp.ClassVar = 30 + cv2: T_CV2 + cv3: T_CV3 + not_cv4: T_CV4 # When using string annotations, this field is not recognized as a ClassVar. + +@dataclass +class IV: + T_IV4 = tp.InitVar + iv0: tp.InitVar[int] + iv1: tp.InitVar + iv2: T_IV2 + iv3: T_IV3 + not_iv4: T_IV4 # When using string annotations, this field is not recognized as an InitVar. diff --git a/Lib/test/test_dataclasses/dataclass_module_4.py b/Lib/test/test_dataclasses/dataclass_module_4.py new file mode 100644 index 00000000000..7e0c8a18356 --- /dev/null +++ b/Lib/test/test_dataclasses/dataclass_module_4.py @@ -0,0 +1,38 @@ +#from __future__ import annotations +USING_STRINGS = False + +# dataclass_module_4.py and dataclass_module_4_str.py are identical +# except only the latter uses string annotations. + +from dataclasses import dataclass +import dataclasses +import typing + +class TypingProxy: + class Nested: + ClassVar = typing.ClassVar + InitVar = dataclasses.InitVar + +T_CV2 = TypingProxy.Nested.ClassVar[int] +T_CV3 = TypingProxy.Nested.ClassVar + +T_IV2 = TypingProxy.Nested.InitVar[int] +T_IV3 = TypingProxy.Nested.InitVar + +@dataclass +class CV: + T_CV4 = TypingProxy.Nested.ClassVar + cv0: TypingProxy.Nested.ClassVar[int] = 20 + cv1: TypingProxy.Nested.ClassVar = 30 + cv2: T_CV2 + cv3: T_CV3 + not_cv4: T_CV4 # When using string annotations, this field is not recognized as a ClassVar. + +@dataclass +class IV: + T_IV4 = TypingProxy.Nested.InitVar + iv0: TypingProxy.Nested.InitVar[int] + iv1: TypingProxy.Nested.InitVar + iv2: T_IV2 + iv3: T_IV3 + not_iv4: T_IV4 # When using string annotations, this field is not recognized as an InitVar. diff --git a/Lib/test/test_dataclasses/dataclass_module_4_str.py b/Lib/test/test_dataclasses/dataclass_module_4_str.py new file mode 100644 index 00000000000..876f3dcf7c8 --- /dev/null +++ b/Lib/test/test_dataclasses/dataclass_module_4_str.py @@ -0,0 +1,38 @@ +from __future__ import annotations +USING_STRINGS = True + +# dataclass_module_4.py and dataclass_module_4_str.py are identical +# except only the latter uses string annotations. + +from dataclasses import dataclass +import dataclasses +import typing + +class TypingProxy: + class Nested: + ClassVar = typing.ClassVar + InitVar = dataclasses.InitVar + +T_CV2 = TypingProxy.Nested.ClassVar[int] +T_CV3 = TypingProxy.Nested.ClassVar + +T_IV2 = TypingProxy.Nested.InitVar[int] +T_IV3 = TypingProxy.Nested.InitVar + +@dataclass +class CV: + T_CV4 = TypingProxy.Nested.ClassVar + cv0: TypingProxy.Nested.ClassVar[int] = 20 + cv1: TypingProxy.Nested.ClassVar = 30 + cv2: T_CV2 + cv3: T_CV3 + not_cv4: T_CV4 # When using string annotations, this field is not recognized as a ClassVar. + +@dataclass +class IV: + T_IV4 = TypingProxy.Nested.InitVar + iv0: TypingProxy.Nested.InitVar[int] + iv1: TypingProxy.Nested.InitVar + iv2: T_IV2 + iv3: T_IV3 + not_iv4: T_IV4 # When using string annotations, this field is not recognized as an InitVar. diff --git a/Lib/test/test_re.py b/Lib/test/test_re.py index 7a480a62b6c..3d703c0a8ce 100644 --- a/Lib/test/test_re.py +++ b/Lib/test/test_re.py @@ -91,10 +91,13 @@ def test_search_star_plus(self): self.assertEqual(re.search('x+', 'axx').span(), (1, 3)) self.assertIsNone(re.search('x', 'aaa')) self.assertEqual(re.match('a*', 'xxx').span(0), (0, 0)) + self.assertEqual(re.prefixmatch('a*', 'xxx').span(0), (0, 0)) self.assertEqual(re.match('a*', 'xxx').span(), (0, 0)) self.assertEqual(re.match('x*', 'xxxa').span(0), (0, 3)) + self.assertEqual(re.prefixmatch('x*', 'xxxa').span(0), (0, 3)) self.assertEqual(re.match('x*', 'xxxa').span(), (0, 3)) self.assertIsNone(re.match('a+', 'xxx')) + self.assertIsNone(re.prefixmatch('a+', 'xxx')) def test_branching(self): """Test Branching @@ -181,6 +184,7 @@ def test_bug_449000(self): def test_bug_1661(self): # Verify that flags do not get silently ignored with compiled patterns pattern = re.compile('.') + self.assertRaises(ValueError, re.prefixmatch, pattern, 'A', re.I) self.assertRaises(ValueError, re.match, pattern, 'A', re.I) self.assertRaises(ValueError, re.search, pattern, 'A', re.I) self.assertRaises(ValueError, re.findall, pattern, 'A', re.I) @@ -518,6 +522,8 @@ def test_re_match(self): self.assertEqual(re.match(b'(a)', string).group(0), b'a') self.assertEqual(re.match(b'(a)', string).group(1), b'a') self.assertEqual(re.match(b'(a)', string).group(1, 1), (b'a', b'a')) + self.assertEqual(re.prefixmatch(b'(a)', string).group(1, 1), + (b'a', b'a')) for a in ("\xe0", "\u0430", "\U0001d49c"): self.assertEqual(re.match(a, a).groups(), ()) self.assertEqual(re.match('(%s)' % a, a).groups(), (a,)) @@ -559,10 +565,8 @@ def __index__(self): self.assertEqual(m.group(2, 1), ('b', 'a')) self.assertEqual(m.group(Index(2), Index(1)), ('b', 'a')) - def test_match_getitem(self): - pat = re.compile('(?:(?Pa)|(?Pb))(?Pc)?') - - m = pat.match('a') + def do_test_match_getitem(self, match_fn): + m = match_fn('a') self.assertEqual(m['a1'], 'a') self.assertEqual(m['b2'], None) self.assertEqual(m['c3'], None) @@ -586,7 +590,7 @@ def test_match_getitem(self): with self.assertRaisesRegex(IndexError, 'no such group'): 'a1={a2}'.format_map(m) - m = pat.match('ac') + m = match_fn('ac') self.assertEqual(m['a1'], 'a') self.assertEqual(m['b2'], None) self.assertEqual(m['c3'], 'c') @@ -603,6 +607,14 @@ def test_match_getitem(self): # No len(). self.assertRaises(TypeError, len, m) + def test_match_getitem(self): + pat = re.compile('(?:(?Pa)|(?Pb))(?Pc)?') + self.do_test_match_getitem(pat.match) + + def test_prefixmatch_getitem(self): + pat = re.compile('(?:(?Pa)|(?Pb))(?Pc)?') + self.do_test_match_getitem(pat.prefixmatch) + def test_re_fullmatch(self): # Issue 16203: Proposal: add re.fullmatch() method. self.assertEqual(re.fullmatch(r"a", "a").span(), (0, 1)) @@ -1641,6 +1653,24 @@ def s_int(scanner, token): return int(token) (['sum', 'op=', 3, 'op*', 'foo', 'op+', 312.5, 'op+', 'bar'], '')) + def test_bug_gh140797(self): + # gh140797: Capturing groups are not allowed in re.Scanner + + msg = r"Cannot use capturing groups in re\.Scanner" + # Capturing group throws an error + with self.assertRaisesRegex(ValueError, msg): + Scanner([("(a)b", None)]) + + # Named Group + with self.assertRaisesRegex(ValueError, msg): + Scanner([("(?Pa)", None)]) + + # Non-capturing groups should pass normally + s = Scanner([("(?:a)b", lambda scanner, token: token)]) + result, rem = s.scan("ab") + self.assertEqual(result,['ab']) + self.assertEqual(rem,'') + def test_bug_448951(self): # bug 448951 (similar to 429357, but with single char match) # (Also test greedy matches.) @@ -2028,7 +2058,7 @@ def test_large_search(self, size): # The huge memuse is because of re.sub() using a list and a join() # to create the replacement result. - @bigmemtest(size=_2G, memuse=16 + 2) + @bigmemtest(size=_2G, memuse=16 + 3) def test_large_subn(self, size): # Issue #10182: indices were 32-bit-truncated. s = 'a' * size @@ -2938,33 +2968,6 @@ def test_disallow_instantiation(self): pat = re.compile("") check_disallow_instantiation(self, type(pat.scanner(""))) - def test_deprecated_modules(self): - deprecated = { - 'sre_compile': ['compile', 'error', - 'SRE_FLAG_IGNORECASE', 'SUBPATTERN', - '_compile_info'], - 'sre_constants': ['error', 'SRE_FLAG_IGNORECASE', 'SUBPATTERN', - '_NamedIntConstant'], - 'sre_parse': ['SubPattern', 'parse', - 'SRE_FLAG_IGNORECASE', 'SUBPATTERN', - '_parse_sub'], - } - for name in deprecated: - with self.subTest(module=name): - sys.modules.pop(name, None) - with self.assertWarns(DeprecationWarning) as w: - __import__(name) - self.assertEqual(str(w.warning), - f"module {name!r} is deprecated") - self.assertEqual(w.filename, __file__) - self.assertIn(name, sys.modules) - mod = sys.modules[name] - self.assertEqual(mod.__name__, name) - self.assertEqual(mod.__package__, '') - for attr in deprecated[name]: - self.assertHasAttr(mod, attr) - del sys.modules[name] - @cpython_only def test_case_helpers(self): import _sre @@ -3150,5 +3153,15 @@ def test_re_tests(self): self.assertTrue(obj.search(s)) +class TestModule(unittest.TestCase): + def test_deprecated__version__(self): + with self.assertWarnsRegex( + DeprecationWarning, + "'__version__' is deprecated and slated for removal in Python 3.20", + ) as cm: + getattr(re, "__version__") + self.assertEqual(cm.filename, __file__) + + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_type_aliases.py b/Lib/test/test_type_aliases.py index 7deca5779ea..05bc6a80888 100644 --- a/Lib/test/test_type_aliases.py +++ b/Lib/test/test_type_aliases.py @@ -8,6 +8,11 @@ Callable, TypeAliasType, TypeVar, TypeVarTuple, ParamSpec, Unpack, get_args, ) +type GlobalTypeAlias = int + +def get_type_alias(): + type TypeAliasInFunc = str + return TypeAliasInFunc class TypeParamsInvalidTest(unittest.TestCase): def test_name_collisions(self): @@ -70,6 +75,8 @@ def inner[B](self): class TypeParamsAliasValueTest(unittest.TestCase): + type TypeAliasInClass = dict + def test_alias_value_01(self): type TA1 = int @@ -142,6 +149,38 @@ def test_subscripting(self): self.assertIs(specialized2.__origin__, VeryGeneric) self.assertEqual(specialized2.__args__, (int, str, float, [bool, range])) + def test___name__(self): + type TypeAliasLocal = GlobalTypeAlias + + self.assertEqual(GlobalTypeAlias.__name__, 'GlobalTypeAlias') + self.assertEqual(get_type_alias().__name__, 'TypeAliasInFunc') + self.assertEqual(self.TypeAliasInClass.__name__, 'TypeAliasInClass') + self.assertEqual(TypeAliasLocal.__name__, 'TypeAliasLocal') + + with self.assertRaisesRegex( + AttributeError, + "readonly attribute", + ): + setattr(TypeAliasLocal, '__name__', 'TA') + + def test___qualname__(self): + type TypeAliasLocal = GlobalTypeAlias + + self.assertEqual(GlobalTypeAlias.__qualname__, + 'GlobalTypeAlias') + self.assertEqual(get_type_alias().__qualname__, + 'get_type_alias..TypeAliasInFunc') + self.assertEqual(self.TypeAliasInClass.__qualname__, + 'TypeParamsAliasValueTest.TypeAliasInClass') + self.assertEqual(TypeAliasLocal.__qualname__, + 'TypeParamsAliasValueTest.test___qualname__..TypeAliasLocal') + + with self.assertRaisesRegex( + AttributeError, + "readonly attribute", + ): + setattr(TypeAliasLocal, '__qualname__', 'TA') + def test_repr(self): type Simple = int self.assertEqual(repr(Simple), Simple.__qualname__) @@ -195,15 +234,25 @@ class TypeAliasConstructorTest(unittest.TestCase): def test_basic(self): TA = TypeAliasType("TA", int) self.assertEqual(TA.__name__, "TA") + self.assertEqual(TA.__qualname__, "TA") self.assertIs(TA.__value__, int) self.assertEqual(TA.__type_params__, ()) self.assertEqual(TA.__module__, __name__) + def test_with_qualname(self): + TA = TypeAliasType("TA", str, qualname="Class.TA") + self.assertEqual(TA.__name__, "TA") + self.assertEqual(TA.__qualname__, "Class.TA") + self.assertIs(TA.__value__, str) + self.assertEqual(TA.__type_params__, ()) + self.assertEqual(TA.__module__, __name__) + def test_attributes_with_exec(self): ns = {} exec("type TA = int", ns, ns) TA = ns["TA"] self.assertEqual(TA.__name__, "TA") + self.assertEqual(TA.__qualname__, "TA") self.assertIs(TA.__value__, int) self.assertEqual(TA.__type_params__, ()) self.assertIs(TA.__module__, None) @@ -212,6 +261,7 @@ def test_generic(self): T = TypeVar("T") TA = TypeAliasType("TA", list[T], type_params=(T,)) self.assertEqual(TA.__name__, "TA") + self.assertEqual(TA.__qualname__, "TA") self.assertEqual(TA.__value__, list[T]) self.assertEqual(TA.__type_params__, (T,)) self.assertEqual(TA.__module__, __name__) @@ -220,6 +270,7 @@ def test_generic(self): def test_not_generic(self): TA = TypeAliasType("TA", list[int], type_params=()) self.assertEqual(TA.__name__, "TA") + self.assertEqual(TA.__qualname__, "TA") self.assertEqual(TA.__value__, list[int]) self.assertEqual(TA.__type_params__, ()) self.assertEqual(TA.__module__, __name__) @@ -270,8 +321,9 @@ def test_expects_type_like(self): TypeAliasType("A", int, type_params=(T, 2)) def test_keywords(self): - TA = TypeAliasType(name="TA", value=int) + TA = TypeAliasType(name="TA", value=int, type_params=(), qualname=None) self.assertEqual(TA.__name__, "TA") + self.assertEqual(TA.__qualname__, "TA") self.assertIs(TA.__value__, int) self.assertEqual(TA.__type_params__, ()) self.assertEqual(TA.__module__, __name__) @@ -285,6 +337,8 @@ def test_errors(self): TypeAliasType("TA", list, ()) with self.assertRaises(TypeError): TypeAliasType("TA", list, type_params=42) + with self.assertRaises(TypeError): + TypeAliasType("TA", list, qualname=range(5)) class TypeAliasTypeTest(unittest.TestCase): @@ -318,6 +372,8 @@ def test_module(self): mod_generics_cache.__name__) self.assertEqual(mod_generics_cache.OldStyle.__module__, mod_generics_cache.__name__) + Alias.__module__ = "ham.spam.eggs" + self.assertEqual(Alias.__module__, "ham.spam.eggs") def test_unpack(self): type Alias = tuple[int, int] diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index d3a0ec531f0..93e17de7534 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -13,6 +13,7 @@ import pickle import re import sys +import warnings from unittest import TestCase, main from unittest.mock import patch from copy import copy, deepcopy @@ -28,32 +29,31 @@ from typing import assert_type, cast, runtime_checkable from typing import get_type_hints from typing import get_origin, get_args, get_protocol_members -from typing import override +from typing import override, disjoint_base from typing import is_typeddict, is_protocol from typing import reveal_type from typing import dataclass_transform -from typing import no_type_check, no_type_check_decorator +from typing import no_type_check from typing import Type -from typing import NamedTuple, NotRequired, Required, ReadOnly, TypedDict +from typing import NamedTuple, NotRequired, Required, ReadOnly, TypedDict, NoExtraItems from typing import IO, TextIO, BinaryIO from typing import Pattern, Match from typing import Annotated, ForwardRef from typing import Self, LiteralString from typing import TypeAlias from typing import ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs -from typing import TypeGuard, TypeIs, NoDefault +from typing import TypeForm, TypeGuard, TypeIs, NoDefault import abc import textwrap import typing import weakref -import warnings import types from test.support import ( - captured_stderr, cpython_only, infinite_recursion, requires_docstrings, import_helper, run_code, - EqualToForwardRef, - exceeds_recursion_limit, run_with_limited_c_stack, - skip_wasi_stack_overflow, skip_emscripten_stack_overflow, + captured_stderr, cpython_only, requires_docstrings, import_helper, run_code, + subTests, EqualToForwardRef, + exceeds_recursion_limit, skip_if_huge_c_stack, skip_wasi_stack_overflow, + skip_emscripten_stack_overflow, ) from test.typinganndata import ( ann_module695, mod_generics_cache, _typed_dict_helper, @@ -784,7 +784,7 @@ def test_typevartuple_none(self): self.assertIs(U_None.__default__, None) self.assertIs(U_None.has_default(), True) - class X[**Ts]: ... + class X[*Ts]: ... Ts, = X.__type_params__ self.assertIs(Ts.__default__, NoDefault) self.assertIs(Ts.has_default(), False) @@ -1293,6 +1293,62 @@ def test_cannot_call_instance(self): with self.assertRaises(TypeError): Ts() + def test_default_variance(self): + Ts = TypeVarTuple('Ts') + self.assertIs(Ts.__covariant__, False) + self.assertIs(Ts.__contravariant__, False) + self.assertIs(Ts.__infer_variance__, False) + self.assertIsNone(Ts.__bound__) + + def test_covariant(self): + Ts_co = TypeVarTuple('Ts_co', covariant=True) + self.assertIs(Ts_co.__covariant__, True) + self.assertIs(Ts_co.__contravariant__, False) + self.assertIs(Ts_co.__infer_variance__, False) + + def test_contravariant(self): + Ts_contra = TypeVarTuple('Ts_contra', contravariant=True) + self.assertIs(Ts_contra.__covariant__, False) + self.assertIs(Ts_contra.__contravariant__, True) + self.assertIs(Ts_contra.__infer_variance__, False) + + def test_infer_variance(self): + Ts = TypeVarTuple('Ts', infer_variance=True) + self.assertIs(Ts.__covariant__, False) + self.assertIs(Ts.__contravariant__, False) + self.assertIs(Ts.__infer_variance__, True) + + def test_bound(self): + Ts_bound = TypeVarTuple('Ts_bound', bound=int) + self.assertIs(Ts_bound.__bound__, int) + Ts_tuple_bound = TypeVarTuple('Ts_tuple_bound', bound=(int, str)) + self.assertEqual(Ts_tuple_bound.__bound__, (int, str)) + obj = object() + Ts_object = TypeVarTuple('Ts_object', bound=obj) + self.assertIs(Ts_object.__bound__, obj) + Ts_no_bound = TypeVarTuple('Ts_no_bound') + self.assertIsNone(Ts_no_bound.__bound__) + + def test_no_bivariant(self): + with self.assertRaises(ValueError): + TypeVarTuple('Ts', covariant=True, contravariant=True) + + def test_cannot_combine_explicit_and_infer(self): + with self.assertRaises(ValueError): + TypeVarTuple('Ts', covariant=True, infer_variance=True) + with self.assertRaises(ValueError): + TypeVarTuple('Ts', contravariant=True, infer_variance=True) + + def test_repr_with_variance(self): + Ts = TypeVarTuple('Ts') + self.assertEqual(repr(Ts), '~Ts') + Ts_co = TypeVarTuple('Ts_co', covariant=True) + self.assertEqual(repr(Ts_co), '+Ts_co') + Ts_contra = TypeVarTuple('Ts_contra', contravariant=True) + self.assertEqual(repr(Ts_contra), '-Ts_contra') + Ts_infer = TypeVarTuple('Ts_infer', infer_variance=True) + self.assertEqual(repr(Ts_infer), 'Ts_infer') + def test_unpacked_typevartuple_is_equal_to_itself(self): Ts = TypeVarTuple('Ts') self.assertEqual((*Ts,)[0], (*Ts,)[0]) @@ -1621,7 +1677,10 @@ def func1(*args: *Ts): pass self.assertEqual(gth(func1), {'args': Unpack[Ts]}) def func2(*args: *tuple[int, str]): pass - self.assertEqual(gth(func2), {'args': Unpack[tuple[int, str]]}) + hint = gth(func2)['args'] + self.assertIsInstance(hint, types.GenericAlias) + self.assertEqual(hint.__args__[0], int) + self.assertIs(hint.__unpacked__, True) class CustomVariadic(Generic[*Ts]): pass @@ -1636,7 +1695,10 @@ def func1(*args: '*Ts'): pass {'args': Unpack[Ts]}) def func2(*args: '*tuple[int, str]'): pass - self.assertEqual(gth(func2), {'args': Unpack[tuple[int, str]]}) + hint = gth(func2)['args'] + self.assertIsInstance(hint, types.GenericAlias) + self.assertEqual(hint.__args__[0], int) + self.assertIs(hint.__unpacked__, True) class CustomVariadic(Generic[*Ts]): pass @@ -3890,8 +3952,8 @@ def meth(self): pass self.assertIsNot(get_protocol_members(PR), P.__protocol_attrs__) acceptable_extra_attrs = { - '_is_protocol', '_is_runtime_protocol', '__parameters__', - '__init__', '__annotations__', '__subclasshook__', '__annotate__', + '_is_protocol', '_is_runtime_protocol', '__typing_is_deprecated_inherited_runtime_protocol__', + '__parameters__', '__init__', '__annotations__', '__subclasshook__', '__annotate__', '__annotations_cache__', '__annotate_func__', } self.assertLessEqual(vars(NonP).keys(), vars(C).keys() | acceptable_extra_attrs) @@ -3983,6 +4045,7 @@ class C: pass def test_defining_generic_protocols(self): T = TypeVar('T') + T2 = TypeVar('T2') S = TypeVar('S') @runtime_checkable @@ -3992,17 +4055,26 @@ def meth(self): pass class P(PR[int, T], Protocol[T]): y = 1 + self.assertEqual(P.__parameters__, (T,)) + with self.assertRaises(TypeError): PR[int] with self.assertRaises(TypeError): P[int, str] + with self.assertRaisesRegex( + TypeError, + re.escape('Some type variables (~S) are not listed in Protocol[~T, ~T2]'), + ): + class ExtraTypeVars(P[S], Protocol[T, T2]): ... class C(PR[int, T]): pass + self.assertEqual(C.__parameters__, (T,)) self.assertIsInstance(C[str](), C) def test_defining_generic_protocols_old_style(self): T = TypeVar('T') + T2 = TypeVar('T2') S = TypeVar('S') @runtime_checkable @@ -4021,9 +4093,19 @@ class P(PR[int, str], Protocol): class P1(Protocol, Generic[T]): def bar(self, x: T) -> str: ... + self.assertEqual(P1.__parameters__, (T,)) + class P2(Generic[T], Protocol): def bar(self, x: T) -> str: ... + self.assertEqual(P2.__parameters__, (T,)) + + msg = re.escape('Some type variables (~S) are not listed in Protocol[~T, ~T2]') + with self.assertRaisesRegex(TypeError, msg): + class ExtraTypeVars(P1[S], Protocol[T, T2]): ... + with self.assertRaisesRegex(TypeError, msg): + class ExtraTypeVars(P2[S], Protocol[T, T2]): ... + @runtime_checkable class PSub(P1[str], Protocol): x = 1 @@ -4036,6 +4118,28 @@ def bar(self, x: str) -> str: self.assertIsInstance(Test(), PSub) + def test_protocol_parameter_order(self): + # https://github.com/python/cpython/issues/137191 + T1 = TypeVar("T1") + T2 = TypeVar("T2", default=object) + + class A(Protocol[T1]): ... + + class B0(A[T2], Generic[T1, T2]): ... + self.assertEqual(B0.__parameters__, (T1, T2)) + + class B1(A[T2], Protocol, Generic[T1, T2]): ... + self.assertEqual(B1.__parameters__, (T1, T2)) + + class B2(A[T2], Protocol[T1, T2]): ... + self.assertEqual(B2.__parameters__, (T1, T2)) + + class B3[T1, T2](A[T2], Protocol): + @staticmethod + def get_typeparams(): + return (T1, T2) + self.assertEqual(B3.__parameters__, B3.get_typeparams()) + def test_pep695_generic_protocol_callable_members(self): @runtime_checkable class Foo[T](Protocol): @@ -4421,6 +4525,70 @@ class P(Protocol): with self.assertRaisesRegex(TypeError, "@runtime_checkable"): isinstance(1, P) + @subTests(['check_obj', 'check_func'], ([42, isinstance], [frozenset, issubclass])) + def test_inherited_runtime_protocol_deprecated(self, check_obj, check_func): + """See GH-132604.""" + + class BareProto(Protocol): + """I am not runtime-checkable.""" + + @runtime_checkable + class RCProto1(Protocol): + """I am runtime-checkable.""" + + class InheritedRCProto1(RCProto1, Protocol): + """I am accidentally runtime-checkable (by inheritance).""" + + @runtime_checkable + class RCProto2(InheritedRCProto1, Protocol): + """Explicit RC -> inherited RC -> explicit RC.""" + def spam(self): ... + + @runtime_checkable + class RCProto3(BareProto, Protocol): + """Not RC -> explicit RC.""" + + class InheritedRCProto2(RCProto3, Protocol): + """Not RC -> explicit RC -> inherited RC.""" + def eggs(self): ... + + class InheritedRCProto3(RCProto2, Protocol): + """Explicit RC -> inherited RC -> explicit RC -> inherited RC.""" + + class Concrete1(BareProto): + pass + + class Concrete2(InheritedRCProto2): + pass + + class Concrete3(InheritedRCProto3): + pass + + depr_message_re = ( + r" isn't explicitly decorated " + r"with @runtime_checkable but it is used in issubclass\(\) or " + r"isinstance\(\). Instance and class checks can only be used with " + r"@runtime_checkable protocols. This will raise a TypeError in Python 3.20." + ) + + for inherited_runtime_proto in InheritedRCProto1, InheritedRCProto2, InheritedRCProto3: + with self.assertWarnsRegex(DeprecationWarning, depr_message_re): + check_func(check_obj, inherited_runtime_proto) + + # Don't warn for explicitly checkable protocols and concrete implementations. + with warnings.catch_warnings(): + warnings.simplefilter("error", DeprecationWarning) + + for checkable in RCProto1, RCProto2, RCProto3, Concrete1, Concrete2, Concrete3: + check_func(check_obj, checkable) + + # Don't warn for uncheckable protocols. + with warnings.catch_warnings(): + warnings.simplefilter("error", DeprecationWarning) + + with self.assertRaises(TypeError): # Self-test. Protocol below can't be runtime-checkable. + check_func(check_obj, BareProto) + def test_super_call_init(self): class P(Protocol): x: int @@ -4932,7 +5100,7 @@ class MM2(collections.abc.MutableMapping, MutableMapping[str, str]): self.assertEqual(MM2.__bases__, (collections.abc.MutableMapping, Generic)) @cpython_only - @run_with_limited_c_stack() + @skip_if_huge_c_stack() @skip_wasi_stack_overflow() @skip_emscripten_stack_overflow() def test_parameters_deep_recursion(self): @@ -5821,6 +5989,7 @@ def test_subclass_special_form(self): Final[int], Literal[1, 2], Concatenate[int, ParamSpec("P")], + TypeForm[int], TypeGuard[int], TypeIs[range], ): @@ -5887,6 +6056,22 @@ class A: with self.assertRaises(TypeError): a[int] + def test_parameter_added_after_parameters_cached(self): + # gh-155752: GenericAlias parameters are cached before substitution, so + # an argument can gain __typing_subst__ after the tuple is calculated. + class Parameter: + pass + + first = Parameter() + first.__typing_subst__ = lambda value: value + late = Parameter() + alias = types.GenericAlias(dict, (first, late)) + self.assertEqual(alias.__parameters__, (first,)) + late.__typing_subst__ = lambda value: value + + with self.assertRaisesRegex(TypeError, "not found in __parameters__"): + alias[0] + def test_return_non_tuple_while_unpacking(self): # GH-138497: GenericAlias objects didn't ensure that __typing_subst__ actually # returned a tuple @@ -6380,62 +6565,8 @@ class F: for clazz in [C, D, E, F]: self.assertEqual(get_type_hints(clazz), expected_result) - def test_meta_no_type_check(self): - depr_msg = ( - "'typing.no_type_check_decorator' is deprecated " - "and slated for removal in Python 3.15" - ) - with self.assertWarnsRegex(DeprecationWarning, depr_msg): - @no_type_check_decorator - def magic_decorator(func): - return func - - self.assertEqual(magic_decorator.__name__, 'magic_decorator') - - @magic_decorator - def foo(a: 'whatevers') -> {}: - pass - - @magic_decorator - class C: - def foo(a: 'whatevers') -> {}: - pass - - self.assertEqual(foo.__name__, 'foo') - th = get_type_hints(foo) - self.assertEqual(th, {}) - cth = get_type_hints(C.foo) - self.assertEqual(cth, {}) - ith = get_type_hints(C().foo) - self.assertEqual(ith, {}) - class InternalsTests(BaseTestCase): - def test_deprecation_for_no_type_params_passed_to__evaluate(self): - with self.assertWarnsRegex( - DeprecationWarning, - ( - "Failing to pass a value to the 'type_params' parameter " - "of 'typing._eval_type' is deprecated" - ) - ) as cm: - self.assertEqual(typing._eval_type(list["int"], globals(), {}), list[int]) - - self.assertEqual(cm.filename, __file__) - - f = ForwardRef("int") - - with self.assertWarnsRegex( - DeprecationWarning, - ( - "Failing to pass a value to the 'type_params' parameter " - "of 'typing.ForwardRef._evaluate' is deprecated" - ) - ) as cm: - self.assertIs(f._evaluate(globals(), {}, recursive_guard=frozenset()), int) - - self.assertEqual(cm.filename, __file__) - def test_collect_parameters(self): typing = import_helper.import_fresh_module("typing") with self.assertWarnsRegex( @@ -7236,6 +7367,24 @@ def add_right(self, node: 'Node[T]' = None): right_hints = get_type_hints(t.add_right, globals(), locals()) self.assertEqual(right_hints['node'], Node[T]) + def test_get_type_hints_preserve_generic_alias_subclasses(self): + # https://github.com/python/cpython/issues/130870 + # A real world example of this is `collections.abc.Callable`. When parameterized, + # the result is a subclass of `types.GenericAlias`. + class MyAlias(types.GenericAlias): + pass + + class MyClass: + def __class_getitem__(cls, args): + return MyAlias(cls, args) + + # Using a forward reference is important, otherwise it works as expected. + # `y` tests that the `GenericAlias` subclass is preserved when stripping `Annotated`. + def func(x: MyClass['int'], y: MyClass[Annotated[int, ...]]): ... + + assert isinstance(get_type_hints(func)['x'], MyAlias) + assert isinstance(get_type_hints(func)['y'], MyAlias) + def test_stringified_typeddict(self): ns = run_code( """ @@ -7249,6 +7398,13 @@ class TD[UniqueT](TypedDict): self.assertEqual(TD.__annotations__, {'a': EqualToForwardRef('UniqueT', owner=TD, module=TD.__module__)}) self.assertEqual(get_type_hints(TD), {'a': TD.__type_params__[0]}) + def test_get_type_hints_order(self): + """Ensure that the order of function annotations matches the order they're defined""" + def f(positional: int, /, normal: str, *args: bytes, kwarg: list, **kwargs: bool) -> tuple: + pass + + self.assertEqual(list(gth(f)), ["positional", "normal", "args", "kwarg", "kwargs", "return"]) + class GetUtilitiesTestCase(TestCase): def test_get_origin(self): @@ -7339,6 +7495,7 @@ class C(Generic[T]): pass self.assertEqual(get_args(Required[int]), (int,)) self.assertEqual(get_args(NotRequired[int]), (int,)) self.assertEqual(get_args(TypeAlias), ()) + self.assertEqual(get_args(TypeForm[int]), (int,)) self.assertEqual(get_args(TypeGuard[int]), (int,)) self.assertEqual(get_args(TypeIs[range]), (range,)) Ts = TypeVarTuple('Ts') @@ -7576,14 +7733,23 @@ def test_mutablesequence(self): self.assertNotIsInstance((), typing.MutableSequence) def test_bytestring(self): + previous_typing_module = sys.modules.pop("typing", None) + self.addCleanup(sys.modules.__setitem__, "typing", previous_typing_module) + with self.assertWarns(DeprecationWarning): - self.assertIsInstance(b'', typing.ByteString) + from typing import ByteString with self.assertWarns(DeprecationWarning): - self.assertIsInstance(bytearray(b''), typing.ByteString) + self.assertIsInstance(b'', ByteString) with self.assertWarns(DeprecationWarning): - class Foo(typing.ByteString): ... + self.assertIsInstance(bytearray(b''), ByteString) with self.assertWarns(DeprecationWarning): - class Bar(typing.ByteString, typing.Awaitable): ... + self.assertIsSubclass(bytes, ByteString) + with self.assertWarns(DeprecationWarning): + self.assertIsSubclass(bytearray, ByteString) + with self.assertWarns(DeprecationWarning): + class Foo(ByteString): ... + with self.assertWarns(DeprecationWarning): + class Bar(ByteString, typing.Awaitable): ... def test_list(self): self.assertIsSubclass(list, typing.List) @@ -8326,78 +8492,13 @@ class Group(NamedTuple): self.assertIs(type(a), Group) self.assertEqual(a, (1, [2])) - def test_namedtuple_keyword_usage(self): - with self.assertWarnsRegex( - DeprecationWarning, - "Creating NamedTuple classes using keyword arguments is deprecated" - ): - LocalEmployee = NamedTuple("LocalEmployee", name=str, age=int) - - nick = LocalEmployee('Nick', 25) - self.assertIsInstance(nick, tuple) - self.assertEqual(nick.name, 'Nick') - self.assertEqual(LocalEmployee.__name__, 'LocalEmployee') - self.assertEqual(LocalEmployee._fields, ('name', 'age')) - self.assertEqual(LocalEmployee.__annotations__, dict(name=str, age=int)) - - with self.assertRaisesRegex( - TypeError, - "Either list of fields or keywords can be provided to NamedTuple, not both" - ): - NamedTuple('Name', [('x', int)], y=str) - - with self.assertRaisesRegex( - TypeError, - "Either list of fields or keywords can be provided to NamedTuple, not both" - ): - NamedTuple('Name', [], y=str) - - with self.assertRaisesRegex( - TypeError, - ( - r"Cannot pass `None` as the 'fields' parameter " - r"and also specify fields using keyword arguments" - ) - ): - NamedTuple('Name', None, x=int) - - def test_namedtuple_special_keyword_names(self): - with self.assertWarnsRegex( - DeprecationWarning, - "Creating NamedTuple classes using keyword arguments is deprecated" - ): - NT = NamedTuple("NT", cls=type, self=object, typename=str, fields=list) - - self.assertEqual(NT.__name__, 'NT') - self.assertEqual(NT._fields, ('cls', 'self', 'typename', 'fields')) - a = NT(cls=str, self=42, typename='foo', fields=[('bar', tuple)]) - self.assertEqual(a.cls, str) - self.assertEqual(a.self, 42) - self.assertEqual(a.typename, 'foo') - self.assertEqual(a.fields, [('bar', tuple)]) - def test_empty_namedtuple(self): - expected_warning = re.escape( - "Failing to pass a value for the 'fields' parameter is deprecated " - "and will be disallowed in Python 3.15. " - "To create a NamedTuple class with 0 fields " - "using the functional syntax, " - "pass an empty list, e.g. `NT1 = NamedTuple('NT1', [])`." - ) - with self.assertWarnsRegex(DeprecationWarning, fr"^{expected_warning}$"): - NT1 = NamedTuple('NT1') + with self.assertRaisesRegex(TypeError, "missing.*required.*argument"): + BAD = NamedTuple('BAD') - expected_warning = re.escape( - "Passing `None` as the 'fields' parameter is deprecated " - "and will be disallowed in Python 3.15. " - "To create a NamedTuple class with 0 fields " - "using the functional syntax, " - "pass an empty list, e.g. `NT2 = NamedTuple('NT2', [])`." - ) - with self.assertWarnsRegex(DeprecationWarning, fr"^{expected_warning}$"): - NT2 = NamedTuple('NT2', None) - - NT3 = NamedTuple('NT2', []) + NT1 = NamedTuple('NT1', {}) + NT2 = NamedTuple('NT2', ()) + NT3 = NamedTuple('NT3', []) class CNT(NamedTuple): pass # empty body @@ -8412,16 +8513,18 @@ class CNT(NamedTuple): def test_namedtuple_errors(self): with self.assertRaises(TypeError): NamedTuple.__new__() + with self.assertRaisesRegex(TypeError, "object is not iterable"): + NamedTuple('Name', None) with self.assertRaisesRegex( TypeError, - "missing 1 required positional argument" + "missing 2 required positional arguments" ): NamedTuple() with self.assertRaisesRegex( TypeError, - "takes from 1 to 2 positional arguments but 3 were given" + "takes 2 positional arguments but 3 were given" ): NamedTuple('Emp', [('name', str)], None) @@ -8433,10 +8536,22 @@ def test_namedtuple_errors(self): with self.assertRaisesRegex( TypeError, - "missing 1 required positional argument: 'typename'" + "got some positional-only arguments passed as keyword arguments" ): NamedTuple(typename='Emp', name=str, id=int) + with self.assertRaisesRegex( + TypeError, + "got an unexpected keyword argument" + ): + NamedTuple('Name', [('x', int)], y=str) + + with self.assertRaisesRegex( + TypeError, + "got an unexpected keyword argument" + ): + NamedTuple('Name', [], y=str) + def test_copy_and_pickle(self): global Emp # pickle wants to reference the class by name Emp = NamedTuple('Emp', [('name', str), ('cool', int)]) @@ -8567,6 +8682,15 @@ class ThisWontWorkEither(NamedTuple): def name(self): return __class__.__name__ + def test_named_tuple_non_sequence_input(self): + field_names = ["x", "y"] + field_values = [int, int] + Point = NamedTuple("Point", zip(field_names, field_values)) + p = Point(1, 2) + self.assertEqual(p.x, 1) + self.assertEqual(p.y, 2) + self.assertEqual(repr(p),"Point(x=1, y=2)") + class TypedDictTests(BaseTestCase): def test_basics_functional_syntax(self): @@ -8937,6 +9061,32 @@ class ChildWithInlineAndOptional(Untotal, Inline): class Wrong(*bases): pass + def test_closed_values(self): + class Implicit(TypedDict): ... + class ExplicitTrue(TypedDict, closed=True): ... + class ExplicitFalse(TypedDict, closed=False): ... + + self.assertIsNone(Implicit.__closed__) + self.assertIs(ExplicitTrue.__closed__, True) + self.assertIs(ExplicitFalse.__closed__, False) + + def test_extra_items_class_arg(self): + class TD(TypedDict, extra_items=int): + a: str + + self.assertIs(TD.__extra_items__, int) + self.assertEqual(TD.__annotations__, {'a': str}) + self.assertEqual(TD.__required_keys__, frozenset({'a'})) + self.assertEqual(TD.__optional_keys__, frozenset()) + + class NoExtra(TypedDict): + a: str + + self.assertIs(NoExtra.__extra_items__, NoExtraItems) + self.assertEqual(NoExtra.__annotations__, {'a': str}) + self.assertEqual(NoExtra.__required_keys__, frozenset({'a'})) + self.assertEqual(NoExtra.__optional_keys__, frozenset()) + def test_is_typeddict(self): self.assertIs(is_typeddict(Point2D), True) self.assertIs(is_typeddict(Union[str, int]), False) @@ -9180,39 +9330,27 @@ class MultipleGenericBases(GenericParent[int], GenericParent[float]): self.assertEqual(CallTypedDict.__orig_bases__, (TypedDict,)) def test_zero_fields_typeddicts(self): - T1 = TypedDict("T1", {}) + T1a = TypedDict("T1a", {}) + T1b = TypedDict("T1b", []) + T1c = TypedDict("T1c", ()) class T2(TypedDict): pass class T3[tvar](TypedDict): pass S = TypeVar("S") class T4(TypedDict, Generic[S]): pass - expected_warning = re.escape( - "Failing to pass a value for the 'fields' parameter is deprecated " - "and will be disallowed in Python 3.15. " - "To create a TypedDict class with 0 fields " - "using the functional syntax, " - "pass an empty dictionary, e.g. `T5 = TypedDict('T5', {})`." - ) - with self.assertWarnsRegex(DeprecationWarning, fr"^{expected_warning}$"): - T5 = TypedDict('T5') - - expected_warning = re.escape( - "Passing `None` as the 'fields' parameter is deprecated " - "and will be disallowed in Python 3.15. " - "To create a TypedDict class with 0 fields " - "using the functional syntax, " - "pass an empty dictionary, e.g. `T6 = TypedDict('T6', {})`." - ) - with self.assertWarnsRegex(DeprecationWarning, fr"^{expected_warning}$"): - T6 = TypedDict('T6', None) - - for klass in T1, T2, T3, T4, T5, T6: + for klass in T1a, T1b, T1c, T2, T3, T4: with self.subTest(klass=klass.__name__): self.assertEqual(klass.__annotations__, {}) self.assertEqual(klass.__required_keys__, set()) self.assertEqual(klass.__optional_keys__, set()) self.assertIsInstance(klass(), dict) + def test_errors(self): + with self.assertRaisesRegex(TypeError, "missing 1 required.*argument"): + TypedDict('TD') + with self.assertRaisesRegex(TypeError, "object is not iterable"): + TypedDict('TD', None) + def test_readonly_inheritance(self): class Base1(TypedDict): a: ReadOnly[int] @@ -9276,6 +9414,71 @@ class AllTheThings(TypedDict): }, ) + def test_closed_inheritance(self): + class Base(TypedDict, extra_items=ReadOnly[Union[str, None]]): + a: int + + self.assertEqual(Base.__required_keys__, frozenset({"a"})) + self.assertEqual(Base.__optional_keys__, frozenset({})) + self.assertEqual(Base.__readonly_keys__, frozenset({})) + self.assertEqual(Base.__mutable_keys__, frozenset({"a"})) + self.assertEqual(Base.__annotations__, {"a": int}) + self.assertEqual(Base.__extra_items__, ReadOnly[Union[str, None]]) + self.assertIsNone(Base.__closed__) + + class Child(Base, extra_items=int): + a: str + + self.assertEqual(Child.__required_keys__, frozenset({'a'})) + self.assertEqual(Child.__optional_keys__, frozenset({})) + self.assertEqual(Child.__readonly_keys__, frozenset({})) + self.assertEqual(Child.__mutable_keys__, frozenset({'a'})) + self.assertEqual(Child.__annotations__, {"a": str}) + self.assertIs(Child.__extra_items__, int) + self.assertIsNone(Child.__closed__) + + class GrandChild(Child, closed=True): + a: float + + self.assertEqual(GrandChild.__required_keys__, frozenset({'a'})) + self.assertEqual(GrandChild.__optional_keys__, frozenset({})) + self.assertEqual(GrandChild.__readonly_keys__, frozenset({})) + self.assertEqual(GrandChild.__mutable_keys__, frozenset({'a'})) + self.assertEqual(GrandChild.__annotations__, {"a": float}) + self.assertIs(GrandChild.__extra_items__, NoExtraItems) + self.assertIs(GrandChild.__closed__, True) + + class GrandGrandChild(GrandChild): + ... + self.assertEqual(GrandGrandChild.__required_keys__, frozenset({'a'})) + self.assertEqual(GrandGrandChild.__optional_keys__, frozenset({})) + self.assertEqual(GrandGrandChild.__readonly_keys__, frozenset({})) + self.assertEqual(GrandGrandChild.__mutable_keys__, frozenset({'a'})) + self.assertEqual(GrandGrandChild.__annotations__, {"a": float}) + self.assertIs(GrandGrandChild.__extra_items__, NoExtraItems) + self.assertIsNone(GrandGrandChild.__closed__) + + def test_implicit_extra_items(self): + class Base(TypedDict): + a: int + + self.assertIs(Base.__extra_items__, NoExtraItems) + self.assertIsNone(Base.__closed__) + + class ChildA(Base, closed=True): + ... + + self.assertEqual(ChildA.__extra_items__, NoExtraItems) + self.assertIs(ChildA.__closed__, True) + + def test_cannot_combine_closed_and_extra_items(self): + with self.assertRaisesRegex( + TypeError, + "Cannot combine closed=True and extra_items" + ): + class TD(TypedDict, closed=True, extra_items=range): + x: str + def test_annotations(self): # _type_check is applied with self.assertRaisesRegex(TypeError, "Plain typing.Final is not valid as type argument"): @@ -9505,6 +9708,12 @@ class A(typing.Match): class B(typing.Pattern): pass + def test_typed_dict_signature(self): + self.assertListEqual( + list(inspect.signature(TypedDict).parameters), + ['typename', 'fields', 'total', 'closed', 'extra_items'] + ) + class AnnotatedTests(BaseTestCase): @@ -10380,6 +10589,17 @@ def test_paramspec_in_nested_generics(self): self.assertEqual(G2[[int, str], float], list[C]) self.assertEqual(G3[[int, str], float], list[C] | int) + def test_paramspec_bound(self): + P = ParamSpec('P', bound=[int, str]) + self.assertEqual(P.__bound__, [int, str]) + P2 = ParamSpec('P2', bound=(int, str)) + self.assertEqual(P2.__bound__, (int, str)) + obj = object() + P3 = ParamSpec('P3', bound=obj) + self.assertIs(P3.__bound__, obj) + P4 = ParamSpec('P4') + self.assertIs(P4.__bound__, None) + def test_paramspec_gets_copied(self): # bpo-46581 P = ParamSpec('P') @@ -10587,6 +10807,72 @@ def test_no_isinstance(self): issubclass(int, TypeIs) +class TypeFormTests(BaseTestCase): + def test_basics(self): + TypeForm[int] # OK + self.assertEqual(TypeForm[int], TypeForm[int]) + + def foo(arg) -> TypeForm[int]: ... + self.assertEqual(gth(foo), {'return': TypeForm[int]}) + + with self.assertRaises(TypeError): + TypeForm[int, str] + + def test_repr(self): + self.assertEqual(repr(TypeForm), 'typing.TypeForm') + cv = TypeForm[int] + self.assertEqual(repr(cv), 'typing.TypeForm[int]') + cv = TypeForm[Employee] + self.assertEqual(repr(cv), 'typing.TypeForm[%s.Employee]' % __name__) + cv = TypeForm[tuple[int]] + self.assertEqual(repr(cv), 'typing.TypeForm[tuple[int]]') + + def test_cannot_subclass(self): + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): + class C(type(TypeForm)): + pass + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): + class D(type(TypeForm[int])): + pass + with self.assertRaisesRegex(TypeError, + r'Cannot subclass typing\.TypeForm'): + class E(TypeForm): + pass + with self.assertRaisesRegex(TypeError, + r'Cannot subclass typing\.TypeForm\[int\]'): + class F(TypeForm[int]): + pass + + def test_call(self): + objs = [ + 1, + "int", + int, + tuple[int, str], + Tuple[int, str], + ] + for obj in objs: + with self.subTest(obj=obj): + self.assertIs(TypeForm(obj), obj) + + with self.assertRaises(TypeError): + TypeForm() + with self.assertRaises(TypeError): + TypeForm("too", "many") + + def test_cannot_init_type(self): + with self.assertRaises(TypeError): + type(TypeForm)() + with self.assertRaises(TypeError): + type(TypeForm[Optional[int]])() + + def test_no_isinstance(self): + with self.assertRaises(TypeError): + isinstance(1, TypeForm[int]) + with self.assertRaises(TypeError): + issubclass(int, TypeForm) + + SpecialAttrsP = typing.ParamSpec('SpecialAttrsP') SpecialAttrsT = typing.TypeVar('SpecialAttrsT', int, float, complex) @@ -10594,6 +10880,10 @@ def test_no_isinstance(self): class SpecialAttrsTests(BaseTestCase): def test_special_attrs(self): + with warnings.catch_warnings( + action='ignore', category=DeprecationWarning + ): + typing_ByteString = typing.ByteString cls_to_check = { # ABC classes typing.AbstractSet: 'AbstractSet', @@ -10602,7 +10892,7 @@ def test_special_attrs(self): typing.AsyncIterable: 'AsyncIterable', typing.AsyncIterator: 'AsyncIterator', typing.Awaitable: 'Awaitable', - typing.ByteString: 'ByteString', + typing_ByteString: 'ByteString', typing.Callable: 'Callable', typing.ChainMap: 'ChainMap', typing.Collection: 'Collection', @@ -10684,6 +10974,7 @@ def test_special_attrs(self): typing.Never: 'Never', typing.Optional: 'Optional', typing.TypeAlias: 'TypeAlias', + typing.TypeForm: 'TypeForm', typing.TypeGuard: 'TypeGuard', typing.TypeIs: 'TypeIs', typing.TypeVar: 'TypeVar', @@ -10698,6 +10989,7 @@ def test_special_attrs(self): typing.Literal[1, 2]: 'Literal', typing.Literal[True, 2]: 'Literal', typing.Optional[Any]: 'Union', + typing.TypeForm[Any]: 'TypeForm', typing.TypeGuard[Any]: 'TypeGuard', typing.TypeIs[Any]: 'TypeIs', typing.Union[Any]: 'Any', @@ -10780,6 +11072,18 @@ def bar(self): self.assertNotIn('__magic__', dir_items) +class DisjointBaseTests(BaseTestCase): + def test_disjoint_base_unmodified(self): + class C: ... + self.assertIs(C, disjoint_base(C)) + + def test_dunder_disjoint_base(self): + @disjoint_base + class C: ... + + self.assertIs(C.__disjoint_base__, True) + + class RevealTypeTests(BaseTestCase): def test_reveal_type(self): obj = object() @@ -10959,7 +11263,8 @@ def test_all_exported_names(self): # there's a few types and metaclasses that aren't exported not k.endswith(('Meta', '_contra', '_co')) and not k.upper() == k and - # but export all things that have __module__ == 'typing' + k not in {"ByteString"} and + # but export all other things that have __module__ == 'typing' getattr(v, '__module__', None) == typing.__name__ ) } diff --git a/Lib/typing.py b/Lib/typing.py index 2b4389b2967..5b764419d8a 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -28,6 +28,7 @@ import sys import types from types import GenericAlias +lazy import annotationlib from _typing import ( _idfunc, @@ -65,7 +66,6 @@ # ABCs (from collections.abc). 'AbstractSet', # collections.abc.Set. - 'ByteString', 'Container', 'ContextManager', 'Hashable', @@ -127,6 +127,7 @@ 'cast', 'clear_overloads', 'dataclass_transform', + 'disjoint_base', 'evaluate_forward_ref', 'final', 'get_args', @@ -140,8 +141,8 @@ 'Never', 'NewType', 'no_type_check', - 'no_type_check_decorator', 'NoDefault', + 'NoExtraItems', 'NoReturn', 'NotRequired', 'overload', @@ -156,21 +157,13 @@ 'Text', 'TYPE_CHECKING', 'TypeAlias', + 'TypeForm', 'TypeGuard', 'TypeIs', 'TypeAliasType', 'Unpack', ] -class _LazyAnnotationLib: - def __getattr__(self, attr): - global _lazy_annotationlib - import annotationlib - _lazy_annotationlib = annotationlib - return getattr(annotationlib, attr) - -_lazy_annotationlib = _LazyAnnotationLib() - def _type_convert(arg, module=None, *, allow_special_forms=False, owner=None): """For converting None to type(None), and strings to ForwardRef.""" @@ -254,19 +247,30 @@ def _type_repr(obj): if isinstance(obj, tuple): # Special case for `repr` of types with `ParamSpec`: return '[' + ', '.join(_type_repr(t) for t in obj) + ']' - return _lazy_annotationlib.type_repr(obj) + return annotationlib.type_repr(obj) -def _collect_type_parameters(args, *, enforce_default_ordering: bool = True): +def _collect_type_parameters( + args, + *, + enforce_default_ordering: bool = True, + validate_all: bool = False, +): """Collect all type parameters in args in order of first appearance (lexicographic order). + Having an explicit `Generic` or `Protocol` base class determines + the exact parameter order. + For example:: >>> P = ParamSpec('P') >>> T = TypeVar('T') >>> _collect_type_parameters((T, Callable[P, T])) (~T, ~P) + >>> _collect_type_parameters((list[T], Generic[P, T])) + (~P, ~T) + """ # required type parameter cannot appear after parameter with default default_encountered = False @@ -298,6 +302,17 @@ def _collect_type_parameters(args, *, enforce_default_ordering: bool = True): ' follows type parameter with a default') parameters.append(t) + elif ( + not validate_all + and isinstance(t, _GenericAlias) + and t.__origin__ in (Generic, Protocol) + ): + # If we see explicit `Generic[...]` or `Protocol[...]` base classes, + # we need to just copy them as-is. + # Unless `validate_all` is passed, in this case it means that + # we are doing a validation of `Generic` subclasses, + # then we collect all unique parameters to be able to inspect them. + parameters = t.__parameters__ else: if _is_unpacked_typevartuple(t): type_var_tuple_encountered = True @@ -408,6 +423,17 @@ def inner(*args, **kwds): return decorator +def _rebuild_generic_alias(alias: GenericAlias, args: tuple[object, ...]) -> GenericAlias: + is_unpacked = alias.__unpacked__ + if _should_unflatten_callable_args(alias, args): + t = alias.__origin__[(args[:-1], args[-1])] + else: + t = alias.__origin__[args] + if is_unpacked: + t = Unpack[t] + return t + + def _deprecation_warning_for_no_type_params_passed(funcname: str) -> None: import warnings @@ -421,16 +447,7 @@ def _deprecation_warning_for_no_type_params_passed(funcname: str) -> None: warnings.warn(depr_message, category=DeprecationWarning, stacklevel=3) -class _Sentinel: - __slots__ = () - def __repr__(self): - return '' - - -_sentinel = _Sentinel() - - -def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=frozenset(), +def _eval_type(t, globalns, localns, type_params, *, recursive_guard=frozenset(), format=None, owner=None, parent_fwdref=None, prefer_fwd_module=False): """Evaluate all forward references in the given type t. @@ -438,10 +455,7 @@ def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=f recursive_guard is used to prevent infinite recursion with a recursive ForwardRef. """ - if type_params is _sentinel: - _deprecation_warning_for_no_type_params_passed("typing._eval_type") - type_params = () - if isinstance(t, _lazy_annotationlib.ForwardRef): + if isinstance(t, annotationlib.ForwardRef): # If the forward_ref has __forward_module__ set, evaluate() infers the globals # from the module, and it will probably pick better than the globals we have here. # We do this only for calls from get_type_hints() (which opts in through the @@ -467,25 +481,20 @@ def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=f _make_forward_ref(arg, parent_fwdref=parent_fwdref) if isinstance(arg, str) else arg for arg in t.__args__ ) - is_unpacked = t.__unpacked__ - if _should_unflatten_callable_args(t, args): - t = t.__origin__[(args[:-1], args[-1])] - else: - t = t.__origin__[args] - if is_unpacked: - t = Unpack[t] + else: + args = t.__args__ ev_args = tuple( _eval_type( a, globalns, localns, type_params, recursive_guard=recursive_guard, format=format, owner=owner, prefer_fwd_module=prefer_fwd_module, ) - for a in t.__args__ + for a in args ) if ev_args == t.__args__: return t if isinstance(t, GenericAlias): - return GenericAlias(t.__origin__, ev_args) + return _rebuild_generic_alias(t, ev_args) if isinstance(t, Union): return functools.reduce(operator.or_, ev_args) else: @@ -573,6 +582,13 @@ def __getitem__(self, parameters): return self._getitem(self, *parameters) +class _TypeFormForm(_SpecialForm, _root=True): + # TypeForm(X) is equivalent to X but indicates to the type checker + # that the object is a TypeForm. + def __call__(self, obj, /): + return obj + + class _AnyMeta(type): def __instancecheck__(self, obj): if self is Any: @@ -883,6 +899,31 @@ def func1(val: list[object]): return _GenericAlias(self, (item,)) +@_TypeFormForm +def TypeForm(self, parameters): + """A special form representing the value that results from the evaluation + of a type expression. + + This value encodes the information supplied in the type expression, and it + represents the type described by that type expression. + + When used in a type expression, TypeForm describes a set of type form + objects. It accepts a single type argument, which must be a valid type + expression. ``TypeForm[T]`` describes the set of all type form objects that + represent the type T or types that are assignable to T. + + Usage:: + + def cast[T](typ: TypeForm[T], value: Any) -> T: ... + + reveal_type(cast(int, "x")) # int + + See PEP 747 for more information. + """ + item = _type_check(parameters, f'{self} accepts only single type.') + return _GenericAlias(self, (item,)) + + @_SpecialForm def TypeIs(self, parameters): """Special typing construct for marking user-defined type predicate functions. @@ -958,7 +999,7 @@ def _make_forward_ref(code, *, parent_fwdref=None, **kwargs): kwargs['module'] = parent_fwdref.__forward_module__ if parent_fwdref.__owner__ is not None: kwargs['owner'] = parent_fwdref.__owner__ - forward_ref = _lazy_annotationlib.ForwardRef(code, **kwargs) + forward_ref = annotationlib.ForwardRef(code, **kwargs) # For compatibility, eagerly compile the forwardref's code. forward_ref.__forward_code__ return forward_ref @@ -993,18 +1034,18 @@ def evaluate_forward_ref( VALUE. """ - if format == _lazy_annotationlib.Format.STRING: + if format == annotationlib.Format.STRING: return forward_ref.__resolved_str__ if forward_ref.__forward_arg__ in _recursive_guard: return forward_ref if format is None: - format = _lazy_annotationlib.Format.VALUE + format = annotationlib.Format.VALUE value = forward_ref.evaluate(globals=globals, locals=locals, type_params=type_params, owner=owner, format=format) - if (isinstance(value, _lazy_annotationlib.ForwardRef) - and format == _lazy_annotationlib.Format.FORWARDREF): + if (isinstance(value, annotationlib.ForwardRef) + and format == annotationlib.Format.FORWARDREF): return value if isinstance(value, str): @@ -1186,20 +1227,22 @@ def _generic_init_subclass(cls, *args, **kwargs): if error: raise TypeError("Cannot inherit from plain Generic") if '__orig_bases__' in cls.__dict__: - tvars = _collect_type_parameters(cls.__orig_bases__) + tvars = _collect_type_parameters(cls.__orig_bases__, validate_all=True) # Look for Generic[T1, ..., Tn]. # If found, tvars must be a subset of it. # If not found, tvars is it. # Also check for and reject plain Generic, # and reject multiple Generic[...]. gvars = None + basename = None for base in cls.__orig_bases__: if (isinstance(base, _GenericAlias) and - base.__origin__ is Generic): + base.__origin__ in (Generic, Protocol)): if gvars is not None: raise TypeError( "Cannot inherit from Generic[...] multiple times.") gvars = base.__parameters__ + basename = base.__origin__.__name__ if gvars is not None: tvarset = set(tvars) gvarset = set(gvars) @@ -1207,7 +1250,7 @@ def _generic_init_subclass(cls, *args, **kwargs): s_vars = ', '.join(str(t) for t in tvars if t not in gvarset) s_args = ', '.join(str(g) for g in gvars) raise TypeError(f"Some type variables ({s_vars}) are" - f" not listed in Generic[{s_args}]") + f" not listed in {basename}[{s_args}]") tvars = gvars cls.__parameters__ = tuple(tvars) @@ -1605,21 +1648,6 @@ def __ror__(self, left): return Union[left, self] -class _DeprecatedGenericAlias(_SpecialGenericAlias, _root=True): - def __init__( - self, origin, nparams, *, removal_version, inst=True, name=None - ): - super().__init__(origin, nparams, inst=inst, name=name) - self._removal_version = removal_version - - def __instancecheck__(self, inst): - import warnings - warnings._deprecated( - f"{self.__module__}.{self._name}", remove=self._removal_version - ) - return super().__instancecheck__(inst) - - class _CallableGenericAlias(_NotIterable, _GenericAlias, _root=True): def __repr__(self): assert self._name == 'Callable' @@ -1832,6 +1860,7 @@ class _TypingEllipsis: _TYPING_INTERNALS = frozenset({ '__parameters__', '__orig_bases__', '__orig_class__', '_is_protocol', '_is_runtime_protocol', '__protocol_attrs__', + '__typing_is_deprecated_inherited_runtime_protocol__', '__non_callable_proto_members__', '__type_params__', }) @@ -1861,8 +1890,8 @@ def _get_protocol_attrs(cls): annotations = base.__annotations__ except Exception: # Only go through annotationlib to handle deferred annotations if we need to - annotations = _lazy_annotationlib.get_annotations( - base, format=_lazy_annotationlib.Format.FORWARDREF + annotations = annotationlib.get_annotations( + base, format=annotationlib.Format.FORWARDREF ) for attr in (*base.__dict__, *annotations): if not attr.startswith('_abc_') and attr not in EXCLUDED_ATTRIBUTES: @@ -1916,7 +1945,9 @@ def _allow_reckless_class_checks(depth=2): 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} + # gh-136047: When `_abc` module is not available, `_py_abc` is required to + # allow `_py_abc.ABCMeta` fallback. + return _caller(depth) in {'abc', '_py_abc', 'functools', None} _PROTO_ALLOWLIST = { @@ -2019,6 +2050,16 @@ def __subclasscheck__(cls, other): "Instance and class checks can only be used with " "@runtime_checkable protocols" ) + if getattr(cls, '__typing_is_deprecated_inherited_runtime_protocol__', False): + # See GH-132604. + import warnings + depr_message = ( + f"{cls!r} isn't explicitly decorated with @runtime_checkable but " + "it is used in issubclass() or isinstance(). Instance and class " + "checks can only be used with @runtime_checkable protocols. " + "This will raise a TypeError in Python 3.20." + ) + warnings.warn(depr_message, category=DeprecationWarning, stacklevel=2) if ( # this attribute is set by @runtime_checkable: cls.__non_callable_proto_members__ @@ -2048,6 +2089,18 @@ def __instancecheck__(cls, instance): raise TypeError("Instance and class checks can only be used with" " @runtime_checkable protocols") + if getattr(cls, '__typing_is_deprecated_inherited_runtime_protocol__', False): + # See GH-132604. + import warnings + + depr_message = ( + f"{cls!r} isn't explicitly decorated with @runtime_checkable but " + "it is used in issubclass() or isinstance(). Instance and class " + "checks can only be used with @runtime_checkable protocols. " + "This will raise a TypeError in Python 3.20." + ) + warnings.warn(depr_message, category=DeprecationWarning, stacklevel=2) + if _abc_instancecheck(cls, instance): return True @@ -2086,8 +2139,8 @@ def _proto_hook(cls, other): try: annos = base.__annotations__ except Exception: - annos = _lazy_annotationlib.get_annotations( - base, format=_lazy_annotationlib.Format.FORWARDREF + annos = annotationlib.get_annotations( + base, format=annotationlib.Format.FORWARDREF ) if attr in annos: break @@ -2140,6 +2193,11 @@ def __init_subclass__(cls, *args, **kwargs): if not cls.__dict__.get('_is_protocol', False): cls._is_protocol = any(b is Protocol for b in cls.__bases__) + # Mark inherited runtime checkability (deprecated). See GH-132604. + if cls._is_protocol and getattr(cls, '_is_runtime_protocol', False): + # This flag is set to False by @runtime_checkable. + cls.__typing_is_deprecated_inherited_runtime_protocol__ = True + # Set (or override) the protocol subclass hook. if '__subclasshook__' not in cls.__dict__: cls.__subclasshook__ = _proto_hook @@ -2286,6 +2344,9 @@ def close(self): ... raise TypeError('@runtime_checkable can be only applied to protocol classes,' ' got %r' % cls) cls._is_runtime_protocol = True + # See GH-132604. + if hasattr(cls, '__typing_is_deprecated_inherited_runtime_protocol__'): + cls.__typing_is_deprecated_inherited_runtime_protocol__ = False # PEP 544 prohibits using issubclass() # with protocols that have non-method members. # See gh-113320 for why we compute this attribute here, @@ -2366,14 +2427,14 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False, """ if getattr(obj, '__no_type_check__', None): return {} - Format = _lazy_annotationlib.Format + Format = annotationlib.Format if format is None: format = Format.VALUE # Classes require a special treatment. if isinstance(obj, type): hints = {} for base in reversed(obj.__mro__): - ann = _lazy_annotationlib.get_annotations(base, format=format) + ann = annotationlib.get_annotations(base, format=format) if format == Format.STRING: hints.update(ann) continue @@ -2406,7 +2467,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False, else: return {k: _strip_annotations(t) for k, t in hints.items()} - hints = _lazy_annotationlib.get_annotations(obj, format=format) + hints = annotationlib.get_annotations(obj, format=format) if ( not hints and not isinstance(obj, types.ModuleType) @@ -2482,7 +2543,7 @@ def _strip_annotations(t): stripped_args = tuple(_strip_annotations(a) for a in t.__args__) if stripped_args == t.__args__: return t - return GenericAlias(t.__origin__, stripped_args) + return _rebuild_generic_alias(t, stripped_args) if isinstance(t, Union): stripped_args = tuple(_strip_annotations(a) for a in t.__args__) if stripped_args == t.__args__: @@ -2630,23 +2691,6 @@ def no_type_check(arg): return arg -def no_type_check_decorator(decorator): - """Decorator to give another decorator the @no_type_check effect. - - This wraps the decorator with something that wraps the decorated - function in @no_type_check. - """ - import warnings - warnings._deprecated("typing.no_type_check_decorator", remove=(3, 15)) - @functools.wraps(decorator) - def wrapped_decorator(*args, **kwds): - func = decorator(*args, **kwds) - func = no_type_check(func) - return func - - return wrapped_decorator - - def _overload_dummy(*args, **kwds): """Helper for @overload to raise when called.""" raise NotImplementedError( @@ -2753,6 +2797,29 @@ class Other(Leaf): # Error reported by type checker return f +def disjoint_base(cls): + """This decorator marks a class as a disjoint base. + + Child classes of a disjoint base cannot inherit from other disjoint bases that are + not parent or child classes of the disjoint base. + + For example: + + @disjoint_base + class Disjoint1: pass + + @disjoint_base + class Disjoint2: pass + + class Disjoint3(Disjoint1, Disjoint2): pass # Type checker error + + Type checkers can use knowledge of disjoint bases to detect unreachable code + and determine when two types can overlap. + """ + cls.__disjoint_base__ = True + return cls + + # Some unconstrained type variables. These were initially used by the container types. # They were never meant for export and are now unused, but we keep them around to # avoid breaking compatibility with users who import them. @@ -2808,9 +2875,6 @@ class Other(Leaf): # Error reported by type checker MutableMapping = _alias(collections.abc.MutableMapping, 2) Sequence = _alias(collections.abc.Sequence, 1) MutableSequence = _alias(collections.abc.MutableSequence, 1) -ByteString = _DeprecatedGenericAlias( - collections.abc.ByteString, 0, removal_version=(3, 17) # Not generic. -) # Tuple accepts variable number of parameters. Tuple = _TupleType(tuple, -1, inst=False, name='Tuple') Tuple.__doc__ = \ @@ -2954,10 +3018,10 @@ def _make_eager_annotate(types): for key, val in types.items()} def annotate(format): match format: - case _lazy_annotationlib.Format.VALUE | _lazy_annotationlib.Format.FORWARDREF: + case annotationlib.Format.VALUE | annotationlib.Format.FORWARDREF: return checked_types - case _lazy_annotationlib.Format.STRING: - return _lazy_annotationlib.annotations_to_string(types) + case annotationlib.Format.STRING: + return annotationlib.annotations_to_string(types) case _: raise NotImplementedError(format) return annotate @@ -2987,9 +3051,9 @@ def __new__(cls, typename, bases, ns): types = ns["__annotations__"] field_names = list(types) annotate = _make_eager_annotate(types) - elif (original_annotate := _lazy_annotationlib.get_annotate_from_class_namespace(ns)) is not None: - types = _lazy_annotationlib.call_annotate_function( - original_annotate, _lazy_annotationlib.Format.FORWARDREF) + elif (original_annotate := annotationlib.get_annotate_from_class_namespace(ns)) is not None: + types = annotationlib.call_annotate_function( + original_annotate, annotationlib.Format.FORWARDREF) field_names = list(types) # For backward compatibility, type-check all the types at creation time @@ -2997,9 +3061,9 @@ def __new__(cls, typename, bases, ns): _type_check(typ, "field annotation must be a type") def annotate(format): - annos = _lazy_annotationlib.call_annotate_function( + annos = annotationlib.call_annotate_function( original_annotate, format) - if format != _lazy_annotationlib.Format.STRING: + if format != annotationlib.Format.STRING: return {key: _type_check(val, f"field {key} annotation must be a type") for key, val in annos.items()} return annos @@ -3049,7 +3113,7 @@ def annotate(format): return nm_tpl -def NamedTuple(typename, fields=_sentinel, /, **kwargs): +def NamedTuple(typename, fields, /): """Typed version of collections.namedtuple. Usage:: @@ -3069,49 +3133,9 @@ class Employee(NamedTuple): Employee = NamedTuple('Employee', [('name', str), ('id', int)]) """ - if fields is _sentinel: - if kwargs: - deprecated_thing = "Creating NamedTuple classes using keyword arguments" - deprecation_msg = ( - "{name} is deprecated and will be disallowed in Python {remove}. " - "Use the class-based or functional syntax instead." - ) - else: - deprecated_thing = "Failing to pass a value for the 'fields' parameter" - example = f"`{typename} = NamedTuple({typename!r}, [])`" - deprecation_msg = ( - "{name} is deprecated and will be disallowed in Python {remove}. " - "To create a NamedTuple class with 0 fields " - "using the functional syntax, " - "pass an empty list, e.g. " - ) + example + "." - elif fields is None: - if kwargs: - raise TypeError( - "Cannot pass `None` as the 'fields' parameter " - "and also specify fields using keyword arguments" - ) - else: - deprecated_thing = "Passing `None` as the 'fields' parameter" - example = f"`{typename} = NamedTuple({typename!r}, [])`" - deprecation_msg = ( - "{name} is deprecated and will be disallowed in Python {remove}. " - "To create a NamedTuple class with 0 fields " - "using the functional syntax, " - "pass an empty list, e.g. " - ) + example + "." - elif kwargs: - raise TypeError("Either list of fields or keywords" - " can be provided to NamedTuple, not both") - if fields is _sentinel or fields is None: - import warnings - warnings._deprecated(deprecated_thing, message=deprecation_msg, remove=(3, 15)) - fields = kwargs.items() types = {n: _type_check(t, f"field {n} annotation must be a type") for n, t in fields} - field_names = [n for n, _ in fields] - - nt = _make_nmtuple(typename, field_names, _make_eager_annotate(types), module=_caller()) + nt = _make_nmtuple(typename, types, _make_eager_annotate(types), module=_caller()) nt.__orig_bases__ = (NamedTuple,) return nt @@ -3124,6 +3148,9 @@ def _namedtuple_mro_entries(bases): NamedTuple.__mro_entries__ = _namedtuple_mro_entries +NoExtraItems = sentinel("NoExtraItems") + + def _get_typeddict_qualifiers(annotation_type): while True: annotation_origin = get_origin(annotation_type) @@ -3147,7 +3174,8 @@ def _get_typeddict_qualifiers(annotation_type): class _TypedDictMeta(type): - def __new__(cls, name, bases, ns, total=True): + def __new__(cls, name, bases, ns, total=True, closed=None, + extra_items=NoExtraItems): """Create a new typed dict class object. This method is called when TypedDict is subclassed, @@ -3159,6 +3187,8 @@ def __new__(cls, name, bases, ns, total=True): if type(base) is not _TypedDictMeta and base is not Generic: raise TypeError('cannot inherit from both a TypedDict type ' 'and a non-TypedDict base class') + if closed is not None and extra_items is not NoExtraItems: + raise TypeError(f"Cannot combine closed={closed!r} and extra_items") if any(issubclass(b, Generic) for b in bases): generic_base = (Generic,) @@ -3175,9 +3205,9 @@ def __new__(cls, name, bases, ns, total=True): if ns_annotations is not None: own_annotate = None own_annotations = ns_annotations - elif (own_annotate := _lazy_annotationlib.get_annotate_from_class_namespace(ns)) is not None: - own_annotations = _lazy_annotationlib.call_annotate_function( - own_annotate, _lazy_annotationlib.Format.FORWARDREF, owner=tp_dict + elif (own_annotate := annotationlib.get_annotate_from_class_namespace(ns)) is not None: + own_annotations = annotationlib.call_annotate_function( + own_annotate, annotationlib.Format.FORWARDREF, owner=tp_dict ) else: own_annotate = None @@ -3244,20 +3274,20 @@ def __annotate__(format): base_annotate = base.__annotate__ if base_annotate is None: continue - base_annos = _lazy_annotationlib.call_annotate_function( + base_annos = annotationlib.call_annotate_function( base_annotate, format, owner=base) annos.update(base_annos) if own_annotate is not None: - own = _lazy_annotationlib.call_annotate_function( + own = annotationlib.call_annotate_function( own_annotate, format, owner=tp_dict) - if format != _lazy_annotationlib.Format.STRING: + if format != annotationlib.Format.STRING: own = { n: _type_check(tp, msg, module=tp_dict.__module__) for n, tp in own.items() } - elif format == _lazy_annotationlib.Format.STRING: - own = _lazy_annotationlib.annotations_to_string(own_annotations) - elif format in (_lazy_annotationlib.Format.FORWARDREF, _lazy_annotationlib.Format.VALUE): + elif format == annotationlib.Format.STRING: + own = annotationlib.annotations_to_string(own_annotations) + elif format in (annotationlib.Format.FORWARDREF, annotationlib.Format.VALUE): own = own_checked_annotations else: raise NotImplementedError(format) @@ -3270,6 +3300,8 @@ def __annotate__(format): tp_dict.__readonly_keys__ = frozenset(readonly_keys) tp_dict.__mutable_keys__ = frozenset(mutable_keys) tp_dict.__total__ = total + tp_dict.__closed__ = closed + tp_dict.__extra_items__ = extra_items return tp_dict __call__ = dict # static method @@ -3281,7 +3313,8 @@ def __subclasscheck__(cls, other): __instancecheck__ = __subclasscheck__ -def TypedDict(typename, fields=_sentinel, /, *, total=True): +def TypedDict(typename, fields, /, *, total=True, closed=None, + extra_items=NoExtraItems): """A simple typed namespace. At runtime it is equivalent to a plain dict. TypedDict creates a dictionary type such that a type checker will expect all @@ -3335,33 +3368,41 @@ class DatabaseUser(TypedDict): id: ReadOnly[int] # the "id" key must not be modified username: str # the "username" key can be changed - See PEPs 589, 655, and 705 for more information. - """ - if fields is _sentinel or fields is None: - import warnings + The closed argument controls whether the TypedDict allows additional + non-required items during inheritance and assignability checks. + If closed=True, the TypedDict does not allow additional items:: - if fields is _sentinel: - deprecated_thing = "Failing to pass a value for the 'fields' parameter" - else: - deprecated_thing = "Passing `None` as the 'fields' parameter" - - example = f"`{typename} = TypedDict({typename!r}, {{{{}}}})`" - deprecation_msg = ( - "{name} is deprecated and will be disallowed in Python {remove}. " - "To create a TypedDict class with 0 fields " - "using the functional syntax, " - "pass an empty dictionary, e.g. " - ) + example + "." - warnings._deprecated(deprecated_thing, message=deprecation_msg, remove=(3, 15)) - fields = {} + Point2D = TypedDict('Point2D', {'x': int, 'y': int}, closed=True) + class Point3D(Point2D): + z: int # Type checker error + + Passing closed=False explicitly requests TypedDict's default open behavior. + If closed is not provided, the behavior is inherited from the superclass. + A type checker is only expected to support a literal False or True as the + value of the closed argument. + + The extra_items argument can instead be used to specify the assignable type + of unknown non-required keys:: + Point2D = TypedDict('Point2D', {'x': int, 'y': int}, extra_items=int) + class Point3D(Point2D): + z: int # OK + label: str # Type checker error + + The extra_items argument is also inherited through subclassing. It is unset + by default, and it may not be used with the closed argument at the same + time. + + See PEPs 589, 655, 705, and 728 for more information. + """ ns = {'__annotations__': dict(fields)} module = _caller() if module is not None: # Setting correct module is necessary to make typed dict classes pickleable. ns['__module__'] = module - td = _TypedDictMeta(typename, (), ns, total=total) + td = _TypedDictMeta(typename, (), ns, total=total, closed=closed, + extra_items=extra_items) td.__orig_bases__ = (TypedDict,) return td @@ -3549,7 +3590,7 @@ def isatty(self) -> bool: pass @abstractmethod - def read(self, n: int = -1) -> AnyStr: + def read(self, n: int = -1, /) -> AnyStr: pass @abstractmethod @@ -3557,15 +3598,15 @@ def readable(self) -> bool: pass @abstractmethod - def readline(self, limit: int = -1) -> AnyStr: + def readline(self, limit: int = -1, /) -> AnyStr: pass @abstractmethod - def readlines(self, hint: int = -1) -> list[AnyStr]: + def readlines(self, hint: int = -1, /) -> list[AnyStr]: pass @abstractmethod - def seek(self, offset: int, whence: int = 0) -> int: + def seek(self, offset: int, whence: int = 0, /) -> int: pass @abstractmethod @@ -3577,7 +3618,7 @@ def tell(self) -> int: pass @abstractmethod - def truncate(self, size: int | None = None) -> int: + def truncate(self, size: int | None = None, /) -> int: pass @abstractmethod @@ -3585,11 +3626,11 @@ def writable(self) -> bool: pass @abstractmethod - def write(self, s: AnyStr) -> int: + def write(self, s: AnyStr, /) -> int: pass @abstractmethod - def writelines(self, lines: list[AnyStr]) -> None: + def writelines(self, lines: list[AnyStr], /) -> None: pass @abstractmethod @@ -3597,7 +3638,7 @@ def __enter__(self) -> IO[AnyStr]: pass @abstractmethod - def __exit__(self, type, value, traceback) -> None: + def __exit__(self, type, value, traceback, /) -> None: pass @@ -3607,7 +3648,7 @@ class BinaryIO(IO[bytes]): __slots__ = () @abstractmethod - def write(self, s: bytes | bytearray) -> int: + def write(self, s: bytes | bytearray, /) -> int: pass @abstractmethod @@ -3843,7 +3884,7 @@ def __getattr__(attr): are only created on-demand here. """ if attr == "ForwardRef": - obj = _lazy_annotationlib.ForwardRef + obj = annotationlib.ForwardRef elif attr in {"Pattern", "Match"}: import re obj = _alias(getattr(re, attr), 1) @@ -3860,6 +3901,48 @@ def __getattr__(attr): ) warnings.warn(depr_message, category=DeprecationWarning, stacklevel=2) obj = _collect_type_parameters + elif attr == "ByteString": + import warnings + + warnings._deprecated( + "typing.ByteString", + message=( + "{name!r} and 'collections.abc.ByteString' are deprecated " + "and slated for removal in Python {remove}" + ), + remove=(3, 17) + ) + + class _DeprecatedGenericAlias(_SpecialGenericAlias, _root=True): + def __init__( + self, origin, nparams, *, removal_version, inst=True, name=None + ): + super().__init__(origin, nparams, inst=inst, name=name) + self._removal_version = removal_version + + def __instancecheck__(self, inst): + import warnings + warnings._deprecated( + f"{self.__module__}.{self._name}", remove=self._removal_version + ) + return super().__instancecheck__(inst) + + def __subclasscheck__(self, cls): + import warnings + warnings._deprecated( + f"{self.__module__}.{self._name}", remove=self._removal_version + ) + return super().__subclasscheck__(cls) + + with warnings.catch_warnings( + action="ignore", category=DeprecationWarning + ): + # Not generic + ByteString = globals()["ByteString"] = _DeprecatedGenericAlias( + collections.abc.ByteString, 0, removal_version=(3, 17) + ) + + return ByteString else: raise AttributeError(f"module {__name__!r} has no attribute {attr!r}") globals()[attr] = obj diff --git a/crates/vm/src/stdlib/_sre.rs b/crates/vm/src/stdlib/_sre.rs index 2e24b1477cc..1cca04d1c0f 100644 --- a/crates/vm/src/stdlib/_sre.rs +++ b/crates/vm/src/stdlib/_sre.rs @@ -543,6 +543,7 @@ mod _sre { flags(HAS_WEAKREF) )] impl Py { + #[pymethod(name = "prefixmatch")] #[pymethod(name = "match")] fn py_match( zelf: PyRef, @@ -1161,6 +1162,7 @@ mod _sre { self.pattern.clone() } + #[pymethod(name = "prefixmatch")] #[pymethod(name = "match")] fn py_match(&self, vm: &VirtualMachine) -> PyResult>> { with_sre_str!(self.pattern, &self.string.clone(), vm, |s| {