mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-29 03:04:38 +08:00
Upgrade typing from 3.14.2 and more impl (#7057)
* Implement TypeAliasType, type annotations, and genericalias fixes - TypeAliasType: lazy value evaluation via closures, __module__, __parameters__, __iter__, evaluate_value, check_type_params, IMMUTABLETYPE flag, Hashable/AsMapping/Iterable traits - TypeAliasType constructor: positional-or-keyword arg validation, duplicate/unexpected kwarg rejection - type.__annotations__ setter: distinguish None assignment from deletion - Annotation scope: name as __annotate__, format as positional-only, __conditional_annotations__ uses Cell for both load and store - Compiler: proper TypeParams/TypeAlias scope with closures, find_ann covers match/try-except handlers - symboltable: deduplicate TypeAlias value scope code - GenericAlias repr: handle list args, avoid deadlock in repr_arg by cloning items before calling repr - AST types: remove IMMUTABLETYPE (heap types, mutable) - pymodule macro: preserve existing __module__ getset descriptors * Update typing from v3.14.3 * Implement TypeAliasType, type annotations, and genericalias fixes - TypeAliasType: lazy value evaluation via closures, __module__, __parameters__, __iter__, evaluate_value, check_type_params, IMMUTABLETYPE flag, Hashable/AsMapping/Iterable traits - TypeAliasType constructor: positional-or-keyword arg validation, duplicate/unexpected kwarg rejection - type.__annotations__ setter: distinguish None assignment from deletion - Annotation scope: name as __annotate__, format as positional-only, __conditional_annotations__ uses Cell for both load and store - Compiler: proper TypeParams/TypeAlias scope with closures, find_ann covers match/try-except handlers - symboltable: deduplicate TypeAlias value scope code - GenericAlias: implement gaiterobject (generic_alias_iterator), starred equality comparison, starred pickle via iterator reduce, split attr_exceptions/attr_blocked for correct __dir__, make_parameters/subs_parameters handle list/tuple args recursively, repr_arg indexed access for mutation safety - AST types: remove IMMUTABLETYPE (heap types, mutable) - pymodule macro: preserve existing __module__ getset descriptors --------- Co-authored-by: CPython Developers <>
This commit is contained in:
1 parent
29f7bb2b40
commit
d567e25ac9
26 files changed
+3738
-261
No files matched your search
+1
@@ -86,6 +86,7 @@ class TestCase(unittest.TestCase):
|
||||
|
||||
self.assertIn(",type=...,", repr_output)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; recursive annotation type not shown as ...
|
||||
def test_recursive_annotation(self):
|
||||
class C:
|
||||
pass
|
||||
|
||||
Vendored
+142
-11
@@ -57,6 +57,11 @@ from queue import Queue, SimpleQueue
|
||||
from weakref import WeakSet, ReferenceType, ref
|
||||
import typing
|
||||
from typing import Unpack
|
||||
try:
|
||||
from tkinter import Event
|
||||
except ImportError:
|
||||
Event = None
|
||||
from string.templatelib import Template, Interpolation
|
||||
|
||||
from typing import TypeVar
|
||||
T = TypeVar('T')
|
||||
@@ -96,7 +101,7 @@ _UNPACKED_TUPLES = [
|
||||
|
||||
class BaseTest(unittest.TestCase):
|
||||
"""Test basics."""
|
||||
generic_types = [type, tuple, list, dict, set, frozenset, enumerate,
|
||||
generic_types = [type, tuple, list, dict, set, frozenset, enumerate, memoryview,
|
||||
defaultdict, deque,
|
||||
SequenceMatcher,
|
||||
dircmp,
|
||||
@@ -133,13 +138,21 @@ class BaseTest(unittest.TestCase):
|
||||
Future, _WorkItem,
|
||||
Morsel,
|
||||
DictReader, DictWriter,
|
||||
array]
|
||||
array,
|
||||
staticmethod,
|
||||
classmethod,
|
||||
Template,
|
||||
Interpolation,
|
||||
]
|
||||
if ctypes is not None:
|
||||
generic_types.extend((ctypes.Array, ctypes.LibraryLoader))
|
||||
generic_types.extend((ctypes.Array, ctypes.LibraryLoader, ctypes.py_object))
|
||||
if ValueProxy is not None:
|
||||
generic_types.extend((ValueProxy, DictProxy, ListProxy, ApplyResult,
|
||||
MPSimpleQueue, MPQueue, MPJoinableQueue))
|
||||
if Event is not None:
|
||||
generic_types.append(Event)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; memoryview, Template, Interpolation, py_object not subscriptable
|
||||
def test_subscriptable(self):
|
||||
for t in self.generic_types:
|
||||
if t is None:
|
||||
@@ -209,7 +222,6 @@ class BaseTest(unittest.TestCase):
|
||||
self.assertEqual(t.__args__, (int,))
|
||||
self.assertEqual(t.__parameters__, ())
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test_repr(self):
|
||||
class MyList(list):
|
||||
pass
|
||||
@@ -225,13 +237,63 @@ class BaseTest(unittest.TestCase):
|
||||
self.assertEqual(repr(x2), 'tuple[*tuple[int, str]]')
|
||||
x3 = tuple[*tuple[int, ...]]
|
||||
self.assertEqual(repr(x3), 'tuple[*tuple[int, ...]]')
|
||||
self.assertTrue(repr(MyList[int]).endswith('.BaseTest.test_repr.<locals>.MyList[int]'))
|
||||
self.assertEndsWith(repr(MyList[int]), '.BaseTest.test_repr.<locals>.MyList[int]')
|
||||
self.assertEqual(repr(list[str]()), '[]') # instances should keep their normal repr
|
||||
|
||||
# gh-105488
|
||||
self.assertTrue(repr(MyGeneric[int]).endswith('MyGeneric[int]'))
|
||||
self.assertTrue(repr(MyGeneric[[]]).endswith('MyGeneric[[]]'))
|
||||
self.assertTrue(repr(MyGeneric[[int, str]]).endswith('MyGeneric[[int, str]]'))
|
||||
self.assertEndsWith(repr(MyGeneric[int]), 'MyGeneric[int]')
|
||||
self.assertEndsWith(repr(MyGeneric[[]]), 'MyGeneric[[]]')
|
||||
self.assertEndsWith(repr(MyGeneric[[int, str]]), 'MyGeneric[[int, str]]')
|
||||
|
||||
def test_evil_repr1(self):
|
||||
# gh-143635
|
||||
class Zap:
|
||||
def __init__(self, container):
|
||||
self.container = container
|
||||
def __getattr__(self, name):
|
||||
if name == "__origin__":
|
||||
self.container.clear()
|
||||
return None
|
||||
if name == "__args__":
|
||||
return ()
|
||||
raise AttributeError
|
||||
|
||||
params = []
|
||||
params.append(Zap(params))
|
||||
alias = GenericAlias(list, (params,))
|
||||
repr_str = repr(alias)
|
||||
self.assertTrue(repr_str.startswith("list[["), repr_str)
|
||||
|
||||
def test_evil_repr2(self):
|
||||
class Zap:
|
||||
def __init__(self, container):
|
||||
self.container = container
|
||||
def __getattr__(self, name):
|
||||
if name == "__qualname__":
|
||||
self.container.clear()
|
||||
return "abcd"
|
||||
if name == "__module__":
|
||||
return None
|
||||
raise AttributeError
|
||||
|
||||
params = []
|
||||
params.append(Zap(params))
|
||||
alias = GenericAlias(list, (params,))
|
||||
repr_str = repr(alias)
|
||||
self.assertTrue(repr_str.startswith("list[["), repr_str)
|
||||
|
||||
def test_evil_repr3(self):
|
||||
# gh-143823
|
||||
lst = []
|
||||
class X:
|
||||
def __repr__(self):
|
||||
lst.clear()
|
||||
return "x"
|
||||
|
||||
lst += [X(), 1]
|
||||
ga = GenericAlias(int, lst)
|
||||
with self.assertRaises(IndexError):
|
||||
repr(ga)
|
||||
|
||||
def test_exposed_type(self):
|
||||
import types
|
||||
@@ -333,7 +395,6 @@ class BaseTest(unittest.TestCase):
|
||||
with self.assertRaises(TypeError):
|
||||
dict[T, T][str, int]
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test_equality(self):
|
||||
self.assertEqual(list[int], list[int])
|
||||
self.assertEqual(dict[str, int], dict[str, int])
|
||||
@@ -352,7 +413,7 @@ class BaseTest(unittest.TestCase):
|
||||
|
||||
def test_issubclass(self):
|
||||
class L(list): ...
|
||||
self.assertTrue(issubclass(L, list))
|
||||
self.assertIsSubclass(L, list)
|
||||
with self.assertRaises(TypeError):
|
||||
issubclass(L, list[str])
|
||||
|
||||
@@ -424,7 +485,6 @@ class BaseTest(unittest.TestCase):
|
||||
self.assertEqual(a.__args__, (list[T], tuple[T, ...]))
|
||||
self.assertEqual(a.__parameters__, (T,))
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test_dir(self):
|
||||
ga = list[int]
|
||||
dir_of_gen_alias = set(dir(ga))
|
||||
@@ -447,6 +507,7 @@ class BaseTest(unittest.TestCase):
|
||||
with self.subTest(entry=entry):
|
||||
getattr(ga, entry) # must not raise `AttributeError`
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; memoryview, Template, Interpolation, py_object not subscriptable
|
||||
def test_weakref(self):
|
||||
for t in self.generic_types:
|
||||
if t is None:
|
||||
@@ -490,6 +551,76 @@ class BaseTest(unittest.TestCase):
|
||||
iter_x = iter(t)
|
||||
del iter_x
|
||||
|
||||
def test_paramspec_specialization(self):
|
||||
# gh-124445
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
type X[**P] = Callable[P, int]
|
||||
|
||||
generic = X[[T]]
|
||||
self.assertEqual(generic.__args__, ([T],))
|
||||
self.assertEqual(generic.__parameters__, (T,))
|
||||
specialized = generic[str]
|
||||
self.assertEqual(specialized.__args__, ([str],))
|
||||
self.assertEqual(specialized.__parameters__, ())
|
||||
|
||||
generic = X[(T,)]
|
||||
self.assertEqual(generic.__args__, (T,))
|
||||
self.assertEqual(generic.__parameters__, (T,))
|
||||
specialized = generic[str]
|
||||
self.assertEqual(specialized.__args__, (str,))
|
||||
self.assertEqual(specialized.__parameters__, ())
|
||||
|
||||
generic = X[[T, U]]
|
||||
self.assertEqual(generic.__args__, ([T, U],))
|
||||
self.assertEqual(generic.__parameters__, (T, U))
|
||||
specialized = generic[str, int]
|
||||
self.assertEqual(specialized.__args__, ([str, int],))
|
||||
self.assertEqual(specialized.__parameters__, ())
|
||||
|
||||
generic = X[(T, U)]
|
||||
self.assertEqual(generic.__args__, (T, U))
|
||||
self.assertEqual(generic.__parameters__, (T, U))
|
||||
specialized = generic[str, int]
|
||||
self.assertEqual(specialized.__args__, (str, int))
|
||||
self.assertEqual(specialized.__parameters__, ())
|
||||
|
||||
def test_nested_paramspec_specialization(self):
|
||||
# gh-124445
|
||||
type X[**P, T] = Callable[P, T]
|
||||
|
||||
x_list = X[[int, str], float]
|
||||
self.assertEqual(x_list.__args__, ([int, str], float))
|
||||
self.assertEqual(x_list.__parameters__, ())
|
||||
|
||||
x_tuple = X[(int, str), float]
|
||||
self.assertEqual(x_tuple.__args__, ((int, str), float))
|
||||
self.assertEqual(x_tuple.__parameters__, ())
|
||||
|
||||
U = TypeVar("U")
|
||||
V = TypeVar("V")
|
||||
|
||||
multiple_params_list = X[[int, U], V]
|
||||
self.assertEqual(multiple_params_list.__args__, ([int, U], V))
|
||||
self.assertEqual(multiple_params_list.__parameters__, (U, V))
|
||||
multiple_params_list_specialized = multiple_params_list[str, float]
|
||||
self.assertEqual(multiple_params_list_specialized.__args__, ([int, str], float))
|
||||
self.assertEqual(multiple_params_list_specialized.__parameters__, ())
|
||||
|
||||
multiple_params_tuple = X[(int, U), V]
|
||||
self.assertEqual(multiple_params_tuple.__args__, ((int, U), V))
|
||||
self.assertEqual(multiple_params_tuple.__parameters__, (U, V))
|
||||
multiple_params_tuple_specialized = multiple_params_tuple[str, float]
|
||||
self.assertEqual(multiple_params_tuple_specialized.__args__, ((int, str), float))
|
||||
self.assertEqual(multiple_params_tuple_specialized.__parameters__, ())
|
||||
|
||||
deeply_nested = X[[U, [V], int], V]
|
||||
self.assertEqual(deeply_nested.__args__, ([U, [V], int], V))
|
||||
self.assertEqual(deeply_nested.__parameters__, (U, V))
|
||||
deeply_nested_specialized = deeply_nested[str, float]
|
||||
self.assertEqual(deeply_nested_specialized.__args__, ([str, [float], int], float))
|
||||
self.assertEqual(deeply_nested_specialized.__parameters__, ())
|
||||
|
||||
|
||||
class TypeIterationTests(unittest.TestCase):
|
||||
_UNITERABLE_TYPES = (list, tuple)
|
||||
|
||||
Vendored
-1
@@ -845,7 +845,6 @@ class TestRecursiveRepr(unittest.TestCase):
|
||||
|
||||
self.assertIs(X.f, X.__repr__.__wrapped__)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'TypeVar' object has no attribute '__name__'
|
||||
def test__type_params__(self):
|
||||
class My:
|
||||
@recursive_repr()
|
||||
|
||||
Vendored
+415
@@ -0,0 +1,415 @@
|
||||
import pickle
|
||||
import types
|
||||
import unittest
|
||||
from test.support import check_syntax_error, run_code
|
||||
from test.typinganndata import mod_generics_cache
|
||||
|
||||
from typing import (
|
||||
Callable, TypeAliasType, TypeVar, TypeVarTuple, ParamSpec, Unpack, get_args,
|
||||
)
|
||||
|
||||
|
||||
class TypeParamsInvalidTest(unittest.TestCase):
|
||||
def test_name_collisions(self):
|
||||
check_syntax_error(self, 'type TA1[A, **A] = None', "duplicate type parameter 'A'")
|
||||
check_syntax_error(self, 'type T[A, *A] = None', "duplicate type parameter 'A'")
|
||||
check_syntax_error(self, 'type T[*A, **A] = None', "duplicate type parameter 'A'")
|
||||
|
||||
def test_name_non_collision_02(self):
|
||||
ns = run_code("""type TA1[A] = lambda A: A""")
|
||||
self.assertIsInstance(ns["TA1"], TypeAliasType)
|
||||
self.assertTrue(callable(ns["TA1"].__value__))
|
||||
self.assertEqual("arg", ns["TA1"].__value__("arg"))
|
||||
|
||||
def test_name_non_collision_03(self):
|
||||
ns = run_code("""
|
||||
class Outer[A]:
|
||||
type TA1[A] = None
|
||||
"""
|
||||
)
|
||||
outer_A, = ns["Outer"].__type_params__
|
||||
inner_A, = ns["Outer"].TA1.__type_params__
|
||||
self.assertIsNot(outer_A, inner_A)
|
||||
|
||||
|
||||
class TypeParamsAccessTest(unittest.TestCase):
|
||||
def test_alias_access_01(self):
|
||||
ns = run_code("type TA1[A, B] = dict[A, B]")
|
||||
alias = ns["TA1"]
|
||||
self.assertIsInstance(alias, TypeAliasType)
|
||||
self.assertEqual(alias.__type_params__, get_args(alias.__value__))
|
||||
|
||||
def test_alias_access_02(self):
|
||||
ns = run_code("""
|
||||
type TA1[A, B] = TA1[A, B] | int
|
||||
"""
|
||||
)
|
||||
alias = ns["TA1"]
|
||||
self.assertIsInstance(alias, TypeAliasType)
|
||||
A, B = alias.__type_params__
|
||||
self.assertEqual(alias.__value__, alias[A, B] | int)
|
||||
|
||||
def test_alias_access_03(self):
|
||||
ns = run_code("""
|
||||
class Outer[A]:
|
||||
def inner[B](self):
|
||||
type TA1[C] = TA1[A, B] | int
|
||||
return TA1
|
||||
"""
|
||||
)
|
||||
cls = ns["Outer"]
|
||||
A, = cls.__type_params__
|
||||
B, = cls.inner.__type_params__
|
||||
alias = cls.inner(None)
|
||||
self.assertIsInstance(alias, TypeAliasType)
|
||||
alias2 = cls.inner(None)
|
||||
self.assertIsNot(alias, alias2)
|
||||
self.assertEqual(len(alias.__type_params__), 1)
|
||||
|
||||
self.assertEqual(alias.__value__, alias[A, B] | int)
|
||||
|
||||
|
||||
class TypeParamsAliasValueTest(unittest.TestCase):
|
||||
def test_alias_value_01(self):
|
||||
type TA1 = int
|
||||
|
||||
self.assertIsInstance(TA1, TypeAliasType)
|
||||
self.assertEqual(TA1.__value__, int)
|
||||
self.assertEqual(TA1.__parameters__, ())
|
||||
self.assertEqual(TA1.__type_params__, ())
|
||||
|
||||
type TA2 = TA1 | str
|
||||
|
||||
self.assertIsInstance(TA2, TypeAliasType)
|
||||
a, b = TA2.__value__.__args__
|
||||
self.assertEqual(a, TA1)
|
||||
self.assertEqual(b, str)
|
||||
self.assertEqual(TA2.__parameters__, ())
|
||||
self.assertEqual(TA2.__type_params__, ())
|
||||
|
||||
def test_alias_value_02(self):
|
||||
class Parent[A]:
|
||||
type TA1[B] = dict[A, B]
|
||||
|
||||
self.assertIsInstance(Parent.TA1, TypeAliasType)
|
||||
self.assertEqual(len(Parent.TA1.__parameters__), 1)
|
||||
self.assertEqual(len(Parent.__parameters__), 1)
|
||||
a, = Parent.__parameters__
|
||||
b, = Parent.TA1.__parameters__
|
||||
self.assertEqual(Parent.__type_params__, (a,))
|
||||
self.assertEqual(Parent.TA1.__type_params__, (b,))
|
||||
self.assertEqual(Parent.TA1.__value__, dict[a, b])
|
||||
|
||||
def test_alias_value_03(self):
|
||||
def outer[A]():
|
||||
type TA1[B] = dict[A, B]
|
||||
return TA1
|
||||
|
||||
o = outer()
|
||||
self.assertIsInstance(o, TypeAliasType)
|
||||
self.assertEqual(len(o.__parameters__), 1)
|
||||
self.assertEqual(len(outer.__type_params__), 1)
|
||||
b = o.__parameters__[0]
|
||||
self.assertEqual(o.__type_params__, (b,))
|
||||
|
||||
def test_alias_value_04(self):
|
||||
def more_generic[T, *Ts, **P]():
|
||||
type TA[T2, *Ts2, **P2] = tuple[Callable[P, tuple[T, *Ts]], Callable[P2, tuple[T2, *Ts2]]]
|
||||
return TA
|
||||
|
||||
alias = more_generic()
|
||||
self.assertIsInstance(alias, TypeAliasType)
|
||||
T2, Ts2, P2 = alias.__type_params__
|
||||
self.assertEqual(alias.__parameters__, (T2, *Ts2, P2))
|
||||
T, Ts, P = more_generic.__type_params__
|
||||
self.assertEqual(alias.__value__, tuple[Callable[P, tuple[T, *Ts]], Callable[P2, tuple[T2, *Ts2]]])
|
||||
|
||||
def test_subscripting(self):
|
||||
type NonGeneric = int
|
||||
type Generic[A] = dict[A, A]
|
||||
type VeryGeneric[T, *Ts, **P] = Callable[P, tuple[T, *Ts]]
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
NonGeneric[int]
|
||||
|
||||
specialized = Generic[int]
|
||||
self.assertIsInstance(specialized, types.GenericAlias)
|
||||
self.assertIs(specialized.__origin__, Generic)
|
||||
self.assertEqual(specialized.__args__, (int,))
|
||||
|
||||
specialized2 = VeryGeneric[int, str, float, [bool, range]]
|
||||
self.assertIsInstance(specialized2, types.GenericAlias)
|
||||
self.assertIs(specialized2.__origin__, VeryGeneric)
|
||||
self.assertEqual(specialized2.__args__, (int, str, float, [bool, range]))
|
||||
|
||||
def test_repr(self):
|
||||
type Simple = int
|
||||
type VeryGeneric[T, *Ts, **P] = Callable[P, tuple[T, *Ts]]
|
||||
|
||||
self.assertEqual(repr(Simple), "Simple")
|
||||
self.assertEqual(repr(VeryGeneric), "VeryGeneric")
|
||||
self.assertEqual(repr(VeryGeneric[int, bytes, str, [float, object]]),
|
||||
"VeryGeneric[int, bytes, str, [float, object]]")
|
||||
self.assertEqual(repr(VeryGeneric[int, []]),
|
||||
"VeryGeneric[int, []]")
|
||||
self.assertEqual(repr(VeryGeneric[int, [VeryGeneric[int], list[str]]]),
|
||||
"VeryGeneric[int, [VeryGeneric[int], list[str]]]")
|
||||
|
||||
def test_recursive_repr(self):
|
||||
type Recursive = Recursive
|
||||
self.assertEqual(repr(Recursive), "Recursive")
|
||||
|
||||
type X = list[Y]
|
||||
type Y = list[X]
|
||||
self.assertEqual(repr(X), "X")
|
||||
self.assertEqual(repr(Y), "Y")
|
||||
|
||||
type GenericRecursive[X] = list[X | GenericRecursive[X]]
|
||||
self.assertEqual(repr(GenericRecursive), "GenericRecursive")
|
||||
self.assertEqual(repr(GenericRecursive[int]), "GenericRecursive[int]")
|
||||
self.assertEqual(repr(GenericRecursive[GenericRecursive[int]]),
|
||||
"GenericRecursive[GenericRecursive[int]]")
|
||||
|
||||
def test_raising(self):
|
||||
type MissingName = list[_My_X]
|
||||
with self.assertRaisesRegex(
|
||||
NameError,
|
||||
"cannot access free variable '_My_X' where it is not associated with a value",
|
||||
):
|
||||
MissingName.__value__
|
||||
_My_X = int
|
||||
self.assertEqual(MissingName.__value__, list[int])
|
||||
del _My_X
|
||||
# Cache should still work:
|
||||
self.assertEqual(MissingName.__value__, list[int])
|
||||
|
||||
# Explicit exception:
|
||||
type ExprException = 1 / 0
|
||||
with self.assertRaises(ZeroDivisionError):
|
||||
ExprException.__value__
|
||||
|
||||
|
||||
class TypeAliasConstructorTest(unittest.TestCase):
|
||||
def test_basic(self):
|
||||
TA = TypeAliasType("TA", int)
|
||||
self.assertEqual(TA.__name__, "TA")
|
||||
self.assertIs(TA.__value__, int)
|
||||
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.assertIs(TA.__value__, int)
|
||||
self.assertEqual(TA.__type_params__, ())
|
||||
self.assertIs(TA.__module__, None)
|
||||
|
||||
def test_generic(self):
|
||||
T = TypeVar("T")
|
||||
TA = TypeAliasType("TA", list[T], type_params=(T,))
|
||||
self.assertEqual(TA.__name__, "TA")
|
||||
self.assertEqual(TA.__value__, list[T])
|
||||
self.assertEqual(TA.__type_params__, (T,))
|
||||
self.assertEqual(TA.__module__, __name__)
|
||||
self.assertIs(type(TA[int]), types.GenericAlias)
|
||||
|
||||
def test_not_generic(self):
|
||||
TA = TypeAliasType("TA", list[int], type_params=())
|
||||
self.assertEqual(TA.__name__, "TA")
|
||||
self.assertEqual(TA.__value__, list[int])
|
||||
self.assertEqual(TA.__type_params__, ())
|
||||
self.assertEqual(TA.__module__, __name__)
|
||||
with self.assertRaisesRegex(
|
||||
TypeError,
|
||||
"Only generic type aliases are subscriptable",
|
||||
):
|
||||
TA[int]
|
||||
|
||||
def test_type_params_order_with_defaults(self):
|
||||
HasNoDefaultT = TypeVar("HasNoDefaultT")
|
||||
WithDefaultT = TypeVar("WithDefaultT", default=int)
|
||||
|
||||
HasNoDefaultP = ParamSpec("HasNoDefaultP")
|
||||
WithDefaultP = ParamSpec("WithDefaultP", default=HasNoDefaultP)
|
||||
|
||||
HasNoDefaultTT = TypeVarTuple("HasNoDefaultTT")
|
||||
WithDefaultTT = TypeVarTuple("WithDefaultTT", default=HasNoDefaultTT)
|
||||
|
||||
for type_params in [
|
||||
(HasNoDefaultT, WithDefaultT),
|
||||
(HasNoDefaultP, WithDefaultP),
|
||||
(HasNoDefaultTT, WithDefaultTT),
|
||||
]:
|
||||
with self.subTest(type_params=type_params):
|
||||
TypeAliasType("A", int, type_params=type_params) # ok
|
||||
|
||||
msg = "follows default type parameter"
|
||||
for type_params in [
|
||||
(WithDefaultT, HasNoDefaultT),
|
||||
(WithDefaultP, HasNoDefaultP),
|
||||
(WithDefaultTT, HasNoDefaultTT),
|
||||
(WithDefaultT, HasNoDefaultP), # different types
|
||||
]:
|
||||
with self.subTest(type_params=type_params):
|
||||
with self.assertRaisesRegex(TypeError, msg):
|
||||
TypeAliasType("A", int, type_params=type_params)
|
||||
|
||||
def test_expects_type_like(self):
|
||||
T = TypeVar("T")
|
||||
|
||||
msg = "Expected a type param"
|
||||
with self.assertRaisesRegex(TypeError, msg):
|
||||
TypeAliasType("A", int, type_params=(1,))
|
||||
with self.assertRaisesRegex(TypeError, msg):
|
||||
TypeAliasType("A", int, type_params=(1, 2))
|
||||
with self.assertRaisesRegex(TypeError, msg):
|
||||
TypeAliasType("A", int, type_params=(T, 2))
|
||||
|
||||
def test_keywords(self):
|
||||
TA = TypeAliasType(name="TA", value=int)
|
||||
self.assertEqual(TA.__name__, "TA")
|
||||
self.assertIs(TA.__value__, int)
|
||||
self.assertEqual(TA.__type_params__, ())
|
||||
self.assertEqual(TA.__module__, __name__)
|
||||
|
||||
def test_errors(self):
|
||||
with self.assertRaises(TypeError):
|
||||
TypeAliasType()
|
||||
with self.assertRaises(TypeError):
|
||||
TypeAliasType("TA")
|
||||
with self.assertRaises(TypeError):
|
||||
TypeAliasType("TA", list, ())
|
||||
with self.assertRaises(TypeError):
|
||||
TypeAliasType("TA", list, type_params=42)
|
||||
|
||||
|
||||
class TypeAliasTypeTest(unittest.TestCase):
|
||||
def test_immutable(self):
|
||||
with self.assertRaises(TypeError):
|
||||
TypeAliasType.whatever = "not allowed"
|
||||
|
||||
def test_no_subclassing(self):
|
||||
with self.assertRaisesRegex(TypeError, "not an acceptable base type"):
|
||||
class MyAlias(TypeAliasType):
|
||||
pass
|
||||
|
||||
def test_union(self):
|
||||
type Alias1 = int
|
||||
type Alias2 = str
|
||||
union = Alias1 | Alias2
|
||||
self.assertIsInstance(union, types.UnionType)
|
||||
self.assertEqual(get_args(union), (Alias1, Alias2))
|
||||
union2 = Alias1 | list[float]
|
||||
self.assertIsInstance(union2, types.UnionType)
|
||||
self.assertEqual(get_args(union2), (Alias1, list[float]))
|
||||
union3 = list[range] | Alias1
|
||||
self.assertIsInstance(union3, types.UnionType)
|
||||
self.assertEqual(get_args(union3), (list[range], Alias1))
|
||||
|
||||
def test_module(self):
|
||||
self.assertEqual(TypeAliasType.__module__, "typing")
|
||||
type Alias = int
|
||||
self.assertEqual(Alias.__module__, __name__)
|
||||
self.assertEqual(mod_generics_cache.Alias.__module__,
|
||||
mod_generics_cache.__name__)
|
||||
self.assertEqual(mod_generics_cache.OldStyle.__module__,
|
||||
mod_generics_cache.__name__)
|
||||
|
||||
def test_unpack(self):
|
||||
type Alias = tuple[int, int]
|
||||
unpacked = (*Alias,)[0]
|
||||
self.assertEqual(unpacked, Unpack[Alias])
|
||||
|
||||
class Foo[*Ts]:
|
||||
pass
|
||||
|
||||
x = Foo[str, *Alias]
|
||||
self.assertEqual(x.__args__, (str, Unpack[Alias]))
|
||||
|
||||
|
||||
# All these type aliases are used for pickling tests:
|
||||
T = TypeVar('T')
|
||||
type SimpleAlias = int
|
||||
type RecursiveAlias = dict[str, RecursiveAlias]
|
||||
type GenericAlias[X] = list[X]
|
||||
type GenericAliasMultipleTypes[X, Y] = dict[X, Y]
|
||||
type RecursiveGenericAlias[X] = dict[str, RecursiveAlias[X]]
|
||||
type BoundGenericAlias[X: int] = set[X]
|
||||
type ConstrainedGenericAlias[LongName: (str, bytes)] = list[LongName]
|
||||
type AllTypesAlias[A, *B, **C] = Callable[C, A] | tuple[*B]
|
||||
|
||||
|
||||
class TypeAliasPickleTest(unittest.TestCase):
|
||||
def test_pickling(self):
|
||||
things_to_test = [
|
||||
SimpleAlias,
|
||||
RecursiveAlias,
|
||||
|
||||
GenericAlias,
|
||||
GenericAlias[T],
|
||||
GenericAlias[int],
|
||||
|
||||
GenericAliasMultipleTypes,
|
||||
GenericAliasMultipleTypes[str, T],
|
||||
GenericAliasMultipleTypes[T, str],
|
||||
GenericAliasMultipleTypes[int, str],
|
||||
|
||||
RecursiveGenericAlias,
|
||||
RecursiveGenericAlias[T],
|
||||
RecursiveGenericAlias[int],
|
||||
|
||||
BoundGenericAlias,
|
||||
BoundGenericAlias[int],
|
||||
BoundGenericAlias[T],
|
||||
|
||||
ConstrainedGenericAlias,
|
||||
ConstrainedGenericAlias[str],
|
||||
ConstrainedGenericAlias[T],
|
||||
|
||||
AllTypesAlias,
|
||||
AllTypesAlias[int, str, T, [T, object]],
|
||||
|
||||
# Other modules:
|
||||
mod_generics_cache.Alias,
|
||||
mod_generics_cache.OldStyle,
|
||||
]
|
||||
for thing in things_to_test:
|
||||
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
|
||||
with self.subTest(thing=thing, proto=proto):
|
||||
pickled = pickle.dumps(thing, protocol=proto)
|
||||
self.assertEqual(pickle.loads(pickled), thing)
|
||||
|
||||
type ClassLevel = str
|
||||
|
||||
def test_pickling_local(self):
|
||||
type A = int
|
||||
things_to_test = [
|
||||
self.ClassLevel,
|
||||
A,
|
||||
]
|
||||
for thing in things_to_test:
|
||||
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
|
||||
with self.subTest(thing=thing, proto=proto):
|
||||
with self.assertRaises(pickle.PickleError):
|
||||
pickle.dumps(thing, protocol=proto)
|
||||
|
||||
|
||||
class TypeParamsExoticGlobalsTest(unittest.TestCase):
|
||||
def test_exec_with_unusual_globals(self):
|
||||
class customdict(dict):
|
||||
def __missing__(self, key):
|
||||
return key
|
||||
|
||||
code = compile("type Alias = undefined", "test", "exec")
|
||||
ns = customdict()
|
||||
exec(code, ns)
|
||||
Alias = ns["Alias"]
|
||||
self.assertEqual(Alias.__value__, "undefined")
|
||||
|
||||
code = compile("class A: type Alias = undefined", "test", "exec")
|
||||
ns = customdict()
|
||||
exec(code, ns)
|
||||
Alias = ns["A"].Alias
|
||||
self.assertEqual(Alias.__value__, "undefined")
|
||||
Vendored
+877
@@ -0,0 +1,877 @@
|
||||
import annotationlib
|
||||
import inspect
|
||||
import textwrap
|
||||
import types
|
||||
import unittest
|
||||
from test.support import run_code, check_syntax_error, import_helper, cpython_only
|
||||
from test.test_inspect import inspect_stringized_annotations
|
||||
|
||||
|
||||
class TypeAnnotationTests(unittest.TestCase):
|
||||
|
||||
def test_lazy_create_annotations(self):
|
||||
# type objects lazy create their __annotations__ dict on demand.
|
||||
# the annotations dict is stored in type.__dict__ (as __annotations_cache__).
|
||||
# a freshly created type shouldn't have an annotations dict yet.
|
||||
foo = type("Foo", (), {})
|
||||
for i in range(3):
|
||||
self.assertFalse("__annotations_cache__" in foo.__dict__)
|
||||
d = foo.__annotations__
|
||||
self.assertTrue("__annotations_cache__" in foo.__dict__)
|
||||
self.assertEqual(foo.__annotations__, d)
|
||||
self.assertEqual(foo.__dict__['__annotations_cache__'], d)
|
||||
del foo.__annotations__
|
||||
|
||||
def test_setting_annotations(self):
|
||||
foo = type("Foo", (), {})
|
||||
for i in range(3):
|
||||
self.assertFalse("__annotations_cache__" in foo.__dict__)
|
||||
d = {'a': int}
|
||||
foo.__annotations__ = d
|
||||
self.assertTrue("__annotations_cache__" in foo.__dict__)
|
||||
self.assertEqual(foo.__annotations__, d)
|
||||
self.assertEqual(foo.__dict__['__annotations_cache__'], d)
|
||||
del foo.__annotations__
|
||||
|
||||
def test_annotations_getset_raises(self):
|
||||
# builtin types don't have __annotations__ (yet!)
|
||||
with self.assertRaises(AttributeError):
|
||||
print(float.__annotations__)
|
||||
with self.assertRaises(TypeError):
|
||||
float.__annotations__ = {}
|
||||
with self.assertRaises(TypeError):
|
||||
del float.__annotations__
|
||||
|
||||
# double delete
|
||||
foo = type("Foo", (), {})
|
||||
foo.__annotations__ = {}
|
||||
del foo.__annotations__
|
||||
with self.assertRaises(AttributeError):
|
||||
del foo.__annotations__
|
||||
|
||||
def test_annotations_are_created_correctly(self):
|
||||
class C:
|
||||
a:int=3
|
||||
b:str=4
|
||||
self.assertEqual(C.__annotations__, {"a": int, "b": str})
|
||||
self.assertTrue("__annotations_cache__" in C.__dict__)
|
||||
del C.__annotations__
|
||||
self.assertFalse("__annotations_cache__" in C.__dict__)
|
||||
|
||||
def test_pep563_annotations(self):
|
||||
isa = inspect_stringized_annotations
|
||||
self.assertEqual(
|
||||
isa.__annotations__, {"a": "int", "b": "str"},
|
||||
)
|
||||
self.assertEqual(
|
||||
isa.MyClass.__annotations__, {"a": "int", "b": "str"},
|
||||
)
|
||||
|
||||
def test_explicitly_set_annotations(self):
|
||||
class C:
|
||||
__annotations__ = {"what": int}
|
||||
self.assertEqual(C.__annotations__, {"what": int})
|
||||
|
||||
def test_explicitly_set_annotate(self):
|
||||
class C:
|
||||
__annotate__ = lambda format: {"what": int}
|
||||
self.assertEqual(C.__annotations__, {"what": int})
|
||||
self.assertIsInstance(C.__annotate__, types.FunctionType)
|
||||
self.assertEqual(C.__annotate__(annotationlib.Format.VALUE), {"what": int})
|
||||
|
||||
def test_del_annotations_and_annotate(self):
|
||||
# gh-132285
|
||||
called = False
|
||||
class A:
|
||||
def __annotate__(format):
|
||||
nonlocal called
|
||||
called = True
|
||||
return {'a': int}
|
||||
|
||||
self.assertEqual(A.__annotations__, {'a': int})
|
||||
self.assertTrue(called)
|
||||
self.assertTrue(A.__annotate__)
|
||||
|
||||
del A.__annotations__
|
||||
called = False
|
||||
|
||||
self.assertEqual(A.__annotations__, {})
|
||||
self.assertFalse(called)
|
||||
self.assertIs(A.__annotate__, None)
|
||||
|
||||
def test_descriptor_still_works(self):
|
||||
class C:
|
||||
def __init__(self, name=None, bases=None, d=None):
|
||||
self.my_annotations = None
|
||||
|
||||
@property
|
||||
def __annotations__(self):
|
||||
if not hasattr(self, 'my_annotations'):
|
||||
self.my_annotations = {}
|
||||
if not isinstance(self.my_annotations, dict):
|
||||
self.my_annotations = {}
|
||||
return self.my_annotations
|
||||
|
||||
@__annotations__.setter
|
||||
def __annotations__(self, value):
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("can only set __annotations__ to a dict")
|
||||
self.my_annotations = value
|
||||
|
||||
@__annotations__.deleter
|
||||
def __annotations__(self):
|
||||
if getattr(self, 'my_annotations', False) is None:
|
||||
raise AttributeError('__annotations__')
|
||||
self.my_annotations = None
|
||||
|
||||
c = C()
|
||||
self.assertEqual(c.__annotations__, {})
|
||||
d = {'a':'int'}
|
||||
c.__annotations__ = d
|
||||
self.assertEqual(c.__annotations__, d)
|
||||
with self.assertRaises(ValueError):
|
||||
c.__annotations__ = 123
|
||||
del c.__annotations__
|
||||
with self.assertRaises(AttributeError):
|
||||
del c.__annotations__
|
||||
self.assertEqual(c.__annotations__, {})
|
||||
|
||||
|
||||
class D(metaclass=C):
|
||||
pass
|
||||
|
||||
self.assertEqual(D.__annotations__, {})
|
||||
d = {'a':'int'}
|
||||
D.__annotations__ = d
|
||||
self.assertEqual(D.__annotations__, d)
|
||||
with self.assertRaises(ValueError):
|
||||
D.__annotations__ = 123
|
||||
del D.__annotations__
|
||||
with self.assertRaises(AttributeError):
|
||||
del D.__annotations__
|
||||
self.assertEqual(D.__annotations__, {})
|
||||
|
||||
def test_partially_executed_module(self):
|
||||
partialexe = import_helper.import_fresh_module("test.typinganndata.partialexecution")
|
||||
self.assertEqual(
|
||||
partialexe.a.__annotations__,
|
||||
{"v1": int, "v2": int},
|
||||
)
|
||||
self.assertEqual(partialexe.b.annos, {"v1": int})
|
||||
|
||||
@cpython_only
|
||||
def test_no_cell(self):
|
||||
# gh-130924: Test that uses of annotations in local scopes do not
|
||||
# create cell variables.
|
||||
def f(x):
|
||||
a: x
|
||||
return x
|
||||
|
||||
self.assertEqual(f.__code__.co_cellvars, ())
|
||||
|
||||
|
||||
def build_module(code: str, name: str = "top") -> types.ModuleType:
|
||||
ns = run_code(code)
|
||||
mod = types.ModuleType(name)
|
||||
mod.__dict__.update(ns)
|
||||
return mod
|
||||
|
||||
|
||||
class TestSetupAnnotations(unittest.TestCase):
|
||||
def check(self, code: str):
|
||||
code = textwrap.dedent(code)
|
||||
for scope in ("module", "class"):
|
||||
with self.subTest(scope=scope):
|
||||
if scope == "class":
|
||||
code = f"class C:\n{textwrap.indent(code, ' ')}"
|
||||
ns = run_code(code)
|
||||
annotations = ns["C"].__annotations__
|
||||
else:
|
||||
annotations = build_module(code).__annotations__
|
||||
self.assertEqual(annotations, {"x": int})
|
||||
|
||||
def test_top_level(self):
|
||||
self.check("x: int = 1")
|
||||
|
||||
def test_blocks(self):
|
||||
self.check("if True:\n x: int = 1")
|
||||
self.check("""
|
||||
while True:
|
||||
x: int = 1
|
||||
break
|
||||
""")
|
||||
self.check("""
|
||||
while False:
|
||||
pass
|
||||
else:
|
||||
x: int = 1
|
||||
""")
|
||||
self.check("""
|
||||
for i in range(1):
|
||||
x: int = 1
|
||||
""")
|
||||
self.check("""
|
||||
for i in range(1):
|
||||
pass
|
||||
else:
|
||||
x: int = 1
|
||||
""")
|
||||
|
||||
def test_try(self):
|
||||
self.check("""
|
||||
try:
|
||||
x: int = 1
|
||||
except:
|
||||
pass
|
||||
""")
|
||||
self.check("""
|
||||
try:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
x: int = 1
|
||||
""")
|
||||
self.check("""
|
||||
try:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
x: int = 1
|
||||
""")
|
||||
self.check("""
|
||||
try:
|
||||
1/0
|
||||
except:
|
||||
x: int = 1
|
||||
""")
|
||||
|
||||
def test_try_star(self):
|
||||
self.check("""
|
||||
try:
|
||||
x: int = 1
|
||||
except* Exception:
|
||||
pass
|
||||
""")
|
||||
self.check("""
|
||||
try:
|
||||
pass
|
||||
except* Exception:
|
||||
pass
|
||||
else:
|
||||
x: int = 1
|
||||
""")
|
||||
self.check("""
|
||||
try:
|
||||
pass
|
||||
except* Exception:
|
||||
pass
|
||||
finally:
|
||||
x: int = 1
|
||||
""")
|
||||
self.check("""
|
||||
try:
|
||||
1/0
|
||||
except* Exception:
|
||||
x: int = 1
|
||||
""")
|
||||
|
||||
def test_match(self):
|
||||
self.check("""
|
||||
match 0:
|
||||
case 0:
|
||||
x: int = 1
|
||||
""")
|
||||
|
||||
|
||||
class AnnotateTests(unittest.TestCase):
|
||||
"""See PEP 649."""
|
||||
def test_manual_annotate(self):
|
||||
def f():
|
||||
pass
|
||||
mod = types.ModuleType("mod")
|
||||
class X:
|
||||
pass
|
||||
|
||||
for obj in (f, mod, X):
|
||||
with self.subTest(obj=obj):
|
||||
self.check_annotations(obj)
|
||||
|
||||
def check_annotations(self, f):
|
||||
self.assertEqual(f.__annotations__, {})
|
||||
self.assertIs(f.__annotate__, None)
|
||||
|
||||
with self.assertRaisesRegex(TypeError, "__annotate__ must be callable or None"):
|
||||
f.__annotate__ = 42
|
||||
f.__annotate__ = lambda: 42
|
||||
with self.assertRaisesRegex(TypeError, r"takes 0 positional arguments but 1 was given"):
|
||||
print(f.__annotations__)
|
||||
|
||||
f.__annotate__ = lambda x: 42
|
||||
with self.assertRaisesRegex(TypeError, r"__annotate__ returned non-dict of type 'int'"):
|
||||
print(f.__annotations__)
|
||||
|
||||
f.__annotate__ = lambda x: {"x": x}
|
||||
self.assertEqual(f.__annotations__, {"x": 1})
|
||||
|
||||
# Setting annotate to None does not invalidate the cached __annotations__
|
||||
f.__annotate__ = None
|
||||
self.assertEqual(f.__annotations__, {"x": 1})
|
||||
|
||||
# But setting it to a new callable does
|
||||
f.__annotate__ = lambda x: {"y": x}
|
||||
self.assertEqual(f.__annotations__, {"y": 1})
|
||||
|
||||
# Setting f.__annotations__ also clears __annotate__
|
||||
f.__annotations__ = {"z": 43}
|
||||
self.assertIs(f.__annotate__, None)
|
||||
|
||||
def test_user_defined_annotate(self):
|
||||
class X:
|
||||
a: int
|
||||
|
||||
def __annotate__(format):
|
||||
return {"a": str}
|
||||
self.assertEqual(X.__annotate__(annotationlib.Format.VALUE), {"a": str})
|
||||
self.assertEqual(annotationlib.get_annotations(X), {"a": str})
|
||||
|
||||
mod = build_module(
|
||||
"""
|
||||
a: int
|
||||
def __annotate__(format):
|
||||
return {"a": str}
|
||||
"""
|
||||
)
|
||||
self.assertEqual(mod.__annotate__(annotationlib.Format.VALUE), {"a": str})
|
||||
self.assertEqual(annotationlib.get_annotations(mod), {"a": str})
|
||||
|
||||
|
||||
class DeferredEvaluationTests(unittest.TestCase):
|
||||
def test_function(self):
|
||||
def func(x: undefined, /, y: undefined, *args: undefined, z: undefined, **kwargs: undefined) -> undefined:
|
||||
pass
|
||||
|
||||
with self.assertRaises(NameError):
|
||||
func.__annotations__
|
||||
|
||||
undefined = 1
|
||||
self.assertEqual(func.__annotations__, {
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"args": 1,
|
||||
"z": 1,
|
||||
"kwargs": 1,
|
||||
"return": 1,
|
||||
})
|
||||
|
||||
def test_async_function(self):
|
||||
async def func(x: undefined, /, y: undefined, *args: undefined, z: undefined, **kwargs: undefined) -> undefined:
|
||||
pass
|
||||
|
||||
with self.assertRaises(NameError):
|
||||
func.__annotations__
|
||||
|
||||
undefined = 1
|
||||
self.assertEqual(func.__annotations__, {
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"args": 1,
|
||||
"z": 1,
|
||||
"kwargs": 1,
|
||||
"return": 1,
|
||||
})
|
||||
|
||||
def test_class(self):
|
||||
class X:
|
||||
a: undefined
|
||||
|
||||
with self.assertRaises(NameError):
|
||||
X.__annotations__
|
||||
|
||||
undefined = 1
|
||||
self.assertEqual(X.__annotations__, {"a": 1})
|
||||
|
||||
def test_module(self):
|
||||
ns = run_code("x: undefined = 1")
|
||||
anno = ns["__annotate__"]
|
||||
with self.assertRaises(NotImplementedError):
|
||||
anno(3)
|
||||
|
||||
with self.assertRaises(NameError):
|
||||
anno(1)
|
||||
|
||||
ns["undefined"] = 1
|
||||
self.assertEqual(anno(1), {"x": 1})
|
||||
|
||||
def test_class_scoping(self):
|
||||
class Outer:
|
||||
def meth(self, x: Nested): ...
|
||||
x: Nested
|
||||
class Nested: ...
|
||||
|
||||
self.assertEqual(Outer.meth.__annotations__, {"x": Outer.Nested})
|
||||
self.assertEqual(Outer.__annotations__, {"x": Outer.Nested})
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test_no_exotic_expressions(self):
|
||||
preludes = [
|
||||
"",
|
||||
"class X:\n ",
|
||||
"def f():\n ",
|
||||
"async def f():\n ",
|
||||
]
|
||||
for prelude in preludes:
|
||||
with self.subTest(prelude=prelude):
|
||||
check_syntax_error(self, prelude + "def func(x: (yield)): ...", "yield expression cannot be used within an annotation")
|
||||
check_syntax_error(self, prelude + "def func(x: (yield from x)): ...", "yield expression cannot be used within an annotation")
|
||||
check_syntax_error(self, prelude + "def func(x: (y := 3)): ...", "named expression cannot be used within an annotation")
|
||||
check_syntax_error(self, prelude + "def func(x: (await 42)): ...", "await expression cannot be used within an annotation")
|
||||
check_syntax_error(self, prelude + "def func(x: [y async for y in x]): ...", "asynchronous comprehension outside of an asynchronous function")
|
||||
check_syntax_error(self, prelude + "def func(x: {y async for y in x}): ...", "asynchronous comprehension outside of an asynchronous function")
|
||||
check_syntax_error(self, prelude + "def func(x: {y: y async for y in x}): ...", "asynchronous comprehension outside of an asynchronous function")
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test_no_exotic_expressions_in_unevaluated_annotations(self):
|
||||
preludes = [
|
||||
"",
|
||||
"class X: ",
|
||||
"def f(): ",
|
||||
"async def f(): ",
|
||||
]
|
||||
for prelude in preludes:
|
||||
with self.subTest(prelude=prelude):
|
||||
check_syntax_error(self, prelude + "(x): (yield)", "yield expression cannot be used within an annotation")
|
||||
check_syntax_error(self, prelude + "(x): (yield from x)", "yield expression cannot be used within an annotation")
|
||||
check_syntax_error(self, prelude + "(x): (y := 3)", "named expression cannot be used within an annotation")
|
||||
check_syntax_error(self, prelude + "(x): (__debug__ := 3)", "named expression cannot be used within an annotation")
|
||||
check_syntax_error(self, prelude + "(x): (await 42)", "await expression cannot be used within an annotation")
|
||||
check_syntax_error(self, prelude + "(x): [y async for y in x]", "asynchronous comprehension outside of an asynchronous function")
|
||||
check_syntax_error(self, prelude + "(x): {y async for y in x}", "asynchronous comprehension outside of an asynchronous function")
|
||||
check_syntax_error(self, prelude + "(x): {y: y async for y in x}", "asynchronous comprehension outside of an asynchronous function")
|
||||
|
||||
def test_ignore_non_simple_annotations(self):
|
||||
ns = run_code("class X: (y): int")
|
||||
self.assertEqual(ns["X"].__annotations__, {})
|
||||
ns = run_code("class X: int.b: int")
|
||||
self.assertEqual(ns["X"].__annotations__, {})
|
||||
ns = run_code("class X: int[str]: int")
|
||||
self.assertEqual(ns["X"].__annotations__, {})
|
||||
|
||||
def test_generated_annotate(self):
|
||||
def func(x: int):
|
||||
pass
|
||||
class X:
|
||||
x: int
|
||||
mod = build_module("x: int")
|
||||
for obj in (func, X, mod):
|
||||
with self.subTest(obj=obj):
|
||||
annotate = obj.__annotate__
|
||||
self.assertIsInstance(annotate, types.FunctionType)
|
||||
self.assertEqual(annotate.__name__, "__annotate__")
|
||||
with self.assertRaises(NotImplementedError):
|
||||
annotate(annotationlib.Format.FORWARDREF)
|
||||
with self.assertRaises(NotImplementedError):
|
||||
annotate(annotationlib.Format.STRING)
|
||||
with self.assertRaises(TypeError):
|
||||
annotate(None)
|
||||
self.assertEqual(annotate(annotationlib.Format.VALUE), {"x": int})
|
||||
|
||||
sig = inspect.signature(annotate)
|
||||
self.assertEqual(sig, inspect.Signature([
|
||||
inspect.Parameter("format", inspect.Parameter.POSITIONAL_ONLY)
|
||||
]))
|
||||
|
||||
def test_comprehension_in_annotation(self):
|
||||
# This crashed in an earlier version of the code
|
||||
ns = run_code("x: [y for y in range(10)]")
|
||||
self.assertEqual(ns["__annotate__"](1), {"x": list(range(10))})
|
||||
|
||||
def test_future_annotations(self):
|
||||
code = """
|
||||
from __future__ import annotations
|
||||
|
||||
def f(x: int) -> int: pass
|
||||
"""
|
||||
ns = run_code(code)
|
||||
f = ns["f"]
|
||||
self.assertIsInstance(f.__annotate__, types.FunctionType)
|
||||
annos = {"x": "int", "return": "int"}
|
||||
self.assertEqual(f.__annotate__(annotationlib.Format.VALUE), annos)
|
||||
self.assertEqual(f.__annotations__, annos)
|
||||
|
||||
def test_set_annotations(self):
|
||||
function_code = textwrap.dedent("""
|
||||
def f(x: int):
|
||||
pass
|
||||
""")
|
||||
class_code = textwrap.dedent("""
|
||||
class f:
|
||||
x: int
|
||||
""")
|
||||
for future in (False, True):
|
||||
for label, code in (("function", function_code), ("class", class_code)):
|
||||
with self.subTest(future=future, label=label):
|
||||
if future:
|
||||
code = "from __future__ import annotations\n" + code
|
||||
ns = run_code(code)
|
||||
f = ns["f"]
|
||||
anno = "int" if future else int
|
||||
self.assertEqual(f.__annotations__, {"x": anno})
|
||||
|
||||
f.__annotations__ = {"x": str}
|
||||
self.assertEqual(f.__annotations__, {"x": str})
|
||||
|
||||
def test_name_clash_with_format(self):
|
||||
# this test would fail if __annotate__'s parameter was called "format"
|
||||
# during symbol table construction
|
||||
code = """
|
||||
class format: pass
|
||||
|
||||
def f(x: format): pass
|
||||
"""
|
||||
ns = run_code(code)
|
||||
f = ns["f"]
|
||||
self.assertEqual(f.__annotations__, {"x": ns["format"]})
|
||||
|
||||
code = """
|
||||
class Outer:
|
||||
class format: pass
|
||||
|
||||
def meth(self, x: format): ...
|
||||
"""
|
||||
ns = run_code(code)
|
||||
self.assertEqual(ns["Outer"].meth.__annotations__, {"x": ns["Outer"].format})
|
||||
|
||||
code = """
|
||||
def f(format):
|
||||
def inner(x: format): pass
|
||||
return inner
|
||||
res = f("closure var")
|
||||
"""
|
||||
ns = run_code(code)
|
||||
self.assertEqual(ns["res"].__annotations__, {"x": "closure var"})
|
||||
|
||||
code = """
|
||||
def f(x: format):
|
||||
pass
|
||||
"""
|
||||
ns = run_code(code)
|
||||
# picks up the format() builtin
|
||||
self.assertEqual(ns["f"].__annotations__, {"x": format})
|
||||
|
||||
code = """
|
||||
def outer():
|
||||
def f(x: format):
|
||||
pass
|
||||
if False:
|
||||
class format: pass
|
||||
return f
|
||||
f = outer()
|
||||
"""
|
||||
ns = run_code(code)
|
||||
with self.assertRaisesRegex(
|
||||
NameError,
|
||||
"cannot access free variable 'format' where it is not associated with a value in enclosing scope",
|
||||
):
|
||||
ns["f"].__annotations__
|
||||
|
||||
|
||||
class ConditionalAnnotationTests(unittest.TestCase):
|
||||
def check_scopes(self, code, true_annos, false_annos):
|
||||
for scope in ("class", "module"):
|
||||
for (cond, expected) in (
|
||||
# Constants (so code might get optimized out)
|
||||
(True, true_annos), (False, false_annos),
|
||||
# Non-constant expressions
|
||||
("not not len", true_annos), ("not len", false_annos),
|
||||
):
|
||||
with self.subTest(scope=scope, cond=cond):
|
||||
code_to_run = code.format(cond=cond)
|
||||
if scope == "class":
|
||||
code_to_run = "class Cls:\n" + textwrap.indent(textwrap.dedent(code_to_run), " " * 4)
|
||||
ns = run_code(code_to_run)
|
||||
if scope == "class":
|
||||
self.assertEqual(ns["Cls"].__annotations__, expected)
|
||||
else:
|
||||
self.assertEqual(ns["__annotate__"](annotationlib.Format.VALUE),
|
||||
expected)
|
||||
|
||||
def test_with(self):
|
||||
code = """
|
||||
class Swallower:
|
||||
def __enter__(self):
|
||||
pass
|
||||
|
||||
def __exit__(self, *args):
|
||||
return True
|
||||
|
||||
with Swallower():
|
||||
if {cond}:
|
||||
about_to_raise: int
|
||||
raise Exception
|
||||
in_with: "with"
|
||||
"""
|
||||
self.check_scopes(code, {"about_to_raise": int}, {"in_with": "with"})
|
||||
|
||||
def test_simple_if(self):
|
||||
code = """
|
||||
if {cond}:
|
||||
in_if: "if"
|
||||
else:
|
||||
in_if: "else"
|
||||
"""
|
||||
self.check_scopes(code, {"in_if": "if"}, {"in_if": "else"})
|
||||
|
||||
def test_if_elif(self):
|
||||
code = """
|
||||
if not len:
|
||||
in_if: "if"
|
||||
elif {cond}:
|
||||
in_elif: "elif"
|
||||
else:
|
||||
in_else: "else"
|
||||
"""
|
||||
self.check_scopes(
|
||||
code,
|
||||
{"in_elif": "elif"},
|
||||
{"in_else": "else"}
|
||||
)
|
||||
|
||||
def test_try(self):
|
||||
code = """
|
||||
try:
|
||||
if {cond}:
|
||||
raise Exception
|
||||
in_try: "try"
|
||||
except Exception:
|
||||
in_except: "except"
|
||||
finally:
|
||||
in_finally: "finally"
|
||||
"""
|
||||
self.check_scopes(
|
||||
code,
|
||||
{"in_except": "except", "in_finally": "finally"},
|
||||
{"in_try": "try", "in_finally": "finally"}
|
||||
)
|
||||
|
||||
def test_try_star(self):
|
||||
code = """
|
||||
try:
|
||||
if {cond}:
|
||||
raise Exception
|
||||
in_try_star: "try"
|
||||
except* Exception:
|
||||
in_except_star: "except"
|
||||
finally:
|
||||
in_finally: "finally"
|
||||
"""
|
||||
self.check_scopes(
|
||||
code,
|
||||
{"in_except_star": "except", "in_finally": "finally"},
|
||||
{"in_try_star": "try", "in_finally": "finally"}
|
||||
)
|
||||
|
||||
def test_while(self):
|
||||
code = """
|
||||
while {cond}:
|
||||
in_while: "while"
|
||||
break
|
||||
else:
|
||||
in_else: "else"
|
||||
"""
|
||||
self.check_scopes(
|
||||
code,
|
||||
{"in_while": "while"},
|
||||
{"in_else": "else"}
|
||||
)
|
||||
|
||||
def test_for(self):
|
||||
code = """
|
||||
for _ in ([1] if {cond} else []):
|
||||
in_for: "for"
|
||||
else:
|
||||
in_else: "else"
|
||||
"""
|
||||
self.check_scopes(
|
||||
code,
|
||||
{"in_for": "for", "in_else": "else"},
|
||||
{"in_else": "else"}
|
||||
)
|
||||
|
||||
def test_match(self):
|
||||
code = """
|
||||
match {cond}:
|
||||
case True:
|
||||
x: "true"
|
||||
case False:
|
||||
x: "false"
|
||||
"""
|
||||
self.check_scopes(
|
||||
code,
|
||||
{"x": "true"},
|
||||
{"x": "false"}
|
||||
)
|
||||
|
||||
def test_nesting_override(self):
|
||||
code = """
|
||||
if {cond}:
|
||||
x: "foo"
|
||||
if {cond}:
|
||||
x: "bar"
|
||||
"""
|
||||
self.check_scopes(
|
||||
code,
|
||||
{"x": "bar"},
|
||||
{}
|
||||
)
|
||||
|
||||
def test_nesting_outer(self):
|
||||
code = """
|
||||
if {cond}:
|
||||
outer_before: "outer_before"
|
||||
if len:
|
||||
inner_if: "inner_if"
|
||||
else:
|
||||
inner_else: "inner_else"
|
||||
outer_after: "outer_after"
|
||||
"""
|
||||
self.check_scopes(
|
||||
code,
|
||||
{"outer_before": "outer_before", "inner_if": "inner_if",
|
||||
"outer_after": "outer_after"},
|
||||
{}
|
||||
)
|
||||
|
||||
def test_nesting_inner(self):
|
||||
code = """
|
||||
if len:
|
||||
outer_before: "outer_before"
|
||||
if {cond}:
|
||||
inner_if: "inner_if"
|
||||
else:
|
||||
inner_else: "inner_else"
|
||||
outer_after: "outer_after"
|
||||
"""
|
||||
self.check_scopes(
|
||||
code,
|
||||
{"outer_before": "outer_before", "inner_if": "inner_if",
|
||||
"outer_after": "outer_after"},
|
||||
{"outer_before": "outer_before", "inner_else": "inner_else",
|
||||
"outer_after": "outer_after"},
|
||||
)
|
||||
|
||||
def test_non_name_annotations(self):
|
||||
code = """
|
||||
before: "before"
|
||||
if {cond}:
|
||||
a = "x"
|
||||
a[0]: int
|
||||
else:
|
||||
a = object()
|
||||
a.b: str
|
||||
after: "after"
|
||||
"""
|
||||
expected = {"before": "before", "after": "after"}
|
||||
self.check_scopes(code, expected, expected)
|
||||
|
||||
|
||||
class RegressionTests(unittest.TestCase):
|
||||
# gh-132479
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; SyntaxError: the symbol 'unique_name_6' must be present in the symbol table
|
||||
def test_complex_comprehension_inlining(self):
|
||||
# Test that the various repro cases from the issue don't crash
|
||||
cases = [
|
||||
"""
|
||||
(unique_name_0): 0
|
||||
unique_name_1: (
|
||||
0
|
||||
for (
|
||||
0
|
||||
for unique_name_2 in 0
|
||||
for () in (0 for unique_name_3 in unique_name_4 for unique_name_5 in name_1)
|
||||
).name_3 in {0: 0 for name_1 in unique_name_8}
|
||||
if name_1
|
||||
)
|
||||
""",
|
||||
"""
|
||||
unique_name_0: 0
|
||||
unique_name_1: {
|
||||
0: 0
|
||||
for unique_name_2 in [0 for name_0 in unique_name_4]
|
||||
if {
|
||||
0: 0
|
||||
for unique_name_5 in 0
|
||||
if name_0
|
||||
if ((name_0 for unique_name_8 in unique_name_9) for [] in 0)
|
||||
}
|
||||
}
|
||||
""",
|
||||
"""
|
||||
0[0]: {0 for name_0 in unique_name_1}
|
||||
unique_name_2: {
|
||||
0: (lambda: name_0 for unique_name_4 in unique_name_5)
|
||||
for unique_name_6 in ()
|
||||
if name_0
|
||||
}
|
||||
""",
|
||||
]
|
||||
for case in cases:
|
||||
case = textwrap.dedent(case)
|
||||
compile(case, "<test>", "exec")
|
||||
|
||||
def test_complex_comprehension_inlining_exec(self):
|
||||
code = """
|
||||
unique_name_1 = unique_name_5 = [1]
|
||||
name_0 = 42
|
||||
unique_name_7: {name_0 for name_0 in unique_name_1}
|
||||
unique_name_2: {
|
||||
0: (lambda: name_0 for unique_name_4 in unique_name_5)
|
||||
for unique_name_6 in [1]
|
||||
if name_0
|
||||
}
|
||||
"""
|
||||
mod = build_module(code)
|
||||
annos = mod.__annotations__
|
||||
self.assertEqual(annos.keys(), {"unique_name_7", "unique_name_2"})
|
||||
self.assertEqual(annos["unique_name_7"], {True})
|
||||
genexp = annos["unique_name_2"][0]
|
||||
lamb = list(genexp)[0]
|
||||
self.assertEqual(lamb(), 42)
|
||||
|
||||
# gh-138349
|
||||
def test_module_level_annotation_plus_listcomp(self):
|
||||
cases = [
|
||||
"""
|
||||
def report_error():
|
||||
pass
|
||||
try:
|
||||
[0 for name_2 in unique_name_0 if (lambda: name_2)]
|
||||
except:
|
||||
pass
|
||||
annotated_name: 0
|
||||
""",
|
||||
"""
|
||||
class Generic:
|
||||
pass
|
||||
try:
|
||||
[0 for name_2 in unique_name_0 if (0 for unique_name_1 in unique_name_2 for unique_name_3 in name_2)]
|
||||
except:
|
||||
pass
|
||||
annotated_name: 0
|
||||
""",
|
||||
"""
|
||||
class Generic:
|
||||
pass
|
||||
annotated_name: 0
|
||||
try:
|
||||
[0 for name_2 in [[0]] for unique_name_1 in unique_name_2 if (lambda: name_2)]
|
||||
except:
|
||||
pass
|
||||
""",
|
||||
]
|
||||
for code in cases:
|
||||
with self.subTest(code=code):
|
||||
mod = build_module(code)
|
||||
annos = mod.__annotations__
|
||||
self.assertEqual(annos, {"annotated_name": 0})
|
||||
Vendored
+1484
File diff suppressed because it is too large.
Load diff
Vendored
+4
-12
@@ -986,7 +986,6 @@ class GenericAliasSubstitutionTests(BaseTestCase):
|
||||
)
|
||||
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test_two_parameters(self):
|
||||
T1 = TypeVar('T1')
|
||||
T2 = TypeVar('T2')
|
||||
@@ -4210,7 +4209,7 @@ class ProtocolTests(BaseTestCase):
|
||||
Alias2 = typing.Union[P, typing.Iterable]
|
||||
self.assertEqual(Alias, Alias2)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: Generic() takes no arguments
|
||||
def test_protocols_pickleable(self):
|
||||
global P, CP # pickle wants to reference the class by name
|
||||
T = TypeVar('T')
|
||||
@@ -5288,7 +5287,7 @@ class GenericTests(BaseTestCase):
|
||||
self.assertNotEqual(repr(base), '')
|
||||
self.assertEqual(base, base)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: Generic() takes no arguments
|
||||
def test_pickle(self):
|
||||
global C # pickle wants to reference the class by name
|
||||
T = TypeVar('T')
|
||||
@@ -5975,7 +5974,6 @@ class FinalDecoratorTests(BaseTestCase):
|
||||
def func(x): ...
|
||||
self.assertIs(func, final(func))
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test_dunder_final(self):
|
||||
@final
|
||||
def func(): ...
|
||||
@@ -7417,7 +7415,6 @@ class EvaluateForwardRefTests(BaseTestCase):
|
||||
list[EqualToForwardRef('A')],
|
||||
)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; ImportError: cannot import name 'fwdref_module'
|
||||
def test_with_module(self):
|
||||
from test.typinganndata import fwdref_module
|
||||
|
||||
@@ -8440,7 +8437,7 @@ class NamedTupleTests(BaseTestCase):
|
||||
self.assertIsInstance(bar.attr, Vanilla)
|
||||
self.assertEqual(bar.attr.name, "attr")
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; + Error calling __set_name__ on 'Annoying' instance attr in 'NamedTupleClass'
|
||||
def test_setname_raises_the_same_as_on_other_classes(self):
|
||||
class CustomException(BaseException): pass
|
||||
|
||||
@@ -8907,14 +8904,12 @@ class TypedDictTests(BaseTestCase):
|
||||
# The TypedDict constructor is not itself a TypedDict
|
||||
self.assertIs(is_typeddict(TypedDict), False)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test_get_type_hints(self):
|
||||
self.assertEqual(
|
||||
get_type_hints(Bar),
|
||||
{'a': typing.Optional[int], 'b': int}
|
||||
)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test_get_type_hints_generic(self):
|
||||
self.assertEqual(
|
||||
get_type_hints(BarGeneric),
|
||||
@@ -9070,7 +9065,6 @@ class TypedDictTests(BaseTestCase):
|
||||
with self.assertRaises(TypeError):
|
||||
WithImplicitAny[str]
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test_non_generic_subscript(self):
|
||||
# For backward compatibility, subscription works
|
||||
# on arbitrary TypedDict types.
|
||||
@@ -9198,7 +9192,6 @@ class TypedDictTests(BaseTestCase):
|
||||
self.assertEqual(Child.__readonly_keys__, frozenset())
|
||||
self.assertEqual(Child.__mutable_keys__, frozenset({'a'}))
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test_combine_qualifiers(self):
|
||||
class AllTheThings(TypedDict):
|
||||
a: Annotated[Required[ReadOnly[int]], "why not"]
|
||||
@@ -9440,7 +9433,7 @@ class RETests(BaseTestCase):
|
||||
self.assertEqual(repr(Match[str]), 'typing.Match[str]')
|
||||
self.assertEqual(repr(Match[bytes]), 'typing.Match[bytes]')
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "type 're\.Match' is not an acceptable base type" does not match "type '_sre.Match' is not an acceptable base type"
|
||||
def test_cannot_subclass(self):
|
||||
with self.assertRaisesRegex(
|
||||
TypeError,
|
||||
@@ -10852,7 +10845,6 @@ class NoDefaultTests(BaseTestCase):
|
||||
with self.assertRaises(TypeError):
|
||||
NoDefault()
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test_no_attributes(self):
|
||||
with self.assertRaises(AttributeError):
|
||||
NoDefault.foo = 3
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
from typing import ForwardRef
|
||||
|
||||
MyList = list[int]
|
||||
MyDict = dict[str, 'MyList']
|
||||
|
||||
fw = ForwardRef('MyDict', module=__name__)
|
||||
@@ -0,0 +1 @@
|
||||
from . import a
|
||||
@@ -0,0 +1,5 @@
|
||||
v1: int
|
||||
|
||||
from . import b
|
||||
|
||||
v2: int
|
||||
@@ -0,0 +1,3 @@
|
||||
from . import a
|
||||
|
||||
annos = a.__annotations__
|
||||
Vendored
+2
-2
@@ -1545,9 +1545,9 @@ class _SpecialGenericAlias(_NotIterable, _BaseGenericAlias, _root=True):
|
||||
self._nparams = nparams
|
||||
self._defaults = defaults
|
||||
if origin.__module__ == 'builtins':
|
||||
self.__doc__ = f'A generic version of {origin.__qualname__}.'
|
||||
self.__doc__ = f'Deprecated alias to {origin.__qualname__}.'
|
||||
else:
|
||||
self.__doc__ = f'A generic version of {origin.__module__}.{origin.__qualname__}.'
|
||||
self.__doc__ = f'Deprecated alias to {origin.__module__}.{origin.__qualname__}.'
|
||||
|
||||
@_tp_cache
|
||||
def __getitem__(self, params):
|
||||
|
||||
+130
-39
@@ -1071,7 +1071,7 @@ impl Compiler {
|
||||
),
|
||||
CompilerScope::Annotation => (
|
||||
bytecode::CodeFlags::NEWLOCALS | bytecode::CodeFlags::OPTIMIZED,
|
||||
0,
|
||||
1, // format is positional-only
|
||||
1, // annotation scope takes one argument (format)
|
||||
0,
|
||||
),
|
||||
@@ -1232,17 +1232,15 @@ impl Compiler {
|
||||
|
||||
/// Enter annotation scope using the symbol table's annotation_block
|
||||
/// Returns false if no annotation_block exists
|
||||
fn enter_annotation_scope(&mut self, func_name: &str) -> CompileResult<bool> {
|
||||
fn enter_annotation_scope(&mut self, _func_name: &str) -> CompileResult<bool> {
|
||||
if !self.push_annotation_symbol_table() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let key = self.symbol_table_stack.len() - 1;
|
||||
let lineno = self.get_source_line_number().get();
|
||||
let annotate_name = format!("<annotate of {func_name}>");
|
||||
|
||||
self.enter_scope(
|
||||
&annotate_name,
|
||||
"__annotate__",
|
||||
CompilerScope::Annotation,
|
||||
key,
|
||||
lineno.to_u32(),
|
||||
@@ -1886,15 +1884,17 @@ impl Compiler {
|
||||
|
||||
// Special handling for class scope implicit cell variables
|
||||
// These are treated as Cell even if not explicitly marked in symbol table
|
||||
// Only for LOAD operations - explicit stores like `__class__ = property(...)`
|
||||
// should use STORE_NAME to store in class namespace dict
|
||||
// __class__ and __classdict__: only LOAD uses Cell (stores go to class namespace)
|
||||
// __conditional_annotations__: both LOAD and STORE use Cell (it's a mutable set
|
||||
// that the annotation scope accesses through the closure)
|
||||
let symbol_scope = {
|
||||
let current_table = self.current_symbol_table();
|
||||
if current_table.typ == CompilerScope::Class
|
||||
&& usage == NameUsage::Load
|
||||
&& (name == "__class__"
|
||||
|| name == "__classdict__"
|
||||
|| name == "__conditional_annotations__")
|
||||
&& ((usage == NameUsage::Load
|
||||
&& (name == "__class__"
|
||||
|| name == "__classdict__"
|
||||
|| name == "__conditional_annotations__"))
|
||||
|| (name == "__conditional_annotations__" && usage == NameUsage::Store))
|
||||
{
|
||||
Some(SymbolScope::Cell)
|
||||
} else {
|
||||
@@ -2437,27 +2437,79 @@ impl Compiler {
|
||||
});
|
||||
|
||||
if let Some(type_params) = type_params {
|
||||
// For TypeAlias, we need to use push_symbol_table to properly handle the TypeAlias scope
|
||||
// Outer scope for TypeParams
|
||||
self.push_symbol_table()?;
|
||||
let key = self.symbol_table_stack.len() - 1;
|
||||
let lineno = self.get_source_line_number().get().to_u32();
|
||||
let scope_name = format!("<generic parameters of {name_string}>");
|
||||
self.enter_scope(&scope_name, CompilerScope::TypeParams, key, lineno)?;
|
||||
|
||||
// Compile type params and push to stack
|
||||
// TypeParams scope is function-like
|
||||
let prev_ctx = self.ctx;
|
||||
self.ctx = CompileContext {
|
||||
loop_data: None,
|
||||
in_class: prev_ctx.in_class,
|
||||
func: FunctionContext::Function,
|
||||
in_async_scope: false,
|
||||
};
|
||||
|
||||
// Compile type params inside the scope
|
||||
self.compile_type_params(type_params)?;
|
||||
// Stack now has [name, type_params_tuple]
|
||||
// Stack: [type_params_tuple]
|
||||
|
||||
// Compile value expression (can now see T1, T2)
|
||||
// Inner closure for lazy value evaluation
|
||||
self.push_symbol_table()?;
|
||||
let inner_key = self.symbol_table_stack.len() - 1;
|
||||
self.enter_scope("TypeAlias", CompilerScope::TypeParams, inner_key, lineno)?;
|
||||
self.compile_expression(value)?;
|
||||
// Stack: [name, type_params_tuple, value]
|
||||
emit!(self, Instruction::ReturnValue);
|
||||
let value_code = self.exit_scope();
|
||||
self.make_closure(value_code, bytecode::MakeFunctionFlags::empty())?;
|
||||
// Stack: [type_params_tuple, value_closure]
|
||||
|
||||
// Pop the TypeAlias scope
|
||||
self.pop_symbol_table();
|
||||
// Swap so unpack_sequence reverse gives correct order
|
||||
emit!(self, Instruction::Swap { index: 2_u32 });
|
||||
// Stack: [value_closure, type_params_tuple]
|
||||
|
||||
// Build tuple and return from TypeParams scope
|
||||
emit!(self, Instruction::BuildTuple { size: 2 });
|
||||
emit!(self, Instruction::ReturnValue);
|
||||
|
||||
let code = self.exit_scope();
|
||||
self.ctx = prev_ctx;
|
||||
self.make_closure(code, bytecode::MakeFunctionFlags::empty())?;
|
||||
emit!(self, Instruction::PushNull);
|
||||
emit!(self, Instruction::Call { nargs: 0 });
|
||||
|
||||
// Unpack: (value_closure, type_params_tuple)
|
||||
// UnpackSequence reverses → stack: [name, type_params_tuple, value_closure]
|
||||
emit!(self, Instruction::UnpackSequence { size: 2 });
|
||||
} else {
|
||||
// Push None for type_params
|
||||
self.emit_load_const(ConstantData::None);
|
||||
// Stack: [name, None]
|
||||
|
||||
// Compile value expression
|
||||
// Create a closure for lazy evaluation of the value
|
||||
self.push_symbol_table()?;
|
||||
let key = self.symbol_table_stack.len() - 1;
|
||||
let lineno = self.get_source_line_number().get().to_u32();
|
||||
self.enter_scope("TypeAlias", CompilerScope::TypeParams, key, lineno)?;
|
||||
|
||||
let prev_ctx = self.ctx;
|
||||
self.ctx = CompileContext {
|
||||
loop_data: None,
|
||||
in_class: prev_ctx.in_class,
|
||||
func: FunctionContext::Function,
|
||||
in_async_scope: false,
|
||||
};
|
||||
|
||||
self.compile_expression(value)?;
|
||||
// Stack: [name, None, value]
|
||||
emit!(self, Instruction::ReturnValue);
|
||||
|
||||
let code = self.exit_scope();
|
||||
self.ctx = prev_ctx;
|
||||
self.make_closure(code, bytecode::MakeFunctionFlags::empty())?;
|
||||
// Stack: [name, None, closure]
|
||||
}
|
||||
|
||||
// Build tuple of 3 elements and call intrinsic
|
||||
@@ -2583,6 +2635,15 @@ impl Compiler {
|
||||
// Enter scope with the type parameter name
|
||||
self.enter_scope(name, CompilerScope::TypeParams, key, lineno)?;
|
||||
|
||||
// TypeParams scope is function-like
|
||||
let prev_ctx = self.ctx;
|
||||
self.ctx = CompileContext {
|
||||
loop_data: None,
|
||||
in_class: prev_ctx.in_class,
|
||||
func: FunctionContext::Function,
|
||||
in_async_scope: false,
|
||||
};
|
||||
|
||||
// Compile the expression
|
||||
if allow_starred && matches!(expr, ast::Expr::Starred(_)) {
|
||||
if let ast::Expr::Starred(starred) = expr {
|
||||
@@ -2598,14 +2659,10 @@ impl Compiler {
|
||||
|
||||
// Exit scope and create closure
|
||||
let code = self.exit_scope();
|
||||
// Note: exit_scope already calls pop_symbol_table, so we don't need to call it again
|
||||
self.ctx = prev_ctx;
|
||||
|
||||
// Create type params function with closure
|
||||
// Create closure for lazy evaluation
|
||||
self.make_closure(code, bytecode::MakeFunctionFlags::empty())?;
|
||||
emit!(self, Instruction::PushNull);
|
||||
|
||||
// Call the function immediately
|
||||
emit!(self, Instruction::Call { nargs: 0 });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -3844,16 +3901,8 @@ impl Compiler {
|
||||
// Check if we have conditional annotations
|
||||
let has_conditional = self.current_symbol_table().has_conditional_annotations;
|
||||
|
||||
// Get parent scope type and name BEFORE pushing annotation symbol table
|
||||
// Get parent scope type BEFORE pushing annotation symbol table
|
||||
let parent_scope_type = self.current_symbol_table().typ;
|
||||
let parent_name = self
|
||||
.symbol_table_stack
|
||||
.last()
|
||||
.map(|t| t.name.as_str())
|
||||
.unwrap_or("module")
|
||||
.to_owned();
|
||||
let scope_name = format!("<annotate of {parent_name}>");
|
||||
|
||||
// Try to push annotation symbol table from current scope
|
||||
if !self.push_current_annotation_symbol_table() {
|
||||
return Ok(false);
|
||||
@@ -3862,7 +3911,12 @@ impl Compiler {
|
||||
// Enter annotation scope for code generation
|
||||
let key = self.symbol_table_stack.len() - 1;
|
||||
let lineno = self.get_source_line_number().get();
|
||||
self.enter_scope(&scope_name, CompilerScope::Annotation, key, lineno.to_u32())?;
|
||||
self.enter_scope(
|
||||
"__annotate__",
|
||||
CompilerScope::Annotation,
|
||||
key,
|
||||
lineno.to_u32(),
|
||||
)?;
|
||||
|
||||
// Add 'format' parameter to varnames
|
||||
self.current_code_info()
|
||||
@@ -3991,6 +4045,9 @@ impl Compiler {
|
||||
let is_generic = type_params.is_some();
|
||||
let mut num_typeparam_args = 0;
|
||||
|
||||
// Save context before entering TypeParams scope
|
||||
let saved_ctx = self.ctx;
|
||||
|
||||
if is_generic {
|
||||
// Count args to pass to type params scope
|
||||
if funcflags.contains(bytecode::MakeFunctionFlags::DEFAULTS) {
|
||||
@@ -4010,6 +4067,14 @@ impl Compiler {
|
||||
type_params_name,
|
||||
)?;
|
||||
|
||||
// TypeParams scope is function-like
|
||||
self.ctx = CompileContext {
|
||||
loop_data: None,
|
||||
in_class: saved_ctx.in_class,
|
||||
func: FunctionContext::Function,
|
||||
in_async_scope: false,
|
||||
};
|
||||
|
||||
// Add parameter names to varnames for the type params scope
|
||||
// These will be passed as arguments when the closure is called
|
||||
let current_info = self.current_code_info();
|
||||
@@ -4068,6 +4133,7 @@ impl Compiler {
|
||||
|
||||
// Exit type params scope and create closure
|
||||
let type_params_code = self.exit_scope();
|
||||
self.ctx = saved_ctx;
|
||||
|
||||
// Make closure for type params code
|
||||
self.make_closure(type_params_code, bytecode::MakeFunctionFlags::empty())?;
|
||||
@@ -4316,12 +4382,26 @@ impl Compiler {
|
||||
Self::find_ann(body) || Self::find_ann(orelse)
|
||||
}
|
||||
ast::Stmt::With(ast::StmtWith { body, .. }) => Self::find_ann(body),
|
||||
ast::Stmt::Match(ast::StmtMatch { cases, .. }) => {
|
||||
cases.iter().any(|case| Self::find_ann(&case.body))
|
||||
}
|
||||
ast::Stmt::Try(ast::StmtTry {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
..
|
||||
}) => Self::find_ann(body) || Self::find_ann(orelse) || Self::find_ann(finalbody),
|
||||
}) => {
|
||||
Self::find_ann(body)
|
||||
|| handlers.iter().any(|h| {
|
||||
let ast::ExceptHandler::ExceptHandler(
|
||||
ast::ExceptHandlerExceptHandler { body, .. },
|
||||
) = h;
|
||||
Self::find_ann(body)
|
||||
})
|
||||
|| Self::find_ann(orelse)
|
||||
|| Self::find_ann(finalbody)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if res {
|
||||
@@ -4458,6 +4538,9 @@ impl Compiler {
|
||||
let is_generic = type_params.is_some();
|
||||
let firstlineno = self.get_source_line_number().get().to_u32();
|
||||
|
||||
// Save context before entering any scopes
|
||||
let saved_ctx = self.ctx;
|
||||
|
||||
// Step 1: If generic, enter type params scope and compile type params
|
||||
if is_generic {
|
||||
let type_params_name = format!("<generic parameters of {name}>");
|
||||
@@ -4472,6 +4555,14 @@ impl Compiler {
|
||||
// Set private name for name mangling
|
||||
self.code_stack.last_mut().unwrap().private = Some(name.to_owned());
|
||||
|
||||
// TypeParams scope is function-like
|
||||
self.ctx = CompileContext {
|
||||
loop_data: None,
|
||||
in_class: saved_ctx.in_class,
|
||||
func: FunctionContext::Function,
|
||||
in_async_scope: false,
|
||||
};
|
||||
|
||||
// Compile type parameters and store as .type_params
|
||||
self.compile_type_params(type_params.unwrap())?;
|
||||
let dot_type_params = self.name(".type_params");
|
||||
@@ -4622,6 +4713,7 @@ impl Compiler {
|
||||
|
||||
// Exit type params scope and wrap in function
|
||||
let type_params_code = self.exit_scope();
|
||||
self.ctx = saved_ctx;
|
||||
|
||||
// Execute the type params function
|
||||
self.make_closure(type_params_code, bytecode::MakeFunctionFlags::empty())?;
|
||||
@@ -6180,8 +6272,7 @@ impl Compiler {
|
||||
|
||||
// Only add to __conditional_annotations__ set if actually conditional
|
||||
if is_conditional {
|
||||
let cond_annotations_name = self.name("__conditional_annotations__");
|
||||
emit!(self, Instruction::LoadName(cond_annotations_name));
|
||||
self.load_name("__conditional_annotations__")?;
|
||||
self.emit_load_const(ConstantData::Integer {
|
||||
value: annotation_index.into(),
|
||||
});
|
||||
|
||||
@@ -396,12 +396,17 @@ impl SymbolTableAnalyzer {
|
||||
// we need to pass class symbols to the annotation scope
|
||||
let is_class = symbol_table.typ == CompilerScope::Class;
|
||||
|
||||
// Clone class symbols if needed for annotation scope (to avoid borrow conflict)
|
||||
let class_symbols_for_ann = if is_class
|
||||
&& annotation_block
|
||||
.as_ref()
|
||||
.is_some_and(|b| b.can_see_class_scope)
|
||||
{
|
||||
// Clone class symbols if needed for child scopes with can_see_class_scope
|
||||
let needs_class_symbols = (is_class
|
||||
&& (sub_tables.iter().any(|st| st.can_see_class_scope)
|
||||
|| annotation_block
|
||||
.as_ref()
|
||||
.is_some_and(|b| b.can_see_class_scope)))
|
||||
|| (!is_class
|
||||
&& class_entry.is_some()
|
||||
&& sub_tables.iter().any(|st| st.can_see_class_scope));
|
||||
|
||||
let class_symbols_clone = if is_class && needs_class_symbols {
|
||||
Some(symbols.clone())
|
||||
} else {
|
||||
None
|
||||
@@ -412,15 +417,32 @@ impl SymbolTableAnalyzer {
|
||||
let inner_scope = unsafe { &mut *(list as *mut _ as *mut Self) };
|
||||
// Analyze sub scopes and collect their free variables
|
||||
for sub_table in sub_tables.iter_mut() {
|
||||
// Sub-scopes (functions, nested classes) don't inherit class_entry
|
||||
let child_free = inner_scope.analyze_symbol_table(sub_table, None)?;
|
||||
// Pass class_entry to sub-scopes that can see the class scope
|
||||
let child_class_entry = if sub_table.can_see_class_scope {
|
||||
if is_class {
|
||||
class_symbols_clone.as_ref()
|
||||
} else {
|
||||
class_entry
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let child_free = inner_scope.analyze_symbol_table(sub_table, child_class_entry)?;
|
||||
// Propagate child's free variables to this scope
|
||||
newfree.extend(child_free);
|
||||
}
|
||||
// PEP 649: Analyze annotation block if present
|
||||
if let Some(annotation_table) = annotation_block {
|
||||
// Pass class symbols to annotation scope if can_see_class_scope
|
||||
let ann_class_entry = class_symbols_for_ann.as_ref().or(class_entry);
|
||||
let ann_class_entry = if annotation_table.can_see_class_scope {
|
||||
if is_class {
|
||||
class_symbols_clone.as_ref()
|
||||
} else {
|
||||
class_entry
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let child_free =
|
||||
inner_scope.analyze_symbol_table(annotation_table, ann_class_entry)?;
|
||||
// Propagate annotation's free variables to this scope
|
||||
@@ -535,24 +557,21 @@ impl SymbolTableAnalyzer {
|
||||
// all is well
|
||||
}
|
||||
SymbolScope::Unknown => {
|
||||
// PEP 649: Check class_entry first (like analyze_name)
|
||||
// If name is bound in enclosing class, mark as GlobalImplicit
|
||||
if let Some(class_symbols) = class_entry
|
||||
&& let Some(class_sym) = class_symbols.get(&symbol.name)
|
||||
{
|
||||
// DEF_BOUND && !DEF_NONLOCAL -> GLOBAL_IMPLICIT
|
||||
if class_sym.is_bound() && class_sym.scope != SymbolScope::Free {
|
||||
symbol.scope = SymbolScope::GlobalImplicit;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Try hard to figure out what the scope of this symbol is.
|
||||
let scope = if symbol.is_bound() {
|
||||
self.found_in_inner_scope(sub_tables, &symbol.name, st_typ)
|
||||
.unwrap_or(SymbolScope::Local)
|
||||
} else if let Some(scope) = self.found_in_outer_scope(&symbol.name, st_typ) {
|
||||
// If found in enclosing scope (function/TypeParams), use that
|
||||
scope
|
||||
} else if let Some(class_symbols) = class_entry
|
||||
&& let Some(class_sym) = class_symbols.get(&symbol.name)
|
||||
&& class_sym.is_bound()
|
||||
&& class_sym.scope != SymbolScope::Free
|
||||
{
|
||||
// If name is bound in enclosing class, use GlobalImplicit
|
||||
// so it can be accessed via __classdict__
|
||||
SymbolScope::GlobalImplicit
|
||||
} else if self.tables.is_empty() {
|
||||
// Don't make assumptions when we don't know.
|
||||
SymbolScope::Unknown
|
||||
@@ -1054,7 +1073,13 @@ impl SymbolTableBuilder {
|
||||
|
||||
if is_conditional && !self.tables.last().unwrap().has_conditional_annotations {
|
||||
self.tables.last_mut().unwrap().has_conditional_annotations = true;
|
||||
// Register __conditional_annotations__ symbol in the scope (USE flag, not DEF)
|
||||
// Register __conditional_annotations__ as both Assigned and Used so that
|
||||
// it becomes a Cell variable in class scope (children reference it as Free)
|
||||
self.register_name(
|
||||
"__conditional_annotations__",
|
||||
SymbolUsage::Assigned,
|
||||
annotation.range(),
|
||||
)?;
|
||||
self.register_name(
|
||||
"__conditional_annotations__",
|
||||
SymbolUsage::Used,
|
||||
@@ -1441,16 +1466,35 @@ impl SymbolTableBuilder {
|
||||
}) => {
|
||||
let was_in_type_alias = self.in_type_alias;
|
||||
self.in_type_alias = true;
|
||||
// Check before entering any sub-scopes
|
||||
let in_class = self
|
||||
.tables
|
||||
.last()
|
||||
.is_some_and(|t| t.typ == CompilerScope::Class);
|
||||
let is_generic = type_params.is_some();
|
||||
if let Some(type_params) = type_params {
|
||||
self.enter_type_param_block(
|
||||
"TypeAlias",
|
||||
self.line_index_start(type_params.range),
|
||||
)?;
|
||||
self.scan_type_params(type_params)?;
|
||||
self.scan_expression(value, ExpressionContext::Load)?;
|
||||
}
|
||||
// Value scope for lazy evaluation
|
||||
self.enter_scope(
|
||||
"TypeAlias",
|
||||
CompilerScope::TypeParams,
|
||||
self.line_index_start(value.range()),
|
||||
);
|
||||
if in_class {
|
||||
if let Some(table) = self.tables.last_mut() {
|
||||
table.can_see_class_scope = true;
|
||||
}
|
||||
self.register_name("__classdict__", SymbolUsage::Used, TextRange::default())?;
|
||||
}
|
||||
self.scan_expression(value, ExpressionContext::Load)?;
|
||||
self.leave_scope();
|
||||
if is_generic {
|
||||
self.leave_scope();
|
||||
} else {
|
||||
self.scan_expression(value, ExpressionContext::Load)?;
|
||||
}
|
||||
self.in_type_alias = was_in_type_alias;
|
||||
self.scan_expression(name, ExpressionContext::Store)?;
|
||||
@@ -1943,11 +1987,16 @@ impl SymbolTableBuilder {
|
||||
) -> SymbolTableResult {
|
||||
// Enter a new TypeParams scope for the bound/default expression
|
||||
// This allows the expression to access outer scope symbols
|
||||
let in_class = self.tables.last().is_some_and(|t| t.can_see_class_scope);
|
||||
let line_number = self.line_index_start(expr.range());
|
||||
self.enter_scope(scope_name, CompilerScope::TypeParams, line_number);
|
||||
|
||||
// Note: In CPython, can_see_class_scope is preserved in the new scope
|
||||
// In RustPython, this is handled through the scope hierarchy
|
||||
if in_class {
|
||||
if let Some(table) = self.tables.last_mut() {
|
||||
table.can_see_class_scope = true;
|
||||
}
|
||||
self.register_name("__classdict__", SymbolUsage::Used, TextRange::default())?;
|
||||
}
|
||||
|
||||
// Set scope_info for better error messages
|
||||
let old_scope_info = self.scope_info;
|
||||
|
||||
@@ -700,7 +700,18 @@ impl ModuleItem for ClassItem {
|
||||
};
|
||||
let class_new = quote_spanned!(ident.span() =>
|
||||
let new_class = <#ident as ::rustpython_vm::class::PyClassImpl>::make_class(ctx);
|
||||
new_class.set_attr(rustpython_vm::identifier!(ctx, __module__), vm.new_pyobj(#module_name));
|
||||
// Only set __module__ string if the class doesn't already have a
|
||||
// getset descriptor for __module__ (which provides instance-level
|
||||
// module resolution, e.g. TypeAliasType)
|
||||
{
|
||||
let module_key = rustpython_vm::identifier!(ctx, __module__);
|
||||
let has_module_getset = new_class.attributes.read()
|
||||
.get(module_key)
|
||||
.is_some_and(|v| v.downcastable::<rustpython_vm::builtins::PyGetSet>());
|
||||
if !has_module_getset {
|
||||
new_class.set_attr(module_key, vm.new_pyobj(#module_name));
|
||||
}
|
||||
}
|
||||
);
|
||||
(class_name, class_new)
|
||||
};
|
||||
@@ -778,7 +789,15 @@ impl ModuleItem for StructSequenceItem {
|
||||
// Generate the class creation code
|
||||
let class_new = quote_spanned!(pytype_ident.span() =>
|
||||
let new_class = <#pytype_ident as ::rustpython_vm::class::PyClassImpl>::make_class(ctx);
|
||||
new_class.set_attr(rustpython_vm::identifier!(ctx, __module__), vm.new_pyobj(#module_name));
|
||||
{
|
||||
let module_key = rustpython_vm::identifier!(ctx, __module__);
|
||||
let has_module_getset = new_class.attributes.read()
|
||||
.get(module_key)
|
||||
.is_some_and(|v| v.downcastable::<rustpython_vm::builtins::PyGetSet>());
|
||||
if !has_module_getset {
|
||||
new_class.set_attr(module_key, vm.new_pyobj(#module_name));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Handle py_attrs for custom names, or use class_name as default
|
||||
|
||||
@@ -718,19 +718,23 @@ impl PyFunction {
|
||||
value: PySetterValue<Option<PyObjectRef>>,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<()> {
|
||||
let annotations = match value {
|
||||
match value {
|
||||
PySetterValue::Assign(Some(value)) => {
|
||||
let annotations = value.downcast::<crate::builtins::PyDict>().map_err(|_| {
|
||||
vm.new_type_error("__annotations__ must be set to a dict object")
|
||||
})?;
|
||||
Some(annotations)
|
||||
*self.annotations.lock() = Some(annotations);
|
||||
*self.annotate.lock() = None;
|
||||
}
|
||||
PySetterValue::Assign(None) | PySetterValue::Delete => None,
|
||||
};
|
||||
*self.annotations.lock() = annotations;
|
||||
|
||||
// Clear __annotate__ when __annotations__ is set
|
||||
*self.annotate.lock() = None;
|
||||
PySetterValue::Assign(None) => {
|
||||
*self.annotations.lock() = None;
|
||||
*self.annotate.lock() = None;
|
||||
}
|
||||
PySetterValue::Delete => {
|
||||
// del only clears cached annotations; __annotate__ is preserved
|
||||
*self.annotations.lock() = None;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
// spell-checker:ignore iparam
|
||||
// spell-checker:ignore iparam gaiterobject
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use super::type_;
|
||||
use crate::{
|
||||
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject,
|
||||
VirtualMachine, atomic_func,
|
||||
builtins::{PyList, PyStr, PyTuple, PyTupleRef, PyType, PyTypeRef},
|
||||
builtins::{PyList, PyStr, PyTuple, PyTupleRef, PyType},
|
||||
class::PyClassImpl,
|
||||
common::hash,
|
||||
convert::ToPyObject,
|
||||
function::{FuncArgs, PyComparisonValue},
|
||||
protocol::{PyMappingMethods, PyNumberMethods},
|
||||
types::{
|
||||
AsMapping, AsNumber, Callable, Comparable, Constructor, GetAttr, Hashable, Iterable,
|
||||
PyComparisonOp, Representable,
|
||||
AsMapping, AsNumber, Callable, Comparable, Constructor, GetAttr, Hashable, IterNext,
|
||||
Iterable, PyComparisonOp, Representable,
|
||||
},
|
||||
};
|
||||
use alloc::fmt;
|
||||
|
||||
// attr_exceptions
|
||||
static ATTR_EXCEPTIONS: [&str; 12] = [
|
||||
// Attributes that are looked up on the GenericAlias itself, not on __origin__
|
||||
static ATTR_EXCEPTIONS: [&str; 9] = [
|
||||
"__class__",
|
||||
"__bases__",
|
||||
"__origin__",
|
||||
"__args__",
|
||||
"__unpacked__",
|
||||
@@ -30,13 +29,14 @@ static ATTR_EXCEPTIONS: [&str; 12] = [
|
||||
"__mro_entries__",
|
||||
"__reduce_ex__", // needed so we don't look up object.__reduce_ex__
|
||||
"__reduce__",
|
||||
"__copy__",
|
||||
"__deepcopy__",
|
||||
];
|
||||
|
||||
// Attributes that are blocked from being looked up on __origin__
|
||||
static ATTR_BLOCKED: [&str; 3] = ["__bases__", "__copy__", "__deepcopy__"];
|
||||
|
||||
#[pyclass(module = "types", name = "GenericAlias")]
|
||||
pub struct PyGenericAlias {
|
||||
origin: PyTypeRef,
|
||||
origin: PyObjectRef,
|
||||
args: PyTupleRef,
|
||||
parameters: PyTupleRef,
|
||||
starred: bool, // for __unpacked__ attribute
|
||||
@@ -62,7 +62,7 @@ impl Constructor for PyGenericAlias {
|
||||
if !args.kwargs.is_empty() {
|
||||
return Err(vm.new_type_error("GenericAlias() takes no keyword arguments"));
|
||||
}
|
||||
let (origin, arguments): (_, PyObjectRef) = args.bind(vm)?;
|
||||
let (origin, arguments): (PyObjectRef, PyObjectRef) = args.bind(vm)?;
|
||||
let args = if let Ok(tuple) = arguments.try_to_ref::<PyTuple>(vm) {
|
||||
tuple.to_owned()
|
||||
} else {
|
||||
@@ -87,10 +87,15 @@ impl Constructor for PyGenericAlias {
|
||||
flags(BASETYPE)
|
||||
)]
|
||||
impl PyGenericAlias {
|
||||
pub fn new(origin: PyTypeRef, args: PyTupleRef, starred: bool, vm: &VirtualMachine) -> Self {
|
||||
pub fn new(
|
||||
origin: impl Into<PyObjectRef>,
|
||||
args: PyTupleRef,
|
||||
starred: bool,
|
||||
vm: &VirtualMachine,
|
||||
) -> Self {
|
||||
let parameters = make_parameters(&args, vm);
|
||||
Self {
|
||||
origin,
|
||||
origin: origin.into(),
|
||||
args,
|
||||
parameters,
|
||||
starred,
|
||||
@@ -98,7 +103,11 @@ impl PyGenericAlias {
|
||||
}
|
||||
|
||||
/// Create a GenericAlias from an origin and PyObjectRef arguments (helper for compatibility)
|
||||
pub fn from_args(origin: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> Self {
|
||||
pub fn from_args(
|
||||
origin: impl Into<PyObjectRef>,
|
||||
args: PyObjectRef,
|
||||
vm: &VirtualMachine,
|
||||
) -> Self {
|
||||
let args = if let Ok(tuple) = args.try_to_ref::<PyTuple>(vm) {
|
||||
tuple.to_owned()
|
||||
} else {
|
||||
@@ -138,15 +147,35 @@ impl PyGenericAlias {
|
||||
}
|
||||
}
|
||||
|
||||
fn repr_arg(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<String> {
|
||||
// ParamSpec args can be lists - format their items with repr_item
|
||||
if obj.class().is(vm.ctx.types.list_type) {
|
||||
let list = obj.downcast_ref::<crate::builtins::PyList>().unwrap();
|
||||
let len = list.borrow_vec().len();
|
||||
let mut parts = Vec::with_capacity(len);
|
||||
// Use indexed access so list mutation during repr causes IndexError
|
||||
for i in 0..len {
|
||||
let item =
|
||||
list.borrow_vec().get(i).cloned().ok_or_else(|| {
|
||||
vm.new_index_error("list index out of range".to_owned())
|
||||
})?;
|
||||
parts.push(repr_item(item, vm)?);
|
||||
}
|
||||
Ok(format!("[{}]", parts.join(", ")))
|
||||
} else {
|
||||
repr_item(obj, vm)
|
||||
}
|
||||
}
|
||||
|
||||
let repr_str = format!(
|
||||
"{}[{}]",
|
||||
repr_item(self.origin.clone().into(), vm)?,
|
||||
repr_item(self.origin.clone(), vm)?,
|
||||
if self.args.is_empty() {
|
||||
"()".to_owned()
|
||||
} else {
|
||||
self.args
|
||||
.iter()
|
||||
.map(|o| repr_item(o.clone(), vm))
|
||||
.map(|o| repr_arg(o.clone(), vm))
|
||||
.collect::<PyResult<Vec<_>>>()?
|
||||
.join(", ")
|
||||
}
|
||||
@@ -172,7 +201,7 @@ impl PyGenericAlias {
|
||||
|
||||
#[pygetset]
|
||||
fn __origin__(&self) -> PyObjectRef {
|
||||
self.origin.clone().into()
|
||||
self.origin.clone()
|
||||
}
|
||||
|
||||
#[pygetset]
|
||||
@@ -182,7 +211,7 @@ impl PyGenericAlias {
|
||||
|
||||
#[pygetset]
|
||||
fn __typing_unpacked_tuple_args__(&self, vm: &VirtualMachine) -> PyObjectRef {
|
||||
if self.starred && self.origin.is(vm.ctx.types.tuple_type) {
|
||||
if self.starred && self.origin.is(vm.ctx.types.tuple_type.as_object()) {
|
||||
self.args.clone().into()
|
||||
} else {
|
||||
vm.ctx.none()
|
||||
@@ -213,11 +242,29 @@ impl PyGenericAlias {
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn __reduce__(zelf: &Py<Self>, vm: &VirtualMachine) -> (PyTypeRef, (PyTypeRef, PyTupleRef)) {
|
||||
(
|
||||
vm.ctx.types.generic_alias_type.to_owned(),
|
||||
(zelf.origin.clone(), zelf.args.clone()),
|
||||
)
|
||||
fn __reduce__(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
|
||||
if zelf.starred {
|
||||
// (next, (iter(GenericAlias(origin, args)),))
|
||||
let next_fn = vm.builtins.get_attr("next", vm)?;
|
||||
let non_starred = Self::new(zelf.origin.clone(), zelf.args.clone(), false, vm);
|
||||
let iter_obj = PyGenericAliasIterator {
|
||||
obj: crate::common::lock::PyMutex::new(Some(non_starred.into_pyobject(vm))),
|
||||
}
|
||||
.into_pyobject(vm);
|
||||
Ok(PyTuple::new_ref(
|
||||
vec![next_fn, PyTuple::new_ref(vec![iter_obj], &vm.ctx).into()],
|
||||
&vm.ctx,
|
||||
))
|
||||
} else {
|
||||
Ok(PyTuple::new_ref(
|
||||
vec![
|
||||
vm.ctx.types.generic_alias_type.to_owned().into(),
|
||||
PyTuple::new_ref(vec![zelf.origin.clone(), zelf.args.clone().into()], &vm.ctx)
|
||||
.into(),
|
||||
],
|
||||
&vm.ctx,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
@@ -245,8 +292,11 @@ impl PyGenericAlias {
|
||||
}
|
||||
|
||||
pub(crate) fn make_parameters(args: &Py<PyTuple>, vm: &VirtualMachine) -> PyTupleRef {
|
||||
make_parameters_from_slice(args.as_slice(), vm)
|
||||
}
|
||||
|
||||
fn make_parameters_from_slice(args: &[PyObjectRef], vm: &VirtualMachine) -> PyTupleRef {
|
||||
let mut parameters: Vec<PyObjectRef> = Vec::with_capacity(args.len());
|
||||
let mut iparam = 0;
|
||||
|
||||
for arg in args {
|
||||
// We don't want __parameters__ descriptor of a bare Python class.
|
||||
@@ -256,37 +306,34 @@ pub(crate) fn make_parameters(args: &Py<PyTuple>, vm: &VirtualMachine) -> PyTupl
|
||||
|
||||
// Check for __typing_subst__ attribute
|
||||
if arg.get_attr(identifier!(vm, __typing_subst__), vm).is_ok() {
|
||||
// Use tuple_add equivalent logic
|
||||
if tuple_index(¶meters, arg).is_none() {
|
||||
if iparam >= parameters.len() {
|
||||
parameters.resize(iparam + 1, vm.ctx.none());
|
||||
}
|
||||
parameters[iparam] = arg.clone();
|
||||
iparam += 1;
|
||||
parameters.push(arg.clone());
|
||||
}
|
||||
} else if let Ok(subparams) = arg.get_attr(identifier!(vm, __parameters__), vm)
|
||||
&& let Ok(sub_params) = subparams.try_to_ref::<PyTuple>(vm)
|
||||
{
|
||||
let len2 = sub_params.len();
|
||||
// Resize if needed
|
||||
if iparam + len2 > parameters.len() {
|
||||
parameters.resize(iparam + len2, vm.ctx.none());
|
||||
}
|
||||
for sub_param in sub_params {
|
||||
// Use tuple_add equivalent logic
|
||||
if tuple_index(¶meters[..iparam], sub_param).is_none() {
|
||||
if iparam >= parameters.len() {
|
||||
parameters.resize(iparam + 1, vm.ctx.none());
|
||||
}
|
||||
parameters[iparam] = sub_param.clone();
|
||||
iparam += 1;
|
||||
if tuple_index(¶meters, sub_param).is_none() {
|
||||
parameters.push(sub_param.clone());
|
||||
}
|
||||
}
|
||||
} else if arg.try_to_ref::<PyTuple>(vm).is_ok() || arg.try_to_ref::<PyList>(vm).is_ok() {
|
||||
// Recursively extract parameters from lists/tuples (ParamSpec args)
|
||||
let items: Vec<PyObjectRef> = if let Ok(t) = arg.try_to_ref::<PyTuple>(vm) {
|
||||
t.as_slice().to_vec()
|
||||
} else {
|
||||
let list = arg.downcast_ref::<PyList>().unwrap();
|
||||
list.borrow_vec().to_vec()
|
||||
};
|
||||
let sub = make_parameters_from_slice(&items, vm);
|
||||
for sub_param in sub.iter() {
|
||||
if tuple_index(¶meters, sub_param).is_none() {
|
||||
parameters.push(sub_param.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resize to actual size
|
||||
parameters.truncate(iparam);
|
||||
PyTuple::new_ref(parameters, &vm.ctx)
|
||||
}
|
||||
|
||||
@@ -433,7 +480,7 @@ pub fn subs_parameters(
|
||||
let arg_items = if let Ok(tuple) = item.try_to_ref::<PyTuple>(vm) {
|
||||
tuple.as_slice().to_vec()
|
||||
} else {
|
||||
vec![item]
|
||||
vec![item.clone()]
|
||||
};
|
||||
let n_items = arg_items.len();
|
||||
|
||||
@@ -457,32 +504,55 @@ pub fn subs_parameters(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is an unpacked TypeVarTuple's _is_unpacked_typevartuple
|
||||
// Recursively substitute params in lists/tuples
|
||||
let is_list = arg.try_to_ref::<PyList>(vm).is_ok();
|
||||
if arg.try_to_ref::<PyTuple>(vm).is_ok() || is_list {
|
||||
let sub_items: Vec<PyObjectRef> = if let Ok(t) = arg.try_to_ref::<PyTuple>(vm) {
|
||||
t.as_slice().to_vec()
|
||||
} else {
|
||||
arg.downcast_ref::<PyList>().unwrap().borrow_vec().to_vec()
|
||||
};
|
||||
let sub_tuple = PyTuple::new_ref(sub_items, &vm.ctx);
|
||||
let sub_result = subs_parameters(
|
||||
alias.clone(),
|
||||
sub_tuple,
|
||||
parameters.clone(),
|
||||
item.clone(),
|
||||
vm,
|
||||
)?;
|
||||
let substituted: PyObjectRef = if is_list {
|
||||
// Convert tuple back to list
|
||||
PyList::from(sub_result.as_slice().to_vec())
|
||||
.into_ref(&vm.ctx)
|
||||
.into()
|
||||
} else {
|
||||
sub_result.into()
|
||||
};
|
||||
new_args.push(substituted);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is an unpacked TypeVarTuple
|
||||
let unpack = is_unpacked_typevartuple(arg, vm)?;
|
||||
|
||||
// Try __typing_subst__ method first,
|
||||
// Try __typing_subst__ method first
|
||||
let substituted_arg = if let Ok(subst) = arg.get_attr(identifier!(vm, __typing_subst__), vm)
|
||||
{
|
||||
// Find parameter index's tuple_index
|
||||
if let Some(iparam) = tuple_index(parameters.as_slice(), arg) {
|
||||
subst.call((arg_items[iparam].clone(),), vm)?
|
||||
} else {
|
||||
// This shouldn't happen in well-formed generics but handle gracefully
|
||||
subs_tvars(arg.clone(), ¶meters, &arg_items, vm)?
|
||||
}
|
||||
} else {
|
||||
// Use subs_tvars for objects with __parameters__
|
||||
subs_tvars(arg.clone(), ¶meters, &arg_items, vm)?
|
||||
};
|
||||
|
||||
if unpack {
|
||||
// Handle unpacked TypeVarTuple's tuple_extend
|
||||
if let Ok(tuple) = substituted_arg.try_to_ref::<PyTuple>(vm) {
|
||||
for elem in tuple {
|
||||
new_args.push(elem.clone());
|
||||
}
|
||||
} else {
|
||||
// This shouldn't happen but handle gracefully
|
||||
new_args.push(substituted_arg);
|
||||
}
|
||||
} else {
|
||||
@@ -519,7 +589,7 @@ impl AsNumber for PyGenericAlias {
|
||||
impl Callable for PyGenericAlias {
|
||||
type Args = FuncArgs;
|
||||
fn call(zelf: &Py<Self>, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
|
||||
PyType::call(&zelf.origin, args, vm).map(|obj| {
|
||||
zelf.origin.call(args, vm).map(|obj| {
|
||||
if let Err(exc) = obj.set_attr(identifier!(vm, __orig_class__), zelf.to_owned(), vm)
|
||||
&& !exc.fast_isinstance(vm.ctx.exceptions.attribute_error)
|
||||
&& !exc.fast_isinstance(vm.ctx.exceptions.type_error)
|
||||
@@ -540,17 +610,17 @@ impl Comparable for PyGenericAlias {
|
||||
) -> PyResult<PyComparisonValue> {
|
||||
op.eq_only(|| {
|
||||
let other = class_or_notimplemented!(Self, other);
|
||||
if zelf.starred != other.starred {
|
||||
return Ok(PyComparisonValue::Implemented(false));
|
||||
}
|
||||
Ok(PyComparisonValue::Implemented(
|
||||
if !zelf.__origin__().rich_compare_bool(
|
||||
&other.__origin__(),
|
||||
PyComparisonOp::Eq,
|
||||
vm,
|
||||
)? {
|
||||
false
|
||||
} else {
|
||||
zelf.__args__()
|
||||
.rich_compare_bool(&other.__args__(), PyComparisonOp::Eq, vm)?
|
||||
},
|
||||
zelf.__origin__()
|
||||
.rich_compare_bool(&other.__origin__(), PyComparisonOp::Eq, vm)?
|
||||
&& zelf.__args__().rich_compare_bool(
|
||||
&other.__args__(),
|
||||
PyComparisonOp::Eq,
|
||||
vm,
|
||||
)?,
|
||||
))
|
||||
})
|
||||
}
|
||||
@@ -559,14 +629,20 @@ impl Comparable for PyGenericAlias {
|
||||
impl Hashable for PyGenericAlias {
|
||||
#[inline]
|
||||
fn hash(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<hash::PyHash> {
|
||||
Ok(zelf.origin.as_object().hash(vm)? ^ zelf.args.as_object().hash(vm)?)
|
||||
Ok(zelf.origin.hash(vm)? ^ zelf.args.as_object().hash(vm)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl GetAttr for PyGenericAlias {
|
||||
fn getattro(zelf: &Py<Self>, attr: &Py<PyStr>, vm: &VirtualMachine) -> PyResult {
|
||||
let attr_str = attr.as_str();
|
||||
for exc in &ATTR_EXCEPTIONS {
|
||||
if *(*exc) == attr.to_string() {
|
||||
if *exc == attr_str {
|
||||
return zelf.as_object().generic_getattr(attr, vm);
|
||||
}
|
||||
}
|
||||
for blocked in &ATTR_BLOCKED {
|
||||
if *blocked == attr_str {
|
||||
return zelf.as_object().generic_getattr(attr, vm);
|
||||
}
|
||||
}
|
||||
@@ -582,27 +658,65 @@ impl Representable for PyGenericAlias {
|
||||
}
|
||||
|
||||
impl Iterable for PyGenericAlias {
|
||||
// ga_iter
|
||||
// spell-checker:ignore gaiterobject
|
||||
// TODO: gaiterobject
|
||||
fn iter(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyResult {
|
||||
// CPython's ga_iter creates an iterator that yields one starred GenericAlias
|
||||
// we don't have gaiterobject yet
|
||||
Ok(PyGenericAliasIterator {
|
||||
obj: crate::common::lock::PyMutex::new(Some(zelf.into())),
|
||||
}
|
||||
.into_pyobject(vm))
|
||||
}
|
||||
}
|
||||
|
||||
let starred_alias = Self::new(
|
||||
zelf.origin.clone(),
|
||||
zelf.args.clone(),
|
||||
true, // starred
|
||||
vm,
|
||||
);
|
||||
let starred_ref = PyRef::new_ref(
|
||||
starred_alias,
|
||||
vm.ctx.types.generic_alias_type.to_owned(),
|
||||
None,
|
||||
);
|
||||
let items = vec![starred_ref.into()];
|
||||
let iter_tuple = PyTuple::new_ref(items, &vm.ctx);
|
||||
Ok(iter_tuple.to_pyobject(vm).get_iter(vm)?.into())
|
||||
// gaiterobject - yields one starred GenericAlias then exhausts
|
||||
#[pyclass(module = "types", name = "generic_alias_iterator")]
|
||||
#[derive(Debug, PyPayload)]
|
||||
pub struct PyGenericAliasIterator {
|
||||
obj: crate::common::lock::PyMutex<Option<PyObjectRef>>,
|
||||
}
|
||||
|
||||
#[pyclass(with(Representable, Iterable, IterNext))]
|
||||
impl PyGenericAliasIterator {
|
||||
#[pymethod]
|
||||
fn __reduce__(&self, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
|
||||
let iter_fn = vm.builtins.get_attr("iter", vm)?;
|
||||
let guard = self.obj.lock();
|
||||
let arg: PyObjectRef = if let Some(ref obj) = *guard {
|
||||
// Not yet exhausted: (iter, (obj,))
|
||||
PyTuple::new_ref(vec![obj.clone()], &vm.ctx).into()
|
||||
} else {
|
||||
// Exhausted: (iter, ((),))
|
||||
let empty = PyTuple::new_ref(vec![], &vm.ctx);
|
||||
PyTuple::new_ref(vec![empty.into()], &vm.ctx).into()
|
||||
};
|
||||
Ok(PyTuple::new_ref(vec![iter_fn, arg], &vm.ctx))
|
||||
}
|
||||
}
|
||||
|
||||
impl Representable for PyGenericAliasIterator {
|
||||
fn repr_str(_zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
|
||||
Ok("<generic_alias_iterator>".to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterable for PyGenericAliasIterator {
|
||||
fn iter(zelf: PyRef<Self>, _vm: &VirtualMachine) -> PyResult {
|
||||
Ok(zelf.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::types::IterNext for PyGenericAliasIterator {
|
||||
fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<crate::protocol::PyIterReturn> {
|
||||
use crate::protocol::PyIterReturn;
|
||||
let mut guard = zelf.obj.lock();
|
||||
let obj = match guard.take() {
|
||||
Some(obj) => obj,
|
||||
None => return Ok(PyIterReturn::StopIteration(None)),
|
||||
};
|
||||
// Create a starred GenericAlias from the original
|
||||
let alias = obj.downcast_ref::<PyGenericAlias>().ok_or_else(|| {
|
||||
vm.new_type_error("generic_alias_iterator expected GenericAlias".to_owned())
|
||||
})?;
|
||||
let starred = PyGenericAlias::new(alias.origin.clone(), alias.args.clone(), true, vm);
|
||||
Ok(PyIterReturn::Return(starred.into_pyobject(vm)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,6 +742,6 @@ pub fn subscript_generic(type_params: PyObjectRef, vm: &VirtualMachine) -> PyRes
|
||||
}
|
||||
|
||||
pub fn init(context: &Context) {
|
||||
let generic_alias_type = &context.types.generic_alias_type;
|
||||
PyGenericAlias::extend_class(context, generic_alias_type);
|
||||
PyGenericAlias::extend_class(context, context.types.generic_alias_type);
|
||||
PyGenericAliasIterator::extend_class(context, context.types.generic_alias_iterator_type);
|
||||
}
|
||||
@@ -511,7 +511,11 @@ impl Representable for PyList {
|
||||
let s = if zelf.__len__() == 0 {
|
||||
"[]".to_owned()
|
||||
} else if let Some(_guard) = ReprGuard::enter(vm, zelf.as_object()) {
|
||||
collection_repr(None, "[", "]", zelf.borrow_vec().iter(), vm)?
|
||||
// Clone elements before calling repr to release the read lock.
|
||||
// Element repr may mutate the list (e.g., list.clear()), which
|
||||
// needs a write lock and would deadlock if read lock is held.
|
||||
let elements: Vec<PyObjectRef> = zelf.borrow_vec().to_vec();
|
||||
collection_repr(None, "[", "]", elements.iter(), vm)?
|
||||
} else {
|
||||
"[...]".to_owned()
|
||||
};
|
||||
|
||||
@@ -927,10 +927,11 @@ impl PyType {
|
||||
}
|
||||
|
||||
let mut attrs = self.attributes.write();
|
||||
// Store to __annotate_func__
|
||||
// Clear cached annotations only when setting to a new callable
|
||||
if !vm.is_none(&value) {
|
||||
attrs.swap_remove(identifier!(vm, __annotations_cache__));
|
||||
}
|
||||
attrs.insert(identifier!(vm, __annotate_func__), value.clone());
|
||||
// Always clear cached annotations when __annotate__ is updated
|
||||
attrs.swap_remove(identifier!(vm, __annotations_cache__));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -999,7 +1000,11 @@ impl PyType {
|
||||
}
|
||||
|
||||
#[pygetset(setter)]
|
||||
fn set___annotations__(&self, value: Option<PyObjectRef>, vm: &VirtualMachine) -> PyResult<()> {
|
||||
fn set___annotations__(
|
||||
&self,
|
||||
value: crate::function::PySetterValue<PyObjectRef>,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<()> {
|
||||
if self.slots.flags.has_feature(PyTypeFlags::IMMUTABLETYPE) {
|
||||
return Err(vm.new_type_error(format!(
|
||||
"cannot set '__annotations__' attribute of immutable type '{}'",
|
||||
@@ -1008,33 +1013,40 @@ impl PyType {
|
||||
}
|
||||
|
||||
let mut attrs = self.attributes.write();
|
||||
// conditional update based on __annotations__ presence
|
||||
let has_annotations = attrs.contains_key(identifier!(vm, __annotations__));
|
||||
|
||||
if has_annotations {
|
||||
// If __annotations__ is in dict, update it
|
||||
if let Some(value) = value {
|
||||
attrs.insert(identifier!(vm, __annotations__), value);
|
||||
} else if attrs
|
||||
.swap_remove(identifier!(vm, __annotations__))
|
||||
.is_none()
|
||||
{
|
||||
return Err(vm.new_attribute_error("__annotations__".to_owned()));
|
||||
match value {
|
||||
crate::function::PySetterValue::Assign(value) => {
|
||||
// SET path: store the value (including None)
|
||||
let key = if has_annotations {
|
||||
identifier!(vm, __annotations__)
|
||||
} else {
|
||||
identifier!(vm, __annotations_cache__)
|
||||
};
|
||||
attrs.insert(key, value);
|
||||
if has_annotations {
|
||||
attrs.swap_remove(identifier!(vm, __annotations_cache__));
|
||||
}
|
||||
}
|
||||
// Also clear __annotations_cache__
|
||||
attrs.swap_remove(identifier!(vm, __annotations_cache__));
|
||||
} else {
|
||||
// Otherwise update only __annotations_cache__
|
||||
if let Some(value) = value {
|
||||
attrs.insert(identifier!(vm, __annotations_cache__), value);
|
||||
} else if attrs
|
||||
.swap_remove(identifier!(vm, __annotations_cache__))
|
||||
.is_none()
|
||||
{
|
||||
return Err(vm.new_attribute_error("__annotations__".to_owned()));
|
||||
crate::function::PySetterValue::Delete => {
|
||||
// DELETE path: remove the key
|
||||
let removed = if has_annotations {
|
||||
attrs
|
||||
.swap_remove(identifier!(vm, __annotations__))
|
||||
.is_some()
|
||||
} else {
|
||||
attrs
|
||||
.swap_remove(identifier!(vm, __annotations_cache__))
|
||||
.is_some()
|
||||
};
|
||||
if !removed {
|
||||
return Err(vm.new_attribute_error("__annotations__".to_owned()));
|
||||
}
|
||||
if has_annotations {
|
||||
attrs.swap_remove(identifier!(vm, __annotations_cache__));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Always clear __annotate_func__ and __annotate__
|
||||
attrs.swap_remove(identifier!(vm, __annotate_func__));
|
||||
attrs.swap_remove(identifier!(vm, __annotate__));
|
||||
|
||||
@@ -1055,7 +1067,15 @@ impl PyType {
|
||||
Some(found)
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| vm.ctx.new_str(ascii!("builtins")).into())
|
||||
.unwrap_or_else(|| {
|
||||
// For non-heap types, extract module from tp_name (e.g. "typing.TypeAliasType" -> "typing")
|
||||
let slot_name = self.slot_name();
|
||||
if let Some((module, _)) = slot_name.rsplit_once('.') {
|
||||
vm.ctx.intern_str(module).to_object()
|
||||
} else {
|
||||
vm.ctx.new_str(ascii!("builtins")).into()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[pygetset(setter)]
|
||||
@@ -1838,6 +1858,13 @@ impl SetAttr for PyType {
|
||||
) -> PyResult<()> {
|
||||
// TODO: pass PyRefExact instead of &str
|
||||
let attr_name = vm.ctx.intern_str(attr_name.as_str());
|
||||
if zelf.slots.flags.has_feature(PyTypeFlags::IMMUTABLETYPE) {
|
||||
return Err(vm.new_type_error(format!(
|
||||
"cannot set '{}' attribute of immutable type '{}'",
|
||||
attr_name,
|
||||
zelf.slot_name()
|
||||
)));
|
||||
}
|
||||
if let Some(attr) = zelf.get_class_attr(attr_name) {
|
||||
let descr_set = attr.class().slots.descr_set.load();
|
||||
if let Some(descriptor) = descr_set {
|
||||
|
||||
+11
-4
@@ -165,10 +165,17 @@ pub trait PyClassImpl: PyClassDef {
|
||||
}
|
||||
}
|
||||
if let Some(module_name) = Self::MODULE_NAME {
|
||||
class.set_attr(
|
||||
identifier!(ctx, __module__),
|
||||
ctx.new_str(module_name).into(),
|
||||
);
|
||||
let module_key = identifier!(ctx, __module__);
|
||||
// Don't overwrite a getset descriptor for __module__ (e.g. TypeAliasType
|
||||
// has an instance-level __module__ getset that should not be replaced)
|
||||
let has_getset = class
|
||||
.attributes
|
||||
.read()
|
||||
.get(module_key)
|
||||
.is_some_and(|v| v.downcastable::<crate::builtins::PyGetSet>());
|
||||
if !has_getset {
|
||||
class.set_attr(module_key, ctx.new_str(module_name).into());
|
||||
}
|
||||
}
|
||||
|
||||
// Don't add __new__ attribute if slot_new is inherited from object
|
||||
|
||||
@@ -671,7 +671,7 @@ impl ExecutingFrame<'_> {
|
||||
} else {
|
||||
let name = self.code.freevars[i - self.code.cellvars.len()];
|
||||
vm.new_name_error(
|
||||
format!("free variable '{name}' referenced before assignment in enclosing scope"),
|
||||
format!("cannot access free variable '{name}' where it is not associated with a value in enclosing scope"),
|
||||
name.to_owned(),
|
||||
)
|
||||
}
|
||||
@@ -3118,7 +3118,7 @@ impl ExecutingFrame<'_> {
|
||||
|
||||
let name = tuple.as_slice()[0].clone();
|
||||
let type_params_obj = tuple.as_slice()[1].clone();
|
||||
let value = tuple.as_slice()[2].clone();
|
||||
let compute_value = tuple.as_slice()[2].clone();
|
||||
|
||||
let type_params: PyTupleRef = if vm.is_none(&type_params_obj) {
|
||||
vm.ctx.empty_tuple.clone()
|
||||
@@ -3131,7 +3131,7 @@ impl ExecutingFrame<'_> {
|
||||
let name = name.downcast::<crate::builtins::PyStr>().map_err(|_| {
|
||||
vm.new_type_error("TypeAliasType name must be a string".to_owned())
|
||||
})?;
|
||||
let type_alias = typing::TypeAliasType::new(name, type_params, value);
|
||||
let type_alias = typing::TypeAliasType::new(name, type_params, compute_value);
|
||||
Ok(type_alias.into_ref(&vm.ctx).into())
|
||||
}
|
||||
bytecode::IntrinsicFunction1::ListToTuple => {
|
||||
|
||||
@@ -77,6 +77,13 @@ macro_rules! impl_base_node {
|
||||
|
||||
#[extend_class]
|
||||
fn extend_class(ctx: &Context, class: &'static Py<PyType>) {
|
||||
// AST types are mutable (heap types in CPython, not IMMUTABLETYPE)
|
||||
// Safety: called during type initialization before any concurrent access
|
||||
unsafe {
|
||||
let flags = &class.slots.flags as *const crate::types::PyTypeFlags
|
||||
as *mut crate::types::PyTypeFlags;
|
||||
(*flags).remove(crate::types::PyTypeFlags::IMMUTABLETYPE);
|
||||
}
|
||||
class.set_attr(
|
||||
identifier!(ctx, _attributes),
|
||||
ctx.empty_tuple.clone().into(),
|
||||
@@ -100,6 +107,13 @@ macro_rules! impl_base_node {
|
||||
|
||||
#[extend_class]
|
||||
fn extend_class_with_fields(ctx: &Context, class: &'static Py<PyType>) {
|
||||
// AST types are mutable (heap types in CPython, not IMMUTABLETYPE)
|
||||
// Safety: called during type initialization before any concurrent access
|
||||
unsafe {
|
||||
let flags = &class.slots.flags as *const crate::types::PyTypeFlags
|
||||
as *mut crate::types::PyTypeFlags;
|
||||
(*flags).remove(crate::types::PyTypeFlags::IMMUTABLETYPE);
|
||||
}
|
||||
class.set_attr(
|
||||
identifier!(ctx, _fields),
|
||||
ctx.new_tuple(vec![
|
||||
@@ -530,6 +544,13 @@ pub(crate) struct NodeExprConstant(NodeExpr);
|
||||
impl NodeExprConstant {
|
||||
#[extend_class]
|
||||
fn extend_class_with_fields(ctx: &Context, class: &'static Py<PyType>) {
|
||||
// AST types are mutable (heap types, not IMMUTABLETYPE)
|
||||
// Safety: called during type initialization before any concurrent access
|
||||
unsafe {
|
||||
let flags = &class.slots.flags as *const crate::types::PyTypeFlags
|
||||
as *mut crate::types::PyTypeFlags;
|
||||
(*flags).remove(crate::types::PyTypeFlags::IMMUTABLETYPE);
|
||||
}
|
||||
class.set_attr(
|
||||
identifier!(ctx, _fields),
|
||||
ctx.new_tuple(vec![
|
||||
|
||||
@@ -25,6 +25,13 @@ pub(crate) mod _ast {
|
||||
impl NodeAst {
|
||||
#[extend_class]
|
||||
fn extend_class(ctx: &Context, class: &'static Py<PyType>) {
|
||||
// AST types are mutable (heap types, not IMMUTABLETYPE)
|
||||
// Safety: called during type initialization before any concurrent access
|
||||
unsafe {
|
||||
let flags = &class.slots.flags as *const crate::types::PyTypeFlags
|
||||
as *mut crate::types::PyTypeFlags;
|
||||
(*flags).remove(crate::types::PyTypeFlags::IMMUTABLETYPE);
|
||||
}
|
||||
let empty_tuple = ctx.empty_tuple.clone();
|
||||
class.set_str_attr("_fields", empty_tuple.clone(), ctx);
|
||||
class.set_str_attr("_attributes", empty_tuple.clone(), ctx);
|
||||
|
||||
@@ -391,7 +391,7 @@ pub(crate) mod typevar {
|
||||
evaluate_default: PyMutex::new(vm.ctx.none()),
|
||||
covariant: false,
|
||||
contravariant: false,
|
||||
infer_variance: false,
|
||||
infer_variance: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -631,7 +631,7 @@ pub(crate) mod typevar {
|
||||
evaluate_default: PyMutex::new(vm.ctx.none()),
|
||||
covariant: false,
|
||||
contravariant: false,
|
||||
infer_variance: false,
|
||||
infer_variance: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+254
-36
@@ -1,4 +1,4 @@
|
||||
// spell-checker:ignore typevarobject funcobj
|
||||
// spell-checker:ignore typevarobject funcobj typevartuples
|
||||
use crate::{
|
||||
Context, PyResult, VirtualMachine, builtins::pystr::AsPyStr, class::PyClassImpl,
|
||||
function::IntoFuncArgs,
|
||||
@@ -29,12 +29,13 @@ pub fn call_typing_func_object<'a>(
|
||||
#[pymodule(name = "_typing", with(super::typevar::typevar))]
|
||||
pub(crate) mod decl {
|
||||
use crate::{
|
||||
Py, PyObjectRef, PyPayload, PyResult, VirtualMachine,
|
||||
builtins::{PyStrRef, PyTupleRef, PyType, PyTypeRef, type_},
|
||||
AsObject, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, atomic_func,
|
||||
builtins::{PyGenericAlias, PyStrRef, PyTuple, PyTupleRef, PyType, PyTypeRef, type_},
|
||||
function::FuncArgs,
|
||||
protocol::PyNumberMethods,
|
||||
types::{AsNumber, Constructor, Representable},
|
||||
protocol::{PyMappingMethods, PyNumberMethods},
|
||||
types::{AsMapping, AsNumber, Constructor, Iterable, Representable},
|
||||
};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
#[pyfunction]
|
||||
pub(crate) fn _idfunc(args: FuncArgs, _vm: &VirtualMachine) -> PyObjectRef {
|
||||
@@ -84,23 +85,47 @@ pub(crate) mod decl {
|
||||
}
|
||||
|
||||
#[pyattr]
|
||||
#[pyclass(name)]
|
||||
#[pyclass(name, module = "typing")]
|
||||
#[derive(Debug, PyPayload)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct TypeAliasType {
|
||||
name: PyStrRef,
|
||||
type_params: PyTupleRef,
|
||||
value: PyObjectRef,
|
||||
// compute_value: PyObjectRef,
|
||||
// module: PyObjectRef,
|
||||
compute_value: PyObjectRef,
|
||||
cached_value: crate::common::lock::PyMutex<Option<PyObjectRef>>,
|
||||
module: Option<PyObjectRef>,
|
||||
is_lazy: bool,
|
||||
}
|
||||
#[pyclass(with(Constructor, Representable, AsNumber), flags(BASETYPE))]
|
||||
#[pyclass(
|
||||
with(Constructor, Representable, AsMapping, AsNumber, Iterable),
|
||||
flags(IMMUTABLETYPE)
|
||||
)]
|
||||
impl TypeAliasType {
|
||||
pub const fn new(name: PyStrRef, type_params: PyTupleRef, value: PyObjectRef) -> Self {
|
||||
/// Create from intrinsic: compute_value is a callable that returns the value
|
||||
pub fn new(name: PyStrRef, type_params: PyTupleRef, compute_value: PyObjectRef) -> Self {
|
||||
Self {
|
||||
name,
|
||||
type_params,
|
||||
value,
|
||||
compute_value,
|
||||
cached_value: crate::common::lock::PyMutex::new(None),
|
||||
module: None,
|
||||
is_lazy: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create with an eagerly evaluated value (used by constructor)
|
||||
fn new_eager(
|
||||
name: PyStrRef,
|
||||
type_params: PyTupleRef,
|
||||
value: PyObjectRef,
|
||||
module: Option<PyObjectRef>,
|
||||
) -> Self {
|
||||
Self {
|
||||
name,
|
||||
type_params,
|
||||
compute_value: value.clone(),
|
||||
cached_value: crate::common::lock::PyMutex::new(Some(value)),
|
||||
module,
|
||||
is_lazy: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,55 +135,200 @@ pub(crate) mod decl {
|
||||
}
|
||||
|
||||
#[pygetset]
|
||||
fn __value__(&self) -> PyObjectRef {
|
||||
self.value.clone()
|
||||
fn __value__(&self, vm: &VirtualMachine) -> PyResult {
|
||||
let cached = self.cached_value.lock().clone();
|
||||
if let Some(value) = cached {
|
||||
return Ok(value);
|
||||
}
|
||||
let value = self.compute_value.call((), vm)?;
|
||||
*self.cached_value.lock() = Some(value.clone());
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
#[pygetset]
|
||||
fn __type_params__(&self) -> PyTupleRef {
|
||||
self.type_params.clone()
|
||||
}
|
||||
|
||||
#[pygetset]
|
||||
fn __parameters__(&self, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
|
||||
// TypeVarTuples must be unpacked in __parameters__
|
||||
unpack_typevartuples(&self.type_params, vm).map(|t| t.into())
|
||||
}
|
||||
|
||||
#[pygetset]
|
||||
fn __module__(&self, vm: &VirtualMachine) -> PyObjectRef {
|
||||
if let Some(ref module) = self.module {
|
||||
return module.clone();
|
||||
}
|
||||
// Fall back to compute_value's __module__ (like PyFunction_GetModule)
|
||||
if let Ok(module) = self.compute_value.get_attr("__module__", vm) {
|
||||
return module;
|
||||
}
|
||||
vm.ctx.none()
|
||||
}
|
||||
|
||||
fn __getitem__(zelf: PyRef<Self>, args: PyObjectRef, vm: &VirtualMachine) -> PyResult {
|
||||
if zelf.type_params.is_empty() {
|
||||
return Err(
|
||||
vm.new_type_error("Only generic type aliases are subscriptable".to_owned())
|
||||
);
|
||||
}
|
||||
let args_tuple = if let Ok(tuple) = args.try_to_ref::<PyTuple>(vm) {
|
||||
tuple.to_owned()
|
||||
} else {
|
||||
PyTuple::new_ref(vec![args], &vm.ctx)
|
||||
};
|
||||
let origin: PyObjectRef = zelf.as_object().to_owned();
|
||||
Ok(PyGenericAlias::new(origin, args_tuple, false, vm).into_pyobject(vm))
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn __reduce__(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyObjectRef {
|
||||
zelf.name.clone().into()
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn __typing_unpacked_tuple_args__(&self, vm: &VirtualMachine) -> PyObjectRef {
|
||||
vm.ctx.none()
|
||||
}
|
||||
|
||||
/// Returns the evaluator for the alias value.
|
||||
#[pygetset]
|
||||
fn evaluate_value(&self, vm: &VirtualMachine) -> PyResult {
|
||||
if self.is_lazy {
|
||||
// Lazy path: return the compute function directly
|
||||
return Ok(self.compute_value.clone());
|
||||
}
|
||||
// Eager path: wrap value in a ConstEvaluator
|
||||
let value = self.compute_value.clone();
|
||||
Ok(vm
|
||||
.new_function("_ConstEvaluator", move |_args: FuncArgs| -> PyResult {
|
||||
Ok(value.clone())
|
||||
})
|
||||
.into())
|
||||
}
|
||||
|
||||
/// Check type_params ordering: non-default params must precede default params.
|
||||
/// Uses __default__ attribute to check if a type param has a default value,
|
||||
/// comparing against typing.NoDefault sentinel (like get_type_param_default).
|
||||
fn check_type_params(
|
||||
type_params: &PyTupleRef,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<Option<PyTupleRef>> {
|
||||
if type_params.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let no_default = &vm.ctx.typing_no_default;
|
||||
let mut default_seen = false;
|
||||
for param in type_params.iter() {
|
||||
let dflt = param.get_attr("__default__", vm).map_err(|_| {
|
||||
vm.new_type_error(format!(
|
||||
"Expected a type param, got {}",
|
||||
param
|
||||
.repr(vm)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|_| "?".to_owned())
|
||||
))
|
||||
})?;
|
||||
let is_no_default = dflt.is(no_default);
|
||||
if is_no_default {
|
||||
if default_seen {
|
||||
return Err(vm.new_type_error(format!(
|
||||
"non-default type parameter '{}' follows default type parameter",
|
||||
param.repr(vm)?
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
default_seen = true;
|
||||
}
|
||||
}
|
||||
Ok(Some(type_params.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Constructor for TypeAliasType {
|
||||
type Args = FuncArgs;
|
||||
|
||||
fn py_new(_cls: &Py<PyType>, args: Self::Args, vm: &VirtualMachine) -> PyResult<Self> {
|
||||
// TypeAliasType(name, value, *, type_params=None)
|
||||
if args.args.len() < 2 {
|
||||
return Err(vm.new_type_error(format!(
|
||||
"TypeAliasType() missing {} required positional argument{}: {}",
|
||||
2 - args.args.len(),
|
||||
if 2 - args.args.len() == 1 { "" } else { "s" },
|
||||
if args.args.is_empty() {
|
||||
"'name' and 'value'"
|
||||
} else {
|
||||
"'value'"
|
||||
}
|
||||
)));
|
||||
// typealias(name, value, *, type_params=())
|
||||
// name and value are positional-or-keyword; type_params is keyword-only.
|
||||
|
||||
// Reject unexpected keyword arguments
|
||||
for key in args.kwargs.keys() {
|
||||
if key != "name" && key != "value" && key != "type_params" {
|
||||
return Err(vm.new_type_error(format!(
|
||||
"typealias() got an unexpected keyword argument '{key}'"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// Reject too many positional arguments
|
||||
if args.args.len() > 2 {
|
||||
return Err(vm.new_type_error(format!(
|
||||
"TypeAliasType() takes 2 positional arguments but {} were given",
|
||||
"typealias() takes exactly 2 positional arguments ({} given)",
|
||||
args.args.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let name = args.args[0]
|
||||
.clone()
|
||||
.downcast::<crate::builtins::PyStr>()
|
||||
.map_err(|_| vm.new_type_error("TypeAliasType name must be a string".to_owned()))?;
|
||||
let value = args.args[1].clone();
|
||||
// Resolve name: positional[0] or kwarg
|
||||
let name = if !args.args.is_empty() {
|
||||
if args.kwargs.contains_key("name") {
|
||||
return Err(vm.new_type_error(
|
||||
"argument for typealias() given by name ('name') and position (1)"
|
||||
.to_owned(),
|
||||
));
|
||||
}
|
||||
args.args[0].clone()
|
||||
} else {
|
||||
args.kwargs.get("name").cloned().ok_or_else(|| {
|
||||
vm.new_type_error(
|
||||
"typealias() missing required argument 'name' (pos 1)".to_owned(),
|
||||
)
|
||||
})?
|
||||
};
|
||||
|
||||
// Resolve value: positional[1] or kwarg
|
||||
let value = if args.args.len() >= 2 {
|
||||
if args.kwargs.contains_key("value") {
|
||||
return Err(vm.new_type_error(
|
||||
"argument for typealias() given by name ('value') and position (2)"
|
||||
.to_owned(),
|
||||
));
|
||||
}
|
||||
args.args[1].clone()
|
||||
} else {
|
||||
args.kwargs.get("value").cloned().ok_or_else(|| {
|
||||
vm.new_type_error(
|
||||
"typealias() missing required argument 'value' (pos 2)".to_owned(),
|
||||
)
|
||||
})?
|
||||
};
|
||||
|
||||
let name = name.downcast::<crate::builtins::PyStr>().map_err(|obj| {
|
||||
vm.new_type_error(format!(
|
||||
"typealias() argument 'name' must be str, not {}",
|
||||
obj.class().name()
|
||||
))
|
||||
})?;
|
||||
|
||||
let type_params = if let Some(tp) = args.kwargs.get("type_params") {
|
||||
tp.clone()
|
||||
let tp = tp
|
||||
.clone()
|
||||
.downcast::<crate::builtins::PyTuple>()
|
||||
.map_err(|_| vm.new_type_error("type_params must be a tuple".to_owned()))?
|
||||
.map_err(|_| vm.new_type_error("type_params must be a tuple".to_owned()))?;
|
||||
Self::check_type_params(&tp, vm)?;
|
||||
tp
|
||||
} else {
|
||||
vm.ctx.empty_tuple.clone()
|
||||
};
|
||||
|
||||
Ok(Self::new(name, type_params, value))
|
||||
// Get caller's module name from frame globals, like typevar.rs caller()
|
||||
let module = vm
|
||||
.current_frame()
|
||||
.and_then(|f| f.globals.get_item("__name__", vm).ok());
|
||||
|
||||
Ok(Self::new_eager(name, type_params, value, module))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +338,19 @@ pub(crate) mod decl {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMapping for TypeAliasType {
|
||||
fn as_mapping() -> &'static PyMappingMethods {
|
||||
static AS_MAPPING: LazyLock<PyMappingMethods> = LazyLock::new(|| PyMappingMethods {
|
||||
subscript: atomic_func!(|mapping, needle, vm| {
|
||||
let zelf = TypeAliasType::mapping_downcast(mapping);
|
||||
TypeAliasType::__getitem__(zelf.to_owned(), needle.to_owned(), vm)
|
||||
}),
|
||||
..PyMappingMethods::NOT_IMPLEMENTED
|
||||
});
|
||||
&AS_MAPPING
|
||||
}
|
||||
}
|
||||
|
||||
impl AsNumber for TypeAliasType {
|
||||
fn as_number() -> &'static PyNumberMethods {
|
||||
static AS_NUMBER: PyNumberMethods = PyNumberMethods {
|
||||
@@ -178,6 +361,41 @@ pub(crate) mod decl {
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterable for TypeAliasType {
|
||||
fn iter(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyResult {
|
||||
// Import typing.Unpack and return iter((Unpack[self],))
|
||||
let typing = vm.import("typing", 0)?;
|
||||
let unpack = typing.get_attr("Unpack", vm)?;
|
||||
let zelf_obj: PyObjectRef = zelf.into();
|
||||
let unpacked = vm.call_method(&unpack, "__getitem__", (zelf_obj,))?;
|
||||
let tuple = PyTuple::new_ref(vec![unpacked], &vm.ctx);
|
||||
Ok(tuple.as_object().get_iter(vm)?.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap TypeVarTuples in Unpack[], matching unpack_typevartuples()
|
||||
fn unpack_typevartuples(type_params: &PyTupleRef, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
|
||||
let has_tvt = type_params
|
||||
.iter()
|
||||
.any(|p| p.downcastable::<crate::stdlib::typevar::TypeVarTuple>());
|
||||
if !has_tvt {
|
||||
return Ok(type_params.clone());
|
||||
}
|
||||
let typing = vm.import("typing", 0)?;
|
||||
let unpack_cls = typing.get_attr("Unpack", vm)?;
|
||||
let new_params: Vec<PyObjectRef> = type_params
|
||||
.iter()
|
||||
.map(|p| {
|
||||
if p.downcastable::<crate::stdlib::typevar::TypeVarTuple>() {
|
||||
vm.call_method(&unpack_cls, "__getitem__", (p.clone(),))
|
||||
} else {
|
||||
Ok(p.clone())
|
||||
}
|
||||
})
|
||||
.collect::<PyResult<_>>()?;
|
||||
Ok(PyTuple::new_ref(new_params, &vm.ctx))
|
||||
}
|
||||
|
||||
pub(crate) fn module_exec(
|
||||
vm: &VirtualMachine,
|
||||
module: &Py<crate::builtins::PyModule>,
|
||||
|
||||
@@ -93,6 +93,7 @@ pub struct TypeZoo {
|
||||
pub typing_no_default_type: &'static Py<PyType>,
|
||||
pub not_implemented_type: &'static Py<PyType>,
|
||||
pub generic_alias_type: &'static Py<PyType>,
|
||||
pub generic_alias_iterator_type: &'static Py<PyType>,
|
||||
pub union_type: &'static Py<PyType>,
|
||||
pub interpolation_type: &'static Py<PyType>,
|
||||
pub template_type: &'static Py<PyType>,
|
||||
@@ -200,6 +201,7 @@ impl TypeZoo {
|
||||
typing_no_default_type: crate::stdlib::typing::NoDefault::init_builtin_type(),
|
||||
not_implemented_type: singletons::PyNotImplemented::init_builtin_type(),
|
||||
generic_alias_type: genericalias::PyGenericAlias::init_builtin_type(),
|
||||
generic_alias_iterator_type: genericalias::PyGenericAliasIterator::init_builtin_type(),
|
||||
union_type: union_::PyUnion::init_builtin_type(),
|
||||
interpolation_type: interpolation::PyInterpolation::init_builtin_type(),
|
||||
template_type: template::PyTemplate::init_builtin_type(),
|
||||
|
||||
Reference in new issue
Block a user