From c10256a7772fd01c8582d55ecc5a8e13ad5403fa Mon Sep 17 00:00:00 2001 From: ShaharNaveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Wed, 22 Oct 2025 11:21:08 +0300 Subject: [PATCH 1/2] Update `functools.py` from 3.13.9 --- Lib/functools.py | 93 ++++++--- Lib/test/test_functools.py | 413 ++++++++++++++++++++++++++++++------- 2 files changed, 402 insertions(+), 104 deletions(-) diff --git a/Lib/functools.py b/Lib/functools.py index 2ae4290f98..4c1175b815 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -19,8 +19,9 @@ # import types, weakref # Deferred to single_dispatch() from reprlib import recursive_repr from _thread import RLock -from types import GenericAlias +# Avoid importing types, so we can speedup import time +GenericAlias = type(list[int]) ################################################################################ ### update_wrapper() and wraps() decorator @@ -236,14 +237,16 @@ def __ge__(self, other): def reduce(function, sequence, initial=_initial_missing): """ - reduce(function, iterable[, initial]) -> value - - Apply a function of two arguments cumulatively to the items of a sequence - or iterable, from left to right, so as to reduce the iterable to a single - value. For example, reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) calculates - ((((1+2)+3)+4)+5). If initial is present, it is placed before the items - of the iterable in the calculation, and serves as a default when the - iterable is empty. + reduce(function, iterable[, initial], /) -> value + + Apply a function of two arguments cumulatively to the items of an iterable, from left to right. + + This effectively reduces the iterable to a single value. If initial is present, + it is placed before the items of the iterable in the calculation, and serves as + a default when the iterable is empty. + + For example, reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) + calculates ((((1 + 2) + 3) + 4) + 5). """ it = iter(sequence) @@ -284,7 +287,7 @@ def __new__(cls, func, /, *args, **keywords): if not callable(func): raise TypeError("the first argument must be callable") - if hasattr(func, "func"): + if isinstance(func, partial): args = func.args + args keywords = {**func.keywords, **keywords} func = func.func @@ -302,13 +305,23 @@ def __call__(self, /, *args, **keywords): @recursive_repr() def __repr__(self): - qualname = type(self).__qualname__ + cls = type(self) + qualname = cls.__qualname__ + module = cls.__module__ args = [repr(self.func)] args.extend(repr(x) for x in self.args) args.extend(f"{k}={v!r}" for (k, v) in self.keywords.items()) - if type(self).__module__ == "functools": - return f"functools.{qualname}({', '.join(args)})" - return f"{qualname}({', '.join(args)})" + return f"{module}.{qualname}({', '.join(args)})" + + def __get__(self, obj, objtype=None): + if obj is None: + return self + import warnings + warnings.warn('functools.partial will be a method descriptor in ' + 'future Python versions; wrap it in staticmethod() ' + 'if you want to preserve the old behavior', + FutureWarning, 2) + return self def __reduce__(self): return type(self), (self.func,), (self.func, self.args, @@ -338,6 +351,9 @@ def __setstate__(self, state): self.args = args self.keywords = kwds + __class_getitem__ = classmethod(GenericAlias) + + try: from _functools import partial except ImportError: @@ -372,28 +388,26 @@ def __init__(self, func, /, *args, **keywords): self.keywords = keywords def __repr__(self): - args = ", ".join(map(repr, self.args)) - keywords = ", ".join("{}={!r}".format(k, v) - for k, v in self.keywords.items()) - format_string = "{module}.{cls}({func}, {args}, {keywords})" - return format_string.format(module=self.__class__.__module__, - cls=self.__class__.__qualname__, - func=self.func, - args=args, - keywords=keywords) + cls = type(self) + module = cls.__module__ + qualname = cls.__qualname__ + args = [repr(self.func)] + args.extend(map(repr, self.args)) + args.extend(f"{k}={v!r}" for k, v in self.keywords.items()) + return f"{module}.{qualname}({', '.join(args)})" def _make_unbound_method(self): def _method(cls_or_self, /, *args, **keywords): keywords = {**self.keywords, **keywords} return self.func(cls_or_self, *self.args, *args, **keywords) _method.__isabstractmethod__ = self.__isabstractmethod__ - _method._partialmethod = self + _method.__partialmethod__ = self return _method def __get__(self, obj, cls=None): get = getattr(self.func, "__get__", None) result = None - if get is not None: + if get is not None and not isinstance(self.func, partial): new_func = get(obj, cls) if new_func is not self.func: # Assume __get__ returning something new indicates the @@ -423,6 +437,17 @@ def _unwrap_partial(func): func = func.func return func +def _unwrap_partialmethod(func): + prev = None + while func is not prev: + prev = func + while isinstance(getattr(func, "__partialmethod__", None), partialmethod): + func = func.__partialmethod__ + while isinstance(func, partialmethod): + func = getattr(func, 'func') + func = _unwrap_partial(func) + return func + ################################################################################ ### LRU Cache function decorator ################################################################################ @@ -483,8 +508,9 @@ def lru_cache(maxsize=128, typed=False): can grow without bound. If *typed* is True, arguments of different types will be cached separately. - For example, f(3.0) and f(3) will be treated as distinct calls with - distinct results. + For example, f(decimal.Decimal("3.0")) and f(3.0) will be treated as + distinct calls with distinct results. Some types such as str and int may + be cached separately even when typed is false. Arguments to the cached function must be hashable. @@ -660,7 +686,7 @@ def cache(user_function, /): def _c3_merge(sequences): """Merges MROs in *sequences* to a single MRO using the C3 algorithm. - Adapted from https://www.python.org/download/releases/2.3/mro/. + Adapted from https://docs.python.org/3/howto/mro.html. """ result = [] @@ -905,7 +931,6 @@ def wrapper(*args, **kw): if not args: raise TypeError(f'{funcname} requires at least ' '1 positional argument') - return dispatch(args[0].__class__)(*args, **kw) funcname = getattr(func, '__name__', 'singledispatch function') @@ -941,13 +966,18 @@ def register(self, cls, method=None): return self.dispatcher.register(cls, func=method) def __get__(self, obj, cls=None): + dispatch = self.dispatcher.dispatch + funcname = getattr(self.func, '__name__', 'singledispatchmethod method') def _method(*args, **kwargs): - method = self.dispatcher.dispatch(args[0].__class__) - return method.__get__(obj, cls)(*args, **kwargs) + if not args: + raise TypeError(f'{funcname} requires at least ' + '1 positional argument') + return dispatch(args[0].__class__).__get__(obj, cls)(*args, **kwargs) _method.__isabstractmethod__ = self.__isabstractmethod__ _method.register = self.register update_wrapper(_method, self.func) + return _method @property @@ -966,6 +996,7 @@ def __init__(self, func): self.func = func self.attrname = None self.__doc__ = func.__doc__ + self.__module__ = func.__module__ def __set_name__(self, owner, name): if self.attrname is None: diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index 32b442b0c0..7df246ef49 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -31,10 +31,6 @@ decimal = import_helper.import_fresh_module('decimal', fresh=['_decimal']) -_partial_types = [py_functools.partial] -if c_functools: - _partial_types.append(c_functools.partial) - @contextlib.contextmanager def replaced_module(name, replacement): @@ -189,6 +185,19 @@ def test_nested_optimization(self): flat = partial(signature, 'asdf', bar=True) self.assertEqual(signature(nested), signature(flat)) + def test_nested_optimization_bug(self): + partial = self.partial + class Builder: + def __call__(self, tag, *children, **attrib): + return (tag, children, attrib) + + def __getattr__(self, tag): + return partial(self, tag) + + B = Builder() + m = B.m + assert m(1, 2, a=2) == ('m', (1, 2), dict(a=2)) + def test_nested_partial_with_attribute(self): # see issue 25137 partial = self.partial @@ -207,10 +216,7 @@ def test_repr(self): kwargs = {'a': object(), 'b': object()} kwargs_reprs = ['a={a!r}, b={b!r}'.format_map(kwargs), 'b={b!r}, a={a!r}'.format_map(kwargs)] - if self.partial in _partial_types: - name = 'functools.partial' - else: - name = self.partial.__name__ + name = f"{self.partial.__module__}.{self.partial.__qualname__}" f = self.partial(capture) self.assertEqual(f'{name}({capture!r})', repr(f)) @@ -229,10 +235,7 @@ def test_repr(self): for kwargs_repr in kwargs_reprs]) def test_recursive_repr(self): - if self.partial in _partial_types: - name = 'functools.partial' - else: - name = self.partial.__name__ + name = f"{self.partial.__module__}.{self.partial.__qualname__}" f = self.partial(capture) f.__setstate__((f, (), {}, {})) @@ -344,8 +347,10 @@ def test_recursive_pickle(self): f.__setstate__((f, (), {}, {})) try: for proto in range(pickle.HIGHEST_PROTOCOL + 1): - with self.assertRaises(RecursionError): - pickle.dumps(f, proto) + # gh-117008: Small limit since pickle uses C stack memory + with support.infinite_recursion(100): + with self.assertRaises(RecursionError): + pickle.dumps(f, proto) finally: f.__setstate__((capture, (), {}, {})) @@ -390,6 +395,29 @@ def __getitem__(self, key): f = self.partial(object) self.assertRaises(TypeError, f.__setstate__, BadSequence()) + def test_partial_as_method(self): + class A: + meth = self.partial(capture, 1, a=2) + cmeth = classmethod(self.partial(capture, 1, a=2)) + smeth = staticmethod(self.partial(capture, 1, a=2)) + + a = A() + self.assertEqual(A.meth(3, b=4), ((1, 3), {'a': 2, 'b': 4})) + self.assertEqual(A.cmeth(3, b=4), ((1, A, 3), {'a': 2, 'b': 4})) + self.assertEqual(A.smeth(3, b=4), ((1, 3), {'a': 2, 'b': 4})) + with self.assertWarns(FutureWarning) as w: + self.assertEqual(a.meth(3, b=4), ((1, 3), {'a': 2, 'b': 4})) + self.assertEqual(w.filename, __file__) + self.assertEqual(a.cmeth(3, b=4), ((1, A, 3), {'a': 2, 'b': 4})) + self.assertEqual(a.smeth(3, b=4), ((1, 3), {'a': 2, 'b': 4})) + + def test_partial_genericalias(self): + alias = self.partial[int] + self.assertIs(alias.__origin__, self.partial) + self.assertEqual(alias.__args__, (int,)) + self.assertEqual(alias.__parameters__, ()) + + @unittest.skipUnless(c_functools, 'requires the C _functools module') class TestPartialC(TestPartial, unittest.TestCase): if c_functools: @@ -454,14 +482,6 @@ class TestPartialCSubclass(TestPartialC): if c_functools: partial = CPartialSubclass - # TODO: RUSTPYTHON - def test_pickle(self): - TestPartial.test_pickle(self) - - # TODO: RUSTPYTHON - def test_recursive_pickle(self): - TestPartial.test_recursive_pickle(self) - # partial subclasses are not optimized for nested calls test_nested_optimization = None @@ -572,6 +592,14 @@ class B: method = functools.partialmethod(func=capture, a=1) def test_repr(self): + self.assertEqual(repr(vars(self.A)['nothing']), + 'functools.partialmethod({})'.format(capture)) + self.assertEqual(repr(vars(self.A)['positional']), + 'functools.partialmethod({}, 1)'.format(capture)) + self.assertEqual(repr(vars(self.A)['keywords']), + 'functools.partialmethod({}, a=2)'.format(capture)) + self.assertEqual(repr(vars(self.A)['spec_keywords']), + 'functools.partialmethod({}, self=1, func=2)'.format(capture)) self.assertEqual(repr(vars(self.A)['both']), 'functools.partialmethod({}, 3, b=4)'.format(capture)) @@ -620,9 +648,7 @@ def check_wrapper(self, wrapper, wrapped, def _default_update(self): - # XXX: RUSTPYTHON; f[T] is not supported yet - # def f[T](a:'This is a new annotation'): - def f(a:'This is a new annotation'): + def f[T](a:'This is a new annotation'): """This is a test""" pass f.attr = 'This is also a test' @@ -632,8 +658,7 @@ def wrapper(b:'This is the prior annotation'): functools.update_wrapper(wrapper, f) return wrapper, f - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON def test_default_update(self): wrapper, f = self._default_update() self.check_wrapper(wrapper, f) @@ -717,6 +742,14 @@ def wrapper(): self.assertTrue(wrapper.__doc__.startswith('max(')) self.assertEqual(wrapper.__annotations__, {}) + def test_update_type_wrapper(self): + def wrapper(*args): pass + + functools.update_wrapper(wrapper, type) + self.assertEqual(wrapper.__name__, 'type') + self.assertEqual(wrapper.__annotations__, {}) + self.assertEqual(wrapper.__type_params__, ()) + class TestWraps(TestUpdateWrapper): @@ -951,9 +984,16 @@ def mycmp(x, y): self.assertRaises(TypeError, hash, k) self.assertNotIsInstance(k, collections.abc.Hashable) + @unittest.skipIf(support.MISSING_C_DOCSTRINGS, + "Signature information for builtins requires docstrings") def test_cmp_to_signature(self): - self.assertEqual(str(Signature.from_callable(self.cmp_to_key)), - '(mycmp)') + sig = Signature.from_callable(self.cmp_to_key) + self.assertEqual(str(sig), '(mycmp)') + def mycmp(x, y): + return y - x + sig = Signature.from_callable(self.cmp_to_key(mycmp)) + self.assertEqual(str(sig), '(obj)') + @unittest.skipUnless(c_functools, 'requires the C _functools module') @@ -961,52 +1001,44 @@ class TestCmpToKeyC(TestCmpToKey, unittest.TestCase): if c_functools: cmp_to_key = c_functools.cmp_to_key - # TODO: RUSTPYTHON - @unittest.expectedFailure + @support.cpython_only + def test_disallow_instantiation(self): + # Ensure that the type disallows instantiation (bpo-43916) + support.check_disallow_instantiation( + self, type(c_functools.cmp_to_key(None)) + ) + + @unittest.expectedFailure # TODO: RUSTPYTHON def test_bad_cmp(self): - super().test_bad_cmp() + return super().test_bad_cmp() - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON def test_cmp_to_key(self): - super().test_cmp_to_key() + return super().test_cmp_to_key() - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON def test_cmp_to_key_arguments(self): - super().test_cmp_to_key_arguments() + return super().test_cmp_to_key_arguments() - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON + def test_cmp_to_signature(self): + return super().test_cmp_to_signature() + + @unittest.expectedFailure # TODO: RUSTPYTHON def test_hash(self): - super().test_hash() + return super().test_hash() - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON def test_obj_field(self): - super().test_obj_field() + return super().test_obj_field() - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON def test_sort_int(self): - super().test_sort_int() + return super().test_sort_int() - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON def test_sort_int_str(self): - super().test_sort_int_str() - - # TODO: RUSTPYTHON - @unittest.expectedFailure - def test_cmp_to_signature(self): - super().test_cmp_to_signature() - - @support.cpython_only - def test_disallow_instantiation(self): - # Ensure that the type disallows instantiation (bpo-43916) - support.check_disallow_instantiation( - self, type(c_functools.cmp_to_key(None)) - ) + return super().test_sort_int_str() class TestCmpToKeyPy(TestCmpToKey, unittest.TestCase): @@ -1340,6 +1372,16 @@ def fib(n): self.module._CacheInfo(hits=0, misses=0, maxsize=None, currsize=0)) +class TestCachePy(TestCache, unittest.TestCase): + module = py_functools + + +@unittest.skipUnless(c_functools, 'requires the C _functools module') +class TestCacheC(TestCache, unittest.TestCase): + if c_functools: + module = c_functools + + class TestLRU: def test_lru(self): @@ -1773,8 +1815,7 @@ def f(x): time.sleep(.01) return 3 * x def test(i, x): - with self.subTest(thread=i): - self.assertEqual(f(x), 3 * x, i) + self.assertEqual(f(x), 3 * x, i) threads = [threading.Thread(target=test, args=(i, v)) for i, v in enumerate([1, 2, 2, 3, 2])] with threading_helper.start_threads(threads): @@ -1878,6 +1919,7 @@ def f(): return 1 self.assertEqual(f.cache_parameters(), {'maxsize': 1000, "typed": True}) + @support.suppress_immortalization() def test_lru_cache_weakrefable(self): @self.module.lru_cache def test_function(x): @@ -1908,12 +1950,33 @@ def test_staticmethod(x): self.assertIsNone(ref()) def test_common_signatures(self): - def orig(): ... + def orig(a, /, b, c=True): ... lru = self.module.lru_cache(1)(orig) + self.assertEqual(str(Signature.from_callable(lru)), '(a, /, b, c=True)') self.assertEqual(str(Signature.from_callable(lru.cache_info)), '()') self.assertEqual(str(Signature.from_callable(lru.cache_clear)), '()') + @support.skip_on_s390x + @unittest.skipIf(support.is_wasi, "WASI has limited C stack") + def test_lru_recursion(self): + + @self.module.lru_cache + def fib(n): + if n <= 1: + return n + return fib(n-1) + fib(n-2) + + if not support.Py_DEBUG: + depth = support.get_c_recursion_limit()*2//7 + with support.infinite_recursion(): + fib(depth) + if self.module == c_functools: + fib.cache_clear() + with support.infinite_recursion(): + with self.assertRaises(RecursionError): + fib(10000) + @py_functools.lru_cache() def py_cached_func(x, y): @@ -2524,6 +2587,74 @@ def _(arg): self.assertTrue(A.t('')) self.assertEqual(A.t(0.0), 0.0) + def test_slotted_class(self): + class Slot: + __slots__ = ('a', 'b') + @functools.singledispatchmethod + def go(self, item, arg): + pass + + @go.register + def _(self, item: int, arg): + return item + arg + + s = Slot() + self.assertEqual(s.go(1, 1), 2) + + def test_classmethod_slotted_class(self): + class Slot: + __slots__ = ('a', 'b') + @functools.singledispatchmethod + @classmethod + def go(cls, item, arg): + pass + + @go.register + @classmethod + def _(cls, item: int, arg): + return item + arg + + s = Slot() + self.assertEqual(s.go(1, 1), 2) + self.assertEqual(Slot.go(1, 1), 2) + + def test_staticmethod_slotted_class(self): + class A: + __slots__ = ['a'] + @functools.singledispatchmethod + @staticmethod + def t(arg): + return arg + @t.register(int) + @staticmethod + def _(arg): + return isinstance(arg, int) + @t.register(str) + @staticmethod + def _(arg): + return isinstance(arg, str) + a = A() + + self.assertTrue(A.t(0)) + self.assertTrue(A.t('')) + self.assertEqual(A.t(0.0), 0.0) + self.assertTrue(a.t(0)) + self.assertTrue(a.t('')) + self.assertEqual(a.t(0.0), 0.0) + + def test_assignment_behavior(self): + # see gh-106448 + class A: + @functools.singledispatchmethod + def t(arg): + return arg + + a = A() + a.t.foo = 'bar' + a2 = A() + with self.assertRaises(AttributeError): + a2.t.foo + def test_classmethod_register(self): class A: def __init__(self, arg): @@ -2659,6 +2790,7 @@ def static_func(arg: int) -> str: """My function docstring""" return str(arg) + prefix = A.__qualname__ + '.' for meth in ( A.func, A().func, @@ -2668,7 +2800,11 @@ def static_func(arg: int) -> str: A().static_func ): with self.subTest(meth=meth): - self.assertEqual(meth.__doc__, 'My function docstring') + self.assertEqual(meth.__qualname__, prefix + meth.__name__) + self.assertEqual(meth.__doc__, + ('My function docstring' + if support.HAVE_PY_DOCSTRINGS + else None)) self.assertEqual(meth.__annotations__['arg'], int) self.assertEqual(A.func.__name__, 'func') @@ -2757,7 +2893,10 @@ def decorated_classmethod(cls, arg: int) -> str: WithSingleDispatch().decorated_classmethod ): with self.subTest(meth=meth): - self.assertEqual(meth.__doc__, 'My function docstring') + self.assertEqual(meth.__doc__, + ('My function docstring' + if support.HAVE_PY_DOCSTRINGS + else None)) self.assertEqual(meth.__annotations__['arg'], int) self.assertEqual( @@ -2829,11 +2968,26 @@ def _(arg: typing.Union[int, typing.Iterable[str]]): def test_invalid_positional_argument(self): @functools.singledispatch - def f(*args): + def f(*args, **kwargs): pass msg = 'f requires at least 1 positional argument' with self.assertRaisesRegex(TypeError, msg): f() + msg = 'f requires at least 1 positional argument' + with self.assertRaisesRegex(TypeError, msg): + f(a=1) + + def test_invalid_positional_argument_singledispatchmethod(self): + class A: + @functools.singledispatchmethod + def t(self, *args, **kwargs): + pass + msg = 't requires at least 1 positional argument' + with self.assertRaisesRegex(TypeError, msg): + A().t() + msg = 't requires at least 1 positional argument' + with self.assertRaisesRegex(TypeError, msg): + A().t(a=1) def test_union(self): @functools.singledispatch @@ -2957,6 +3111,115 @@ def _(arg: typing.List[float] | bytes): self.assertEqual(f(""), "default") self.assertEqual(f(b""), "default") + def test_method_equal_instances(self): + # gh-127750: Reference to self was cached + class A: + def __eq__(self, other): + return True + def __hash__(self): + return 1 + @functools.singledispatchmethod + def t(self, arg): + return self + + a = A() + b = A() + self.assertIs(a.t(1), a) + self.assertIs(b.t(2), b) + + def test_method_bad_hash(self): + class A: + def __eq__(self, other): + raise AssertionError + def __hash__(self): + raise AssertionError + @functools.singledispatchmethod + def t(self, arg): + pass + + # Should not raise + A().t(1) + hash(A().t) + A().t == A().t + + def test_method_no_reference_loops(self): + # gh-127750: Created a strong reference to self + class A: + @functools.singledispatchmethod + def t(self, arg): + return weakref.ref(self) + + a = A() + r = a.t(1) + self.assertIsNotNone(r()) + del a # delete a after a.t + if not support.check_impl_detail(cpython=True): + support.gc_collect() + self.assertIsNone(r()) + + a = A() + t = a.t + del a # delete a before a.t + support.gc_collect() + r = t(1) + self.assertIsNotNone(r()) + del t + if not support.check_impl_detail(cpython=True): + support.gc_collect() + self.assertIsNone(r()) + + def test_signatures(self): + @functools.singledispatch + def func(item, arg: int) -> str: + return str(item) + @func.register + def _(item: int, arg: bytes) -> str: + return str(item) + + self.assertEqual(str(Signature.from_callable(func)), + '(item, arg: int) -> str') + + def test_method_signatures(self): + class A: + def m(self, item, arg: int) -> str: + return str(item) + @classmethod + def cm(cls, item, arg: int) -> str: + return str(item) + @functools.singledispatchmethod + def func(self, item, arg: int) -> str: + return str(item) + @func.register + def _(self, item, arg: bytes) -> str: + return str(item) + + @functools.singledispatchmethod + @classmethod + def cls_func(cls, item, arg: int) -> str: + return str(arg) + @func.register + @classmethod + def _(cls, item, arg: bytes) -> str: + return str(item) + + @functools.singledispatchmethod + @staticmethod + def static_func(item, arg: int) -> str: + return str(arg) + @func.register + @staticmethod + def _(item, arg: bytes) -> str: + return str(item) + + self.assertEqual(str(Signature.from_callable(A.func)), + '(self, item, arg: int) -> str') + self.assertEqual(str(Signature.from_callable(A().func)), + '(self, item, arg: int) -> str') + self.assertEqual(str(Signature.from_callable(A.cls_func)), + '(cls, item, arg: int) -> str') + self.assertEqual(str(Signature.from_callable(A.static_func)), + '(item, arg: int) -> str') + class CachedCostItem: _cost = 1 @@ -3007,8 +3270,7 @@ def test_cached_attribute_name_differs_from_func_name(self): self.assertEqual(item.get_cost(), 4) self.assertEqual(item.cached_cost, 3) - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON def test_object_with_slots(self): item = CachedCostItemWithSlots() with self.assertRaisesRegex( @@ -3032,8 +3294,7 @@ class MyClass(metaclass=MyMeta): ): MyClass.prop - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON def test_reuse_different_names(self): """Disallow this case because decorated function a would not be cached.""" with self.assertRaises(TypeError) as ctx: @@ -3089,7 +3350,13 @@ def test_access_from_class(self): self.assertIsInstance(CachedCostItem.cost, py_functools.cached_property) def test_doc(self): - self.assertEqual(CachedCostItem.cost.__doc__, "The cost of the item.") + self.assertEqual(CachedCostItem.cost.__doc__, + ("The cost of the item." + if support.HAVE_PY_DOCSTRINGS + else None)) + + def test_module(self): + self.assertEqual(CachedCostItem.cost.__module__, CachedCostItem.__module__) def test_subclass_with___set__(self): """Caching still works for a subclass defining __set__.""" From 6902669e2c41d97f7a056c0fcfad790aaef1be95 Mon Sep 17 00:00:00 2001 From: ShaharNaveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Wed, 22 Oct 2025 11:29:59 +0300 Subject: [PATCH 2/2] mark/unmark tests --- Lib/test/test_functools.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index 7df246ef49..8050c4c897 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -395,6 +395,7 @@ def __getitem__(self, key): f = self.partial(object) self.assertRaises(TypeError, f.__setstate__, BadSequence()) + @unittest.expectedFailure # TODO: RUSTPYTHON def test_partial_as_method(self): class A: meth = self.partial(capture, 1, a=2) @@ -464,6 +465,14 @@ def __str__(self): self.assertIn('astr', r) self.assertIn("['sth']", r) + @unittest.expectedFailure # TODO: RUSTPYTHON + def test_repr(self): + return super().test_repr() + + @unittest.expectedFailure # TODO: RUSTPYTHON + def test_recursive_repr(self): + return super().test_recursive_repr() + class TestPartialPy(TestPartial, unittest.TestCase): module = py_functools @@ -658,7 +667,6 @@ def wrapper(b:'This is the prior annotation'): functools.update_wrapper(wrapper, f) return wrapper, f - @unittest.expectedFailure # TODO: RUSTPYTHON def test_default_update(self): wrapper, f = self._default_update() self.check_wrapper(wrapper, f)