@@ -6219,6 +6219,75 @@ o.v = BIG
62196219o.v = BIG
62206220assert sys.getrefcount(BIG) == base, "reassignment leaked refs"
62216221
6222+ [case testNativeAttrSetterTypeErrorPreservesValue]
6223+ import sys
6224+ from typing import Any
6225+ from testutil import assertRaises
6226+
6227+ class Fields:
6228+ def __init__(self, items: list[int], number: int, pair: tuple[int, int]) -> None:
6229+ self.items = items
6230+ self.number = number
6231+ self.pair = pair
6232+
6233+ class DeletableFields:
6234+ __deletable__ = ("pair",)
6235+
6236+ def __init__(self, pair: tuple[int, int]) -> None:
6237+ self.pair = pair
6238+
6239+ def test_type_error_preserves_value() -> None:
6240+ getrefcount: Any = getattr(sys, "getrefcount")
6241+ items = [1]
6242+ shift = 70
6243+ number = 1 << shift
6244+ pair_number = 1 << (shift + 1)
6245+ pair = (pair_number, pair_number)
6246+ fields = Fields(items, number, pair)
6247+ dynamic_fields: Any = fields
6248+ items_refcount = getrefcount(items)
6249+ number_refcount = getrefcount(number)
6250+ pair_number_refcount = getrefcount(pair_number)
6251+
6252+ with assertRaises(AttributeError):
6253+ del dynamic_fields.items
6254+ # Do not make successful pointer assignments before these checks. Free-threaded
6255+ # builds reclaim the replaced value using a QSBR-delayed decref.
6256+ with assertRaises(TypeError):
6257+ dynamic_fields.items = object()
6258+ with assertRaises(TypeError):
6259+ dynamic_fields.number = object()
6260+ with assertRaises(TypeError):
6261+ dynamic_fields.pair = object()
6262+
6263+ assert getrefcount(items) == items_refcount
6264+ assert getrefcount(number) == number_refcount
6265+ assert getrefcount(pair_number) == pair_number_refcount
6266+ assert fields.items is items
6267+ assert fields.number is number
6268+ assert fields.pair == pair
6269+ assert fields.pair[0] is pair_number
6270+
6271+ def test_deletion_releases_value() -> None:
6272+ getrefcount: Any = getattr(sys, "getrefcount")
6273+ shift = 70
6274+ number = 1 << shift
6275+ # Single-word reference fields use delayed decrefs on free-threaded builds. An
6276+ # unboxed tuple instead exercises the synchronous deletion path changed by the fix.
6277+ pair = (number, number)
6278+ fields = DeletableFields(pair)
6279+ dynamic_fields: Any = fields
6280+ stored_refcount = getrefcount(number)
6281+
6282+ del dynamic_fields.pair
6283+
6284+ after = getrefcount(number)
6285+ assert after == stored_refcount - 2, (stored_refcount, after)
6286+ # Keep the compiled tuple local live across both refcount measurements.
6287+ assert pair[0] is number
6288+ with assertRaises(AttributeError):
6289+ fields.pair
6290+
62226291[case testBorrowedFinalAttributeInLambdaAndNestedFunction]
62236292from typing import Final, Callable
62246293
0 commit comments