private-attribute-cpp 2.1.5__tar.gz → 2.1.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. {private_attribute_cpp-2.1.5/private_attribute_cpp.egg-info → private_attribute_cpp-2.1.7}/PKG-INFO +1 -1
  2. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7}/private_attribute.cpp +267 -254
  3. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7/private_attribute_cpp.egg-info}/PKG-INFO +1 -1
  4. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7}/setup.py +1 -1
  5. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7}/LICENSE +0 -0
  6. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7}/MANIFEST.in +0 -0
  7. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7}/README.md +0 -0
  8. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7}/picosha2.h +0 -0
  9. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7}/private_attribute.pyi +0 -0
  10. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7}/private_attribute_cpp.egg-info/SOURCES.txt +0 -0
  11. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7}/private_attribute_cpp.egg-info/dependency_links.txt +0 -0
  12. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7}/private_attribute_cpp.egg-info/not-zip-safe +0 -0
  13. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7}/private_attribute_cpp.egg-info/top_level.txt +0 -0
  14. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7}/setup.cfg +0 -0
  15. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7}/tests/test_attr_classify_owner.py +0 -0
  16. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7}/tests/test_class_private_attrs.py +0 -0
  17. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7}/tests/test_gc_collect.py +0 -0
  18. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7}/tests/test_private_abc.py +0 -0
  19. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7}/tests/test_private_attrs.py +0 -0
  20. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7}/tests/test_private_func.py +0 -0
  21. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7}/tests/test_private_inheritance.py +0 -0
  22. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7}/tests/test_private_method.py +0 -0
  23. {private_attribute_cpp-2.1.5 → private_attribute_cpp-2.1.7}/tests/test_private_wrap.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: private_attribute_cpp
3
- Version: 2.1.5
3
+ Version: 2.1.7
4
4
  Summary: A Python package that provides a way to define private attributes in C++ implementation.
5
5
  Home-page: https://github.com/Locked-chess-official/private_attribute_cpp
6
6
  Author: HuangHaoHua
@@ -220,9 +220,6 @@ namespace {
220
220
  // defaults as "not a real original" and keep walking upward.
221
221
  static traverseproc captured_subtype_traverse = NULL;
222
222
  static inquiry captured_subtype_clear = NULL;
223
- // all type tp_finalizer map
224
- static std::unordered_map<uintptr_t, destructor> all_type_finalize;
225
- // all type tp_del map (old-style destructor; see PrivateAttr_tp_del)
226
223
  static std::unordered_map<uintptr_t, destructor> all_type_del;
227
224
 
228
225
  static std::shared_mutex all_register_new_metaclass_mutex;
@@ -299,7 +296,6 @@ clean_all_storages() noexcept
299
296
  AllData::all_type_setattro.clear();
300
297
  AllData::all_type_traverse.clear();
301
298
  AllData::all_type_clear.clear();
302
- AllData::all_type_finalize.clear();
303
299
  AllData::all_type_del.clear();
304
300
  AllData::all_register_type_weak_ref.clear();
305
301
  AllData::store_module_self.clear();
@@ -1355,7 +1351,6 @@ typedef struct {
1355
1351
  static void PrivateAttr_object_init_private_dict(uintptr_t obj_id, uintptr_t type_id) noexcept;
1356
1352
  static int PrivateAttr_tp_setattro(PyObject* self, PyObject* name, PyObject* value) noexcept;
1357
1353
  static void PrivateAttr_tp_finalize(PyObject* self) noexcept;
1358
- static void PrivateAttr_tp_del(PyObject* self) noexcept;
1359
1354
  static void clear_object_related(uintptr_t id_self, uintptr_t typ_id) noexcept;
1360
1355
 
1361
1356
  static PyObject*
@@ -1434,58 +1429,6 @@ PrivateAttr_tp_setattro(PyObject* self, PyObject* name, PyObject* value) noexcep
1434
1429
  return PyObject_GenericSetAttr(self, name, value);
1435
1430
  }
1436
1431
 
1437
- static void
1438
- PrivateAttr_tp_finalize(PyObject* self) noexcept
1439
- {
1440
- PyTypeObject* typ = Py_TYPE(self);
1441
- uintptr_t typ_id = (uintptr_t)typ;
1442
- if (::AllData::all_type_finalize.find(typ_id) != ::AllData::all_type_finalize.end()){
1443
- if (::AllData::all_type_finalize[typ_id]) {
1444
- ::AllData::all_type_finalize[typ_id](self);
1445
- ensure_tp(typ);
1446
- }
1447
- }
1448
- // Per-object cleanup moved to PrivateAttr_tp_del (see below).
1449
- }
1450
-
1451
- // ========================================================================
1452
- // tp_del : per-object cleanup for private attributes.
1453
- //
1454
- // subtype_dealloc calls tp_del in BOTH dealloc branches (non-GC typeobject.c
1455
- // ~2737 and GC ~2814) before clear_slots / basedealloc, and explicitly checks
1456
- // Py_REFCNT(self) > 0 to detect resurrection. This is the natural hook for
1457
- // clearing ::AllData per-object entries:
1458
- // - it is reached on every object death path (refcount drop AND cyclic GC),
1459
- // - the resurrection check is explicit and readable here (Py_REFCNT > 0),
1460
- // - no dependency on tp_finalize / weakref / slots / __dict__.
1461
- // We keep PrivateAttr_tp_finalize (so user __del__ / tp_finalize still runs
1462
- // via PyObject_CallFinalizerFromDealloc) but it no longer clears storage.
1463
- // ========================================================================
1464
- static void
1465
- PrivateAttr_tp_del(PyObject* self) noexcept
1466
- {
1467
- uintptr_t id_self = (uintptr_t)self;
1468
- PyTypeObject* typ = Py_TYPE(self);
1469
- uintptr_t typ_id = (uintptr_t)typ;
1470
-
1471
- // If a user tp_finalize / tp_del resurrected the object, subtype_dealloc
1472
- // returns without freeing, so we must NOT clear the private storage yet.
1473
- if (Py_REFCNT(self) > 0) {
1474
- return;
1475
- }
1476
-
1477
- // Chain to the original tp_del if there is one (we are called instead of
1478
- // the original, so we must invoke it to keep old-style destructors working).
1479
- if (::AllData::all_type_del.find(typ_id) != ::AllData::all_type_del.end()) {
1480
- destructor orig = ::AllData::all_type_del[typ_id];
1481
- if (orig && orig != PrivateAttr_tp_del) {
1482
- orig(self);
1483
- }
1484
- }
1485
-
1486
- clear_object_related(id_self, typ_id);
1487
- }
1488
-
1489
1432
  // shared helper for clearing object-related data (used by tp_clear and tp_finalize)
1490
1433
  static void
1491
1434
  clear_object_related(uintptr_t id_self, uintptr_t typ_id) noexcept
@@ -1597,6 +1540,61 @@ PrivateAttr_tp_traverse(PyObject* self, visitproc visit, void* arg) noexcept
1597
1540
  }
1598
1541
  }
1599
1542
 
1543
+ // Instance-dict visit (mirror of subtype_traverse's dictoffset block).
1544
+ // The saved "original" for a private-attribute class is subtype_traverse,
1545
+ // but because the class's LIVE tp_traverse has been replaced with this
1546
+ // wrapper, subtype_traverse skips its own slot-walking loop, keeps base ==
1547
+ // typ, and its `type->tp_dictoffset != base->tp_dictoffset` check sees equal
1548
+ // offsets -> it never visits the instance __dict__. The instance __dict__
1549
+ // (e.g. `self.__dict__['x'] = self` self-cycles) would then be invisible to
1550
+ // the collector and leak. Do the visit explicitly here instead.
1551
+ //
1552
+ // The managed-dict API is version-dependent: 3.13+ exposes the public
1553
+ // PyObject_VisitManagedDict, 3.12 only the internal _PyObject_VisitManagedDict,
1554
+ // and <=3.11 has no managed dict at all (classic positive tp_dictoffset,
1555
+ // visited through _PyObject_GetDictPtr like subtype_traverse does).
1556
+ #if PY_VERSION_HEX >= 0x030D0000
1557
+ if ((typ->tp_flags & Py_TPFLAGS_MANAGED_DICT)
1558
+ && typ->tp_dictoffset == -1) {
1559
+ int err = PyObject_VisitManagedDict(self, visit, arg);
1560
+ if (err) {
1561
+ return err;
1562
+ }
1563
+ }
1564
+ #elif PY_VERSION_HEX >= 0x030C0000
1565
+ if ((typ->tp_flags & Py_TPFLAGS_MANAGED_DICT)
1566
+ && typ->tp_dictoffset == -1) {
1567
+ int err = _PyObject_VisitManagedDict(self, visit, arg);
1568
+ if (err) {
1569
+ return err;
1570
+ }
1571
+ }
1572
+ #else
1573
+ PyObject** dictptr = _PyObject_GetDictPtr(self);
1574
+ if (dictptr && *dictptr) {
1575
+ int err = visit(*dictptr, arg);
1576
+ if (err) {
1577
+ return err;
1578
+ }
1579
+ }
1580
+ #endif
1581
+
1582
+ // Instance->type link (mirror of subtype_traverse's heap-type visit).
1583
+ // subtype_traverse(self) does Py_VISIT(Py_TYPE(self)) only when the
1584
+ // resolved base traverse does NOT belong to a heap type. Because the
1585
+ // instance's LIVE tp_traverse has been replaced with this wrapper,
1586
+ // subtype_traverse keeps base == typ (heap), so that Py_VISIT is skipped
1587
+ // and a cycle like A ->(tp_dict)-> a and a ->(ob_type)-> A is invisible
1588
+ // to the collector. Visit the instance's type explicitly when it is a heap
1589
+ // type, matching subtype_traverse's intent. (This is the same fix applied
1590
+ // to the metaclass wrapper PrivateAttrType_tp_traverse.)
1591
+ {
1592
+ PyTypeObject* meta = Py_TYPE(self);
1593
+ if (meta && (meta->tp_flags & Py_TPFLAGS_HEAPTYPE)) {
1594
+ Py_VISIT((PyObject*)meta);
1595
+ }
1596
+ }
1597
+
1600
1598
  // traverse per-object private attributes for this type and its parents
1601
1599
  std::vector<uintptr_t> parent_ids;
1602
1600
  if (::AllData::all_type_parent_id.find(typ_id) != ::AllData::all_type_parent_id.end()){
@@ -1647,11 +1645,99 @@ PrivateAttr_tp_clear(PyObject* self) noexcept
1647
1645
  if (orig) orig(self);
1648
1646
  }
1649
1647
 
1648
+ // Instance-dict clear (mirror of subtype_clear's dict handling). Same
1649
+ // reasoning as the traverse side: subtype_clear can't run its dict block
1650
+ // once the LIVE tp_clear has been replaced, so clear the instance dict
1651
+ // explicitly to break __dict__ self-cycles. Version-dependent APIs, see
1652
+ // the matching traverse block above.
1653
+ #if PY_VERSION_HEX >= 0x030D0000
1654
+ if ((typ->tp_flags & Py_TPFLAGS_MANAGED_DICT)
1655
+ && typ->tp_dictoffset == -1) {
1656
+ PyObject_ClearManagedDict(self);
1657
+ }
1658
+ #elif PY_VERSION_HEX >= 0x030C0000
1659
+ if ((typ->tp_flags & Py_TPFLAGS_MANAGED_DICT)
1660
+ && typ->tp_dictoffset == -1) {
1661
+ _PyObject_ClearManagedDict(self);
1662
+ }
1663
+ #else
1664
+ PyObject** dictptr = _PyObject_GetDictPtr(self);
1665
+ if (dictptr && *dictptr) {
1666
+ Py_CLEAR(*dictptr);
1667
+ }
1668
+ #endif
1669
+
1650
1670
  // clear our per-object data
1651
1671
  clear_object_related(id_self, typ_id);
1652
1672
  return 0;
1653
1673
  }
1654
1674
 
1675
+ class KeepPythonException {
1676
+ private:
1677
+ PyObject* typ;
1678
+ PyObject* exc;
1679
+ PyObject* tb;
1680
+ public:
1681
+ KeepPythonException() noexcept {
1682
+ // under 3.12 use PyErr_Fetch
1683
+ #if PY_VERSION_HEX < 0x030C0000
1684
+ PyErr_Fetch(&typ, &exc, &tb);
1685
+ #else
1686
+ exc = PyErr_GetRaisedException();
1687
+ #endif
1688
+ }
1689
+
1690
+ ~KeepPythonException() noexcept {
1691
+ // under 3.12 use PyErr_Restore
1692
+ #if PY_VERSION_HEX < 0x030C0000
1693
+ PyErr_Restore(typ, exc, tb);
1694
+ #else
1695
+ PyErr_SetRaisedException(exc);
1696
+ #endif
1697
+ }
1698
+ };
1699
+
1700
+ static void
1701
+ PrivateAttr_tp_finalize(PyObject* self) noexcept
1702
+ {
1703
+ KeepPythonException _;
1704
+ uintptr_t id_self = (uintptr_t)self;
1705
+ PyTypeObject* typ = Py_TYPE(self);
1706
+ uintptr_t typ_id = (uintptr_t)typ;
1707
+
1708
+ Py_ssize_t refcnt = Py_REFCNT(self);
1709
+ // Chain to the original tp_finalize if there is one.
1710
+ if (::AllData::all_type_del.find(typ_id) != ::AllData::all_type_del.end()) {
1711
+ destructor orig = ::AllData::all_type_del[typ_id];
1712
+ if (orig && orig != PrivateAttr_tp_finalize) {
1713
+ orig(self);
1714
+ }
1715
+ }
1716
+ if (Py_REFCNT(self) > refcnt) {
1717
+ return;
1718
+ }
1719
+ clear_object_related(id_self, typ_id);
1720
+ }
1721
+
1722
+ static destructor
1723
+ get_need_tp_finalize(PyTypeObject* cls) noexcept
1724
+ {
1725
+ PyTypeObject* base = cls->tp_base;
1726
+ while (base) {
1727
+ if (base->tp_finalize != PrivateAttr_tp_finalize) {
1728
+ return base->tp_finalize;
1729
+ } else if (::AllData::all_type_del.find((uintptr_t)base) != ::AllData::all_type_del.end()) {
1730
+ if (::AllData::all_type_del[(uintptr_t)base]) {
1731
+ return ::AllData::all_type_del[(uintptr_t)base];
1732
+ }
1733
+ base = base->tp_base;
1734
+ } else {
1735
+ base = base->tp_base;
1736
+ }
1737
+ }
1738
+ return NULL;
1739
+ }
1740
+
1655
1741
  // get_type_need_tp_* : the metaclass counterparts of get_need_tp_*.
1656
1742
  // get_need_tp_* -> find the original slots of *types created by* the
1657
1743
  // metaclasses (wrapped with the instance-level
@@ -1659,20 +1745,19 @@ PrivateAttr_tp_clear(PyObject* self) noexcept
1659
1745
  // get_type_need_tp_* -> find the original slots of the *metaclasses*
1660
1746
  // themselves (wrapped with the metaclass-level
1661
1747
  // PrivateAttrType_getattr / PrivateAttrType_setattr /
1662
- // register_finalize / PrivateAttrType_tp_traverse /
1748
+ // PrivateAttrType_tp_traverse /
1663
1749
  // PrivateAttrType_tp_clear).
1664
1750
  static getattrofunc get_type_need_tp_getattro(PyTypeObject* cls) noexcept;
1665
1751
  static setattrofunc get_type_need_tp_setattro(PyTypeObject* cls) noexcept;
1666
1752
  static traverseproc get_type_need_tp_traverse(PyTypeObject* cls) noexcept;
1667
1753
  static inquiry get_type_need_tp_clear(PyTypeObject* cls) noexcept;
1668
- static destructor get_type_need_tp_finalize(PyTypeObject* cls) noexcept;
1669
1754
 
1670
1755
  // metaclass-level (type object) traverse/clear: call original if any, and handle AllData-stored type attributes
1671
1756
  static int
1672
1757
  PrivateAttrType_tp_traverse(PyObject* self, visitproc visit, void* arg) noexcept
1673
1758
  {
1674
- PyTypeObject* typ = (PyTypeObject*)self;
1675
- uintptr_t typ_id = (uintptr_t)typ;
1759
+ PyTypeObject* typ = Py_TYPE(self);
1760
+ uintptr_t typ_id = (uintptr_t)self;
1676
1761
 
1677
1762
  // Re-entrancy guard: see PrivateAttrRecursionGuard above.
1678
1763
  if (!PrivateAttrRecursionGuard::try_enter((uintptr_t)self)) {
@@ -1680,20 +1765,39 @@ PrivateAttrType_tp_traverse(PyObject* self, visitproc visit, void* arg) noexcept
1680
1765
  }
1681
1766
  PrivateAttrRecursionGuard guard((uintptr_t)self);
1682
1767
 
1683
- // Call the real CPython type_traverse (PyType_Type.tp_traverse) so the
1684
- // class's own hard self-cycle (tp_dict <-> tp_mro <-> tp_bases) is visible
1685
- // to the cyclic GC. Walking the metaclass base chain here would land on the
1686
- // instance-level delegating defaults (subtype_traverse / PrivateAttr_tp_traverse)
1687
- // that re-enter our guard and return 0, leaving the class's internal cycle
1688
- // invisible to the collector and the class uncollectable.
1768
+ // Recover the original traverse via get_type_need_tp_traverse, skipping
1769
+ // any delegating default (subtype_traverse / PrivateAttr_tp_traverse / our
1770
+ // own wrapper). For a type object the only correct "real" traverse is
1771
+ // CPython's type_traverse; when no real one exists (plain class objects
1772
+ // delegate through subtype_traverse, which would follow the LIVE tp_traverse
1773
+ // slot straight back into this wrapper), we rely on the AllData traversal
1774
+ // below to reach the class's hard self-cycle (tp_dict / tp_mro / tp_bases)
1775
+ // through the type-level private attributes we visit.
1689
1776
  {
1690
- traverseproc orig = PyType_Type.tp_traverse; // == type_traverse
1777
+ traverseproc orig = get_type_need_tp_traverse(typ);
1691
1778
  if (orig) {
1692
1779
  int res = orig(self, visit, arg);
1693
1780
  if (res) return res;
1694
1781
  }
1695
1782
  }
1696
1783
 
1784
+ // Mirror CPython's subtype_traverse for the instance->type link.
1785
+ // subtype_traverse(self) does Py_VISIT(Py_TYPE(self)) when the instance is
1786
+ // a heap type and the resolved base traverse does not belong to a heap
1787
+ // type, so cycles between a class object and its (heap) metaclass are
1788
+ // visible to the collector. type_traverse alone does NOT visit ob_type:
1789
+ // it only walks tp_dict / tp_mro / tp_bases, so without this step a cycle
1790
+ // like M ->(dict attr)-> C and C ->(ob_type)-> M is invisible to GC
1791
+ // and leaks. self here is a TYPE object; its Py_TYPE is the metaclass
1792
+ // (which is a heap type when registered/created by type()). We only emit
1793
+ // the visit when the metaclass is a heap type, matching subtype_traverse's
1794
+ // condition and avoiding the trivial/static type link.
1795
+ {
1796
+ if (typ->tp_flags & Py_TPFLAGS_HEAPTYPE) {
1797
+ Py_VISIT((PyObject*)typ);
1798
+ }
1799
+ }
1800
+
1697
1801
  // Traverse PyObjects stored in AllData for this type:
1698
1802
  // - direct type attributes ::AllData::type_attr_dict[typ_id]
1699
1803
  // - attributes stored under parents for this type ::AllData::all_type_subclass_attr[parent_id][typ_id]
@@ -1760,8 +1864,8 @@ PrivateAttrType_tp_traverse(PyObject* self, visitproc visit, void* arg) noexcept
1760
1864
  static int
1761
1865
  PrivateAttrType_tp_clear(PyObject* self) noexcept
1762
1866
  {
1763
- PyTypeObject* typ = (PyTypeObject*)self;
1764
- uintptr_t typ_id = (uintptr_t)typ;
1867
+ PyTypeObject* typ = Py_TYPE(self);
1868
+ uintptr_t typ_id = (uintptr_t)self;
1765
1869
 
1766
1870
  // Re-entrancy guard: see PrivateAttrRecursionGuard above.
1767
1871
  if (!PrivateAttrRecursionGuard::try_enter((uintptr_t)self)) {
@@ -1769,14 +1873,17 @@ PrivateAttrType_tp_clear(PyObject* self) noexcept
1769
1873
  }
1770
1874
  PrivateAttrRecursionGuard guard((uintptr_t)self);
1771
1875
 
1772
- // Call the real CPython type_clear (PyType_Type.tp_clear) so the class's
1773
- // hard self-cycle (tp_dict / tp_mro / ht_module) is actually broken when the
1774
- // cyclic GC decides this class is garbage. The walker path would resolve to
1775
- // the instance-level delegating defaults (subtype_clear / PrivateAttr_tp_clear)
1776
- // that re-enter our guard and return 0, leaving the cycle intact and the
1777
- // class permanently stuck in gc.garbage.
1876
+ // Recover the original clear via get_type_need_tp_clear, skipping any
1877
+ // delegating default (subtype_clear / PrivateAttr_tp_clear / our own
1878
+ // wrapper). subtype_clear on a type object would clear the class's own
1879
+ // tp_dict (its namespace), which must never happen here; when the walker
1880
+ // finds no real clear, the AllData cleanup below still breaks the cycle by
1881
+ // clearing the type-level private attributes we stored (their PyObjectStorage
1882
+ // DECREFs release the references that keep the class's tp_dict / tp_mro /
1883
+ // ht_module alive), after which CPython's own type_clear runs on the class
1884
+ // once the collector re-visits it.
1778
1885
  {
1779
- inquiry orig = PyType_Type.tp_clear; // == type_clear
1886
+ inquiry orig = get_type_need_tp_clear(typ);
1780
1887
  if (orig) {
1781
1888
  int result = orig(self);
1782
1889
  if (result) return result;
@@ -2549,13 +2656,10 @@ static getattrofunc get_need_tp_getattro(PyTypeObject* cls) noexcept;
2549
2656
  static setattrofunc get_need_tp_setattro(PyTypeObject* cls) noexcept;
2550
2657
  static traverseproc get_need_tp_traverse(PyTypeObject* cls) noexcept;
2551
2658
  static inquiry get_need_tp_clear(PyTypeObject* cls) noexcept;
2552
- static destructor get_need_tp_finalize(PyTypeObject* cls) noexcept;
2553
- static destructor get_need_tp_del(PyTypeObject* cls) noexcept;
2554
2659
 
2555
2660
  // instance-level traverse/clear for types that use private attributes
2556
2661
  static int PrivateAttr_tp_traverse(PyObject* self, visitproc visit, void* arg) noexcept;
2557
2662
  static int PrivateAttr_tp_clear(PyObject* self) noexcept;
2558
- static void PrivateAttr_tp_del(PyObject* self) noexcept;
2559
2663
  // shared helper to clear object related data (avoid double clear)
2560
2664
  static void clear_object_related(uintptr_t id_self, uintptr_t typ_id) noexcept;
2561
2665
  // metaclass-level wrappers (if needed)
@@ -2640,38 +2744,12 @@ ensure_tp(PyTypeObject* type_instance) noexcept
2640
2744
  }
2641
2745
  {
2642
2746
  if (type_instance->tp_finalize != PrivateAttr_tp_finalize) {
2643
- if (!type_instance->tp_finalize && ::AllData::all_type_finalize.find(type_id) == ::AllData::all_type_finalize.end()) {
2644
- ::AllData::all_type_finalize[type_id] = get_need_tp_finalize(type_instance);
2747
+ if (!type_instance->tp_finalize && ::AllData::all_type_del.find(type_id) == ::AllData::all_type_del.end()) {
2748
+ ::AllData::all_type_del[type_id] = get_need_tp_finalize(type_instance);
2645
2749
  } else {
2646
- ::AllData::all_type_finalize[type_id] = type_instance->tp_finalize;
2750
+ ::AllData::all_type_del[type_id] = type_instance->tp_finalize;
2647
2751
  }
2648
2752
  type_instance->tp_finalize = PrivateAttr_tp_finalize;
2649
- } else if (::AllData::all_type_finalize.find(type_id) == ::AllData::all_type_finalize.end()) {
2650
- PyTypeObject* base = type_instance->tp_base;
2651
- uintptr_t base_id = (uintptr_t)(base);
2652
- if (::AllData::all_type_finalize.find(base_id) != ::AllData::all_type_finalize.end()) {
2653
- ::AllData::all_type_finalize[type_id] = ::AllData::all_type_finalize[base_id];
2654
- } else if (base && base->tp_finalize && base->tp_finalize != PrivateAttr_tp_finalize) {
2655
- ::AllData::all_type_finalize[type_id] = base->tp_finalize;
2656
- }
2657
- }
2658
- }
2659
- {
2660
- if (type_instance->tp_del != PrivateAttr_tp_del) {
2661
- if (!type_instance->tp_del && ::AllData::all_type_del.find(type_id) == ::AllData::all_type_del.end()) {
2662
- ::AllData::all_type_del[type_id] = get_need_tp_del(type_instance);
2663
- } else {
2664
- ::AllData::all_type_del[type_id] = type_instance->tp_del;
2665
- }
2666
- type_instance->tp_del = PrivateAttr_tp_del;
2667
- } else if (::AllData::all_type_del.find(type_id) == ::AllData::all_type_del.end()) {
2668
- PyTypeObject* base = type_instance->tp_base;
2669
- uintptr_t base_id = (uintptr_t)(base);
2670
- if (::AllData::all_type_del.find(base_id) != ::AllData::all_type_del.end()) {
2671
- ::AllData::all_type_del[type_id] = ::AllData::all_type_del[base_id];
2672
- } else if (base && base->tp_del && base->tp_del != PrivateAttr_tp_del) {
2673
- ::AllData::all_type_del[type_id] = base->tp_del;
2674
- }
2675
2753
  }
2676
2754
  }
2677
2755
  }
@@ -3032,25 +3110,6 @@ get_need_tp_setattro(PyTypeObject* cls) noexcept
3032
3110
  return NULL;
3033
3111
  }
3034
3112
 
3035
- static destructor
3036
- get_need_tp_finalize(PyTypeObject* cls) noexcept
3037
- {
3038
- PyTypeObject* base = cls->tp_base;
3039
- while (base) {
3040
- if (base->tp_finalize != PrivateAttr_tp_finalize) {
3041
- return base->tp_finalize;
3042
- } else if (::AllData::all_type_finalize.find((uintptr_t)base) != ::AllData::all_type_finalize.end()) {
3043
- if (::AllData::all_type_finalize[(uintptr_t)base]) {
3044
- return ::AllData::all_type_finalize[(uintptr_t)base];
3045
- }
3046
- base = base->tp_base;
3047
- } else {
3048
- base = base->tp_base;
3049
- }
3050
- }
3051
- return NULL;
3052
- }
3053
-
3054
3113
  static traverseproc
3055
3114
  get_need_tp_traverse(PyTypeObject* cls) noexcept
3056
3115
  {
@@ -3093,29 +3152,6 @@ get_need_tp_clear(PyTypeObject* cls) noexcept
3093
3152
  return NULL;
3094
3153
  }
3095
3154
 
3096
- static destructor
3097
- get_need_tp_del(PyTypeObject* cls) noexcept
3098
- {
3099
- PyTypeObject* base = cls->tp_base;
3100
- while (base) {
3101
- if (base->tp_del != PrivateAttr_tp_del) {
3102
- return base->tp_del;
3103
- } else if (::AllData::all_type_del.find((uintptr_t)base) != ::AllData::all_type_del.end()) {
3104
- if (::AllData::all_type_del[(uintptr_t)base]) {
3105
- return ::AllData::all_type_del[(uintptr_t)base];
3106
- }
3107
- base = base->tp_base;
3108
- } else {
3109
- base = base->tp_base;
3110
- }
3111
- }
3112
- return NULL;
3113
- }
3114
-
3115
- // NOTE: register_finalize is defined later in this file; forward-declare it
3116
- // here because get_type_need_tp_finalize() needs to compare against it.
3117
- static void register_finalize(PyObject* cls) noexcept;
3118
-
3119
3155
  // ========================================================================
3120
3156
  // get_type_need_tp_* : find the ORIGINAL slot implementation for METACLASS
3121
3157
  // slots.
@@ -3125,7 +3161,6 @@ static void register_finalize(PyObject* cls) noexcept;
3125
3161
  // wrappers:
3126
3162
  // tp_getattro -> PrivateAttrType_getattr
3127
3163
  // tp_setattro -> PrivateAttrType_setattr
3128
- // tp_finalize -> register_finalize
3129
3164
  // tp_traverse -> PrivateAttrType_tp_traverse
3130
3165
  // tp_clear -> PrivateAttrType_tp_clear
3131
3166
  //
@@ -3175,65 +3210,81 @@ get_type_need_tp_setattro(PyTypeObject* cls) noexcept
3175
3210
  return NULL;
3176
3211
  }
3177
3212
 
3178
- static destructor
3179
- get_type_need_tp_finalize(PyTypeObject* cls) noexcept
3180
- {
3181
- PyTypeObject* base = cls->tp_base;
3182
- while (base) {
3183
- if (base->tp_finalize != register_finalize) {
3184
- return base->tp_finalize;
3185
- } else if (::AllData::all_type_finalize.find((uintptr_t)base) != ::AllData::all_type_finalize.end()) {
3186
- if (::AllData::all_type_finalize[(uintptr_t)base]) {
3187
- return ::AllData::all_type_finalize[(uintptr_t)base];
3188
- }
3189
- base = base->tp_base;
3190
- } else {
3191
- base = base->tp_base;
3192
- }
3193
- }
3194
- return NULL;
3195
- }
3196
-
3197
3213
  static traverseproc
3198
3214
  get_type_need_tp_traverse(PyTypeObject* cls) noexcept
3199
3215
  {
3216
+ // cls is a type object (an instance of a metaclass). CPython only invokes
3217
+ // a real traverse on a tracked type object directly; otherwise it delegates
3218
+ // through subtype_traverse. So walk the metaclass base chain and skip every
3219
+ // delegating default (subtype_traverse, PrivateAttr_tp_traverse, our own
3220
+ // wrapper) AND NULL slots, returning the nearest real implementation.
3221
+ //
3222
+ // For a plain metaclass chain the walk is
3223
+ // cls -> ... -> PrivateAttrBase -> object -> (tp_base == NULL)
3224
+ // and object->tp_traverse is NULL, so the loop keeps walking to the end and
3225
+ // falls through to the PyType_Type.tp_traverse fallback below: for a type
3226
+ // object the ONLY correct real traverse is CPython's own type_traverse,
3227
+ // which is what makes the class's hard self-cycle (tp_dict / tp_mro /
3228
+ // tp_bases) visible to the cyclic GC. Without that fallback the class
3229
+ // becomes uncollectable. For a REGISTERED metaclass chain the saved-map
3230
+ // branch may find a real implementation before the fallback is reached.
3200
3231
  PyTypeObject* base = cls->tp_base;
3201
3232
  while (base) {
3202
- if (base->tp_traverse != PrivateAttrType_tp_traverse
3203
- && base->tp_traverse != ::AllData::captured_subtype_traverse) {
3204
- return base->tp_traverse;
3205
- } else if (::AllData::all_type_traverse.find((uintptr_t)base) != ::AllData::all_type_traverse.end()) {
3206
- traverseproc saved = ::AllData::all_type_traverse[(uintptr_t)base];
3207
- if (saved && saved != ::AllData::captured_subtype_traverse) {
3233
+ traverseproc slot = base->tp_traverse;
3234
+ if (slot
3235
+ && slot != PrivateAttrType_tp_traverse
3236
+ && slot != ::AllData::captured_subtype_traverse
3237
+ && slot != PrivateAttr_tp_traverse) {
3238
+ return slot;
3239
+ }
3240
+ auto it = ::AllData::all_type_traverse.find((uintptr_t)base);
3241
+ if (it != ::AllData::all_type_traverse.end()) {
3242
+ traverseproc saved = it->second;
3243
+ if (saved
3244
+ && saved != ::AllData::captured_subtype_traverse
3245
+ && saved != PrivateAttr_tp_traverse
3246
+ && saved != PrivateAttrType_tp_traverse) {
3208
3247
  return saved;
3209
3248
  }
3210
- base = base->tp_base;
3211
- } else {
3212
- base = base->tp_base;
3213
3249
  }
3250
+ base = base->tp_base;
3214
3251
  }
3215
- return NULL;
3252
+ // Fallback to CPython's real type_traverse (PyType_Type.tp_traverse). A
3253
+ // NULL slot is never a real implementation, so exhausting the chain here
3254
+ // means no custom traverse exists and the only correct original for a type
3255
+ // object is type_traverse itself.
3256
+ return PyType_Type.tp_traverse;
3216
3257
  }
3217
3258
 
3218
3259
  static inquiry
3219
3260
  get_type_need_tp_clear(PyTypeObject* cls) noexcept
3220
3261
  {
3262
+ // Same as get_type_need_tp_traverse: skip every delegating default
3263
+ // (subtype_clear, PrivateAttr_tp_clear, our own wrapper) AND NULL slots,
3264
+ // then fall back to CPython's real type_clear (PyType_Type.tp_clear) so a
3265
+ // plain metaclass chain still breaks the class's hard self-cycle.
3221
3266
  PyTypeObject* base = cls->tp_base;
3222
3267
  while (base) {
3223
- if (base->tp_clear != PrivateAttrType_tp_clear
3224
- && base->tp_clear != ::AllData::captured_subtype_clear) {
3225
- return base->tp_clear;
3226
- } else if (::AllData::all_type_clear.find((uintptr_t)base) != ::AllData::all_type_clear.end()) {
3227
- inquiry saved = ::AllData::all_type_clear[(uintptr_t)base];
3228
- if (saved && saved != ::AllData::captured_subtype_clear) {
3268
+ inquiry slot = base->tp_clear;
3269
+ if (slot
3270
+ && slot != PrivateAttrType_tp_clear
3271
+ && slot != ::AllData::captured_subtype_clear
3272
+ && slot != PrivateAttr_tp_clear) {
3273
+ return slot;
3274
+ }
3275
+ auto it = ::AllData::all_type_clear.find((uintptr_t)base);
3276
+ if (it != ::AllData::all_type_clear.end()) {
3277
+ inquiry saved = it->second;
3278
+ if (saved
3279
+ && saved != ::AllData::captured_subtype_clear
3280
+ && saved != PrivateAttr_tp_clear
3281
+ && saved != PrivateAttrType_tp_clear) {
3229
3282
  return saved;
3230
3283
  }
3231
- base = base->tp_base;
3232
- } else {
3233
- base = base->tp_base;
3234
3284
  }
3285
+ base = base->tp_base;
3235
3286
  }
3236
- return NULL;
3287
+ return PyType_Type.tp_clear;
3237
3288
  }
3238
3289
 
3239
3290
  static int
@@ -3328,12 +3379,6 @@ PrivateAttrType_finalize(PyObject* cls) noexcept
3328
3379
  if (::AllData::all_type_setattro.find(typ_id) != ::AllData::all_type_setattro.end()) {
3329
3380
  ::AllData::all_type_setattro.erase(typ_id);
3330
3381
  }
3331
- if (::AllData::all_type_finalize.find(typ_id) != ::AllData::all_type_finalize.end()) {
3332
- ::AllData::all_type_finalize.erase(typ_id);
3333
- }
3334
- if (::AllData::all_type_del.find(typ_id) != ::AllData::all_type_del.end()) {
3335
- ::AllData::all_type_del.erase(typ_id);
3336
- }
3337
3382
  ::AllData::all_type_mutex.erase(typ_id);
3338
3383
  // Clear the per-type buckets of the object-attr tables (created in
3339
3384
  // PrivateAttrType_postprocess / PrivateAttr_object_init_private_dict);
@@ -3554,21 +3599,6 @@ postprocess_for_PrivateAttr(PyObject* /*self*/, PyObject* args) noexcept
3554
3599
  Py_RETURN_NONE;
3555
3600
  }
3556
3601
 
3557
- static void
3558
- register_finalize(PyObject* cls) noexcept
3559
- {
3560
- Py_ssize_t original_ref = Py_REFCNT(cls);
3561
- PyTypeObject* base = Py_TYPE(cls)->tp_base;
3562
- while (base && base->tp_finalize == register_finalize) {
3563
- base = base->tp_base;
3564
- }
3565
- if (base && base->tp_finalize) base->tp_finalize(cls);
3566
- if (Py_REFCNT(cls) != original_ref) {
3567
- return;
3568
- }
3569
- PrivateAttrType_finalize(cls);
3570
- }
3571
-
3572
3602
  #define METACLASS_WEAKREF_CALLBACK_REMOVE_ITEM(name) do {\
3573
3603
  if (::AllData::all_type_##name.find(id) != ::AllData::all_type_##name.end()) {\
3574
3604
  ::AllData::all_type_##name.erase(id);\
@@ -3584,6 +3614,13 @@ metaclass_weakref_callback(PyObject* self, PyObject* /*weakref*/) noexcept
3584
3614
  }
3585
3615
  uintptr_t id = (uintptr_t)pointer;
3586
3616
 
3617
+ // Clean up the class-level AllData owned by this registered metaclass
3618
+ // (type_attr_dict / all_type_subclass_attr / object buckets / caches).
3619
+ // This replaces the old tp_finalize hook (register_finalize) which has
3620
+ // been removed: a weakref callback fires on every death path (refcount and
3621
+ // cyclic GC) without making the metaclass "finalizable".
3622
+ PrivateAttrType_finalize((PyObject*)id);
3623
+
3587
3624
  std::unique_lock lock(::AllData::all_register_new_metaclass_mutex);
3588
3625
 
3589
3626
  auto it = ::AllData::all_register_type_weak_ref.find(id);
@@ -3595,7 +3632,6 @@ metaclass_weakref_callback(PyObject* self, PyObject* /*weakref*/) noexcept
3595
3632
  METACLASS_WEAKREF_CALLBACK_REMOVE_ITEM(setattro);
3596
3633
  METACLASS_WEAKREF_CALLBACK_REMOVE_ITEM(traverse);
3597
3634
  METACLASS_WEAKREF_CALLBACK_REMOVE_ITEM(clear);
3598
- METACLASS_WEAKREF_CALLBACK_REMOVE_ITEM(finalize);
3599
3635
  METACLASS_WEAKREF_CALLBACK_REMOVE_ITEM(del);
3600
3636
 
3601
3637
  Py_RETURN_NONE;
@@ -3688,48 +3724,37 @@ register_metaclass_head(PyObject* metaclass) noexcept
3688
3724
  }
3689
3725
  ::AllData::all_register_type_weak_ref[id] = ref;
3690
3726
  PyTypeObject* mt = (PyTypeObject*)metaclass;
3727
+ // Save the ORIGINAL slots before wrapping. Always resolve through the
3728
+ // get_type_need_tp_* walkers so a delegating default (CPython's
3729
+ // subtype_traverse/subtype_clear, which a plain heap metaclass inherits)
3730
+ // is never stored as the "original". The walkers skip those defaults and
3731
+ // fall back to PyType_Type.tp_* (the real implementation for a type
3732
+ // object). If an entry already exists for this id, keep it (idempotent).
3691
3733
  // save and replace tp_getattro
3692
3734
  if (mt->tp_getattro != PrivateAttrType_getattr) {
3693
- if (!mt->tp_getattro && ::AllData::all_type_getattro.find(id) == ::AllData::all_type_getattro.end()) {
3735
+ if (::AllData::all_type_getattro.find(id) == ::AllData::all_type_getattro.end()) {
3694
3736
  ::AllData::all_type_getattro[id] = get_type_need_tp_getattro(mt);
3695
- } else {
3696
- ::AllData::all_type_getattro[id] = mt->tp_getattro;
3697
3737
  }
3698
3738
  mt->tp_getattro = PrivateAttrType_getattr;
3699
3739
  }
3700
3740
  // save and replace tp_setattro
3701
3741
  if (mt->tp_setattro != PrivateAttrType_setattr) {
3702
- if (!mt->tp_setattro && ::AllData::all_type_setattro.find(id) == ::AllData::all_type_setattro.end()) {
3742
+ if (::AllData::all_type_setattro.find(id) == ::AllData::all_type_setattro.end()) {
3703
3743
  ::AllData::all_type_setattro[id] = get_type_need_tp_setattro(mt);
3704
- } else {
3705
- ::AllData::all_type_setattro[id] = mt->tp_setattro;
3706
3744
  }
3707
3745
  mt->tp_setattro = PrivateAttrType_setattr;
3708
3746
  }
3709
- // save and replace tp_finalize
3710
- if (mt->tp_finalize != register_finalize) {
3711
- if (!mt->tp_finalize && ::AllData::all_type_finalize.find(id) == ::AllData::all_type_finalize.end()) {
3712
- ::AllData::all_type_finalize[id] = get_type_need_tp_finalize(mt);
3713
- } else {
3714
- ::AllData::all_type_finalize[id] = mt->tp_finalize;
3715
- }
3716
- mt->tp_finalize = register_finalize;
3717
- }
3718
3747
  // save and replace tp_traverse
3719
3748
  if (mt->tp_traverse != PrivateAttrType_tp_traverse) {
3720
- if (!mt->tp_traverse && ::AllData::all_type_traverse.find(id) == ::AllData::all_type_traverse.end()) {
3749
+ if (::AllData::all_type_traverse.find(id) == ::AllData::all_type_traverse.end()) {
3721
3750
  ::AllData::all_type_traverse[id] = get_type_need_tp_traverse(mt);
3722
- } else {
3723
- ::AllData::all_type_traverse[id] = mt->tp_traverse;
3724
3751
  }
3725
3752
  mt->tp_traverse = PrivateAttrType_tp_traverse;
3726
3753
  }
3727
3754
  // save and replace tp_clear
3728
3755
  if (mt->tp_clear != PrivateAttrType_tp_clear) {
3729
- if (!mt->tp_clear && ::AllData::all_type_clear.find(id) == ::AllData::all_type_clear.end()) {
3756
+ if (::AllData::all_type_clear.find(id) == ::AllData::all_type_clear.end()) {
3730
3757
  ::AllData::all_type_clear[id] = get_type_need_tp_clear(mt);
3731
- } else {
3732
- ::AllData::all_type_clear[id] = mt->tp_clear;
3733
3758
  }
3734
3759
  mt->tp_clear = PrivateAttrType_tp_clear;
3735
3760
  }
@@ -3778,43 +3803,31 @@ ensure_metaclass_tp(PyObject* /*self*/, PyObject* metaclass) noexcept
3778
3803
  uintptr_t id = (uintptr_t)metaclass;
3779
3804
  if (::AllData::all_register_type_weak_ref.find(id) != ::AllData::all_register_type_weak_ref.end()) {
3780
3805
  PyTypeObject* mt = (PyTypeObject*)metaclass;
3806
+ // Same as register_metaclass_head: always resolve the ORIGINAL slots
3807
+ // through the get_type_need_tp_* walkers (never store a delegating
3808
+ // default such as subtype_traverse/subtype_clear). Idempotent when an
3809
+ // entry already exists.
3781
3810
  if (mt->tp_getattro != PrivateAttrType_getattr) {
3782
- if (!mt->tp_getattro && ::AllData::all_type_getattro.find(id) == ::AllData::all_type_getattro.end()) {
3811
+ if (::AllData::all_type_getattro.find(id) == ::AllData::all_type_getattro.end()) {
3783
3812
  ::AllData::all_type_getattro[id] = get_type_need_tp_getattro(mt);
3784
- } else {
3785
- ::AllData::all_type_getattro[id] = mt->tp_getattro;
3786
3813
  }
3787
3814
  mt->tp_getattro = PrivateAttrType_getattr;
3788
3815
  }
3789
3816
  if (mt->tp_setattro != PrivateAttrType_setattr) {
3790
- if (!mt->tp_setattro && ::AllData::all_type_setattro.find(id) == ::AllData::all_type_setattro.end()) {
3817
+ if (::AllData::all_type_setattro.find(id) == ::AllData::all_type_setattro.end()) {
3791
3818
  ::AllData::all_type_setattro[id] = get_type_need_tp_setattro(mt);
3792
- } else {
3793
- ::AllData::all_type_setattro[id] = mt->tp_setattro;
3794
3819
  }
3795
3820
  mt->tp_setattro = PrivateAttrType_setattr;
3796
3821
  }
3797
- if (mt->tp_finalize != register_finalize) {
3798
- if (!mt->tp_finalize && ::AllData::all_type_finalize.find(id) == ::AllData::all_type_finalize.end()) {
3799
- ::AllData::all_type_finalize[id] = get_type_need_tp_finalize(mt);
3800
- } else {
3801
- ::AllData::all_type_finalize[id] = mt->tp_finalize;
3802
- }
3803
- mt->tp_finalize = register_finalize;
3804
- }
3805
3822
  if (mt->tp_traverse != PrivateAttrType_tp_traverse) {
3806
- if (!mt->tp_traverse && ::AllData::all_type_traverse.find(id) == ::AllData::all_type_traverse.end()) {
3823
+ if (::AllData::all_type_traverse.find(id) == ::AllData::all_type_traverse.end()) {
3807
3824
  ::AllData::all_type_traverse[id] = get_type_need_tp_traverse(mt);
3808
- } else {
3809
- ::AllData::all_type_traverse[id] = mt->tp_traverse;
3810
3825
  }
3811
3826
  mt->tp_traverse = PrivateAttrType_tp_traverse;
3812
3827
  }
3813
3828
  if (mt->tp_clear != PrivateAttrType_tp_clear) {
3814
- if (!mt->tp_clear && ::AllData::all_type_clear.find(id) == ::AllData::all_type_clear.end()) {
3829
+ if (::AllData::all_type_clear.find(id) == ::AllData::all_type_clear.end()) {
3815
3830
  ::AllData::all_type_clear[id] = get_type_need_tp_clear(mt);
3816
- } else {
3817
- ::AllData::all_type_clear[id] = mt->tp_clear;
3818
3831
  }
3819
3832
  mt->tp_clear = PrivateAttrType_tp_clear;
3820
3833
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: private_attribute_cpp
3
- Version: 2.1.5
3
+ Version: 2.1.7
4
4
  Summary: A Python package that provides a way to define private attributes in C++ implementation.
5
5
  Home-page: https://github.com/Locked-chess-official/private_attribute_cpp
6
6
  Author: HuangHaoHua
@@ -18,7 +18,7 @@ readme = open('README.md').read()
18
18
 
19
19
  setup(
20
20
  name='private_attribute_cpp',
21
- version='2.1.5',
21
+ version='2.1.7',
22
22
  author="HuangHaoHua",
23
23
  author_email="13140752715@example.com",
24
24
  description='A Python package that provides a way to define private attributes in C++ implementation.',