private-attribute-cpp 2.1.4__tar.gz → 2.1.6__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.4/private_attribute_cpp.egg-info → private_attribute_cpp-2.1.6}/PKG-INFO +1 -1
  2. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.6}/private_attribute.cpp +377 -322
  3. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.6/private_attribute_cpp.egg-info}/PKG-INFO +1 -1
  4. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.6}/private_attribute_cpp.egg-info/SOURCES.txt +1 -0
  5. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.6}/setup.py +1 -1
  6. private_attribute_cpp-2.1.6/tests/test_gc_collect.py +145 -0
  7. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.6}/LICENSE +0 -0
  8. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.6}/MANIFEST.in +0 -0
  9. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.6}/README.md +0 -0
  10. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.6}/picosha2.h +0 -0
  11. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.6}/private_attribute.pyi +0 -0
  12. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.6}/private_attribute_cpp.egg-info/dependency_links.txt +0 -0
  13. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.6}/private_attribute_cpp.egg-info/not-zip-safe +0 -0
  14. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.6}/private_attribute_cpp.egg-info/top_level.txt +0 -0
  15. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.6}/setup.cfg +0 -0
  16. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.6}/tests/test_attr_classify_owner.py +0 -0
  17. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.6}/tests/test_class_private_attrs.py +0 -0
  18. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.6}/tests/test_private_abc.py +0 -0
  19. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.6}/tests/test_private_attrs.py +0 -0
  20. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.6}/tests/test_private_func.py +0 -0
  21. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.6}/tests/test_private_inheritance.py +0 -0
  22. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.6}/tests/test_private_method.py +0 -0
  23. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.6}/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.4
3
+ Version: 2.1.6
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
@@ -203,6 +203,7 @@ namespace {
203
203
  };
204
204
  static std::unordered_map<uintptr_t, std::unordered_map<uintptr_t, std::shared_ptr<std::shared_mutex>>>
205
205
  all_object_mutex, all_type_subclass_mutex;
206
+ static std::mutex object_creatmutex_mutex, type_createmutex_mutex, type_parenttype_createmutex_mutex;
206
207
  static std::unordered_map<uintptr_t, std::vector<uintptr_t>> all_type_parent_id;
207
208
  // all type tp_getattro map
208
209
  static std::unordered_map<uintptr_t, getattrofunc> all_type_getattro;
@@ -219,15 +220,63 @@ namespace {
219
220
  // defaults as "not a real original" and keep walking upward.
220
221
  static traverseproc captured_subtype_traverse = NULL;
221
222
  static inquiry captured_subtype_clear = NULL;
222
- // all type tp_finalizer map
223
- static std::unordered_map<uintptr_t, destructor> all_type_finalize;
223
+ static std::unordered_map<uintptr_t, destructor> all_type_del;
224
224
 
225
225
  static std::shared_mutex all_register_new_metaclass_mutex;
226
226
  static std::unordered_map<uintptr_t, PyObjectStorage> all_register_type_weak_ref;
227
+ static std::vector<PyObjectStorage> store_module_self;
227
228
  };
228
229
  };
229
230
 
230
- static void
231
+ static inline void
232
+ object_create_mutex(uintptr_t final_id, uintptr_t obj_id) noexcept
233
+ {
234
+ // get object_creatmutex_mutex
235
+ std::lock_guard<std::mutex> lock(AllData::object_creatmutex_mutex);
236
+ if (::AllData::all_object_attr[final_id].find(obj_id) == ::AllData::all_object_attr[final_id].end()) {
237
+ ::AllData::all_object_attr[final_id][obj_id] = {};
238
+ }
239
+ if (::AllData::all_object_mutex.find(final_id) == ::AllData::all_object_mutex.end()) {
240
+ ::AllData::all_object_mutex[final_id] = {};
241
+ }
242
+ if (::AllData::all_object_mutex[final_id].find(obj_id) == ::AllData::all_object_mutex[final_id].end()) {
243
+ std::shared_ptr<std::shared_mutex> lock(new std::shared_mutex());
244
+ ::AllData::all_object_mutex[final_id][obj_id] = lock;
245
+ }
246
+ }
247
+
248
+ static inline void
249
+ type_create_mutex(uintptr_t typ_id) noexcept {
250
+ std::lock_guard<std::mutex> lock(AllData::type_createmutex_mutex);
251
+ if (::AllData::type_attr_dict.find(typ_id) == ::AllData::type_attr_dict.end()) {
252
+ ::AllData::type_attr_dict[typ_id] = {};
253
+ }
254
+ if (::AllData::all_type_mutex.find(typ_id) == ::AllData::all_type_mutex.end()) {
255
+ std::shared_ptr<std::shared_mutex> lock(new std::shared_mutex());
256
+ ::AllData::all_type_mutex[typ_id] = lock;
257
+ }
258
+ }
259
+
260
+ static inline void
261
+ type_parenttype_create_mutex(uintptr_t final_id, uintptr_t typ_id) noexcept
262
+ {
263
+ std::lock_guard<std::mutex> lock(AllData::type_parenttype_createmutex_mutex);
264
+ if (::AllData::all_type_subclass_attr.find(final_id) == ::AllData::all_type_subclass_attr.end()) {
265
+ ::AllData::all_type_subclass_attr[final_id] = {};
266
+ }
267
+ if (::AllData::all_type_subclass_attr[final_id].find(typ_id) == ::AllData::all_type_subclass_attr[final_id].end()) {
268
+ ::AllData::all_type_subclass_attr[final_id][typ_id] = {};
269
+ }
270
+ if (::AllData::all_type_subclass_mutex.find(final_id) == ::AllData::all_type_subclass_mutex.end()) {
271
+ ::AllData::all_type_subclass_mutex[final_id] = {};
272
+ }
273
+ if (::AllData::all_type_subclass_mutex[final_id].find(typ_id) == ::AllData::all_type_subclass_mutex[final_id].end()) {
274
+ std::shared_ptr<std::shared_mutex> lock(new std::shared_mutex());
275
+ ::AllData::all_type_subclass_mutex[final_id][typ_id] = lock;
276
+ }
277
+ }
278
+
279
+ static inline void
231
280
  clean_all_storages() noexcept
232
281
  {
233
282
  AllData::cache.clear();
@@ -247,8 +296,9 @@ clean_all_storages() noexcept
247
296
  AllData::all_type_setattro.clear();
248
297
  AllData::all_type_traverse.clear();
249
298
  AllData::all_type_clear.clear();
250
- AllData::all_type_finalize.clear();
299
+ AllData::all_type_del.clear();
251
300
  AllData::all_register_type_weak_ref.clear();
301
+ AllData::store_module_self.clear();
252
302
  }
253
303
 
254
304
  struct FinalObject
@@ -563,16 +613,7 @@ id_getattr(uintptr_t final_id, const std::string& attr_name, PyObject* obj, PyOb
563
613
  PyErr_SetString(PyExc_TypeError, "type not found");
564
614
  return NULL;
565
615
  }
566
- if (::AllData::all_object_attr[final_id].find(obj_id) == ::AllData::all_object_attr[final_id].end()) {
567
- ::AllData::all_object_attr[final_id][obj_id] = {};
568
- }
569
- if (::AllData::all_object_mutex.find(final_id) == ::AllData::all_object_mutex.end()) {
570
- ::AllData::all_object_mutex[final_id] = {};
571
- }
572
- if (::AllData::all_object_mutex[final_id].find(obj_id) == ::AllData::all_object_mutex[final_id].end()) {
573
- std::shared_ptr<std::shared_mutex> lock(new std::shared_mutex());
574
- ::AllData::all_object_mutex[final_id][obj_id] = lock;
575
- }
616
+ object_create_mutex(final_id, obj_id);
576
617
  PyObject* result = NULL;
577
618
  if (final_object.status != -1) {
578
619
  result = final_object.result;
@@ -699,16 +740,7 @@ id_setattr(uintptr_t final_id, const std::string& attr_name, PyObject* obj, PyOb
699
740
  PyErr_SetString(PyExc_TypeError, "type not found");
700
741
  return -1;
701
742
  }
702
- if (::AllData::all_object_attr[final_id].find(obj_id) == ::AllData::all_object_attr[final_id].end()) {
703
- ::AllData::all_object_attr[final_id][obj_id] = {};
704
- }
705
- if (::AllData::all_object_mutex.find(final_id) == ::AllData::all_object_mutex.end()) {
706
- ::AllData::all_object_mutex[final_id] = {};
707
- }
708
- if (::AllData::all_object_mutex[final_id].find(obj_id) == ::AllData::all_object_mutex[final_id].end()) {
709
- std::shared_ptr<std::shared_mutex> lock(new std::shared_mutex());
710
- ::AllData::all_object_mutex[final_id][obj_id] = lock;
711
- }
743
+ object_create_mutex(final_id, obj_id);
712
744
  // first: call __set__ method
713
745
  PyObject* result = NULL;
714
746
  if (final_object.status != -1) {
@@ -764,13 +796,7 @@ type_setattr(PyObject* typ, const std::string& attr_name, PyObject* value) noexc
764
796
  return -1;
765
797
  }
766
798
  if (final_id == typ_id) {
767
- if (::AllData::type_attr_dict.find(typ_id) == ::AllData::type_attr_dict.end()) {
768
- ::AllData::type_attr_dict[typ_id] = {};
769
- }
770
- if (::AllData::all_type_mutex.find(typ_id) == ::AllData::all_type_mutex.end()) {
771
- std::shared_ptr<std::shared_mutex> lock(new std::shared_mutex());
772
- ::AllData::all_type_mutex[typ_id] = lock;
773
- }
799
+ type_create_mutex(typ_id);
774
800
  {
775
801
  std::unique_lock<std::shared_mutex> lock(*::AllData::all_type_mutex[typ_id]);
776
802
  ::AllData::type_attr_dict[typ_id][final_key] = value;
@@ -779,19 +805,7 @@ type_setattr(PyObject* typ, const std::string& attr_name, PyObject* value) noexc
779
805
  } else {
780
806
  // The accessed class is a subclass that stores its own value for a
781
807
  // parent's private name separately: all_type_subclass_attr[final_id][typ_id].
782
- if (::AllData::all_type_subclass_attr.find(final_id) == ::AllData::all_type_subclass_attr.end()) {
783
- ::AllData::all_type_subclass_attr[final_id] = {};
784
- }
785
- if (::AllData::all_type_subclass_attr[final_id].find(typ_id) == ::AllData::all_type_subclass_attr[final_id].end()) {
786
- ::AllData::all_type_subclass_attr[final_id][typ_id] = {};
787
- }
788
- if (::AllData::all_type_subclass_mutex.find(final_id) == ::AllData::all_type_subclass_mutex.end()) {
789
- ::AllData::all_type_subclass_mutex[final_id] = {};
790
- }
791
- if (::AllData::all_type_subclass_mutex[final_id].find(typ_id) == ::AllData::all_type_subclass_mutex[final_id].end()) {
792
- std::shared_ptr<std::shared_mutex> lock(new std::shared_mutex());
793
- ::AllData::all_type_subclass_mutex[final_id][typ_id] = lock;
794
- }
808
+ type_parenttype_create_mutex(final_id, typ_id);
795
809
  {
796
810
  std::unique_lock<std::shared_mutex> lock(*::AllData::all_type_subclass_mutex[final_id][typ_id]);
797
811
  ::AllData::all_type_subclass_attr[final_id][typ_id][final_key] = value;
@@ -830,16 +844,7 @@ id_delattr(uintptr_t final_id, const std::string& attr_name, PyObject* obj, PyOb
830
844
  PyErr_SetString(PyExc_TypeError, "type not found");
831
845
  return -1;
832
846
  }
833
- if (::AllData::all_object_attr[final_id].find(obj_id) == ::AllData::all_object_attr[final_id].end()) {
834
- ::AllData::all_object_attr[final_id][obj_id] = {};
835
- }
836
- if (::AllData::all_object_mutex.find(final_id) == ::AllData::all_object_mutex.end()) {
837
- ::AllData::all_object_mutex[final_id] = {};
838
- }
839
- if (::AllData::all_object_mutex[final_id].find(obj_id) == ::AllData::all_object_mutex[final_id].end()) {
840
- std::shared_ptr<std::shared_mutex> lock(new std::shared_mutex());
841
- ::AllData::all_object_mutex[final_id][obj_id] = lock;
842
- }
847
+ object_create_mutex(final_id, obj_id);
843
848
  // first: find attribute on type to find "__delete__"
844
849
  PyObject* result = NULL;
845
850
  if (final_object.status == 0) {
@@ -897,13 +902,7 @@ type_delattr(PyObject* typ, const std::string& attr_name) noexcept
897
902
  return -1;
898
903
  }
899
904
  if (typ_id == final_id) {
900
- if (::AllData::type_attr_dict.find(typ_id) == ::AllData::type_attr_dict.end()) {
901
- ::AllData::type_attr_dict[typ_id] = {};
902
- }
903
- if (::AllData::all_type_mutex.find(typ_id) == ::AllData::all_type_mutex.end()) {
904
- std::shared_ptr<std::shared_mutex> lock(new std::shared_mutex());
905
- ::AllData::all_type_mutex[typ_id] = lock;
906
- }
905
+ type_create_mutex(typ_id);
907
906
  std::unique_lock<std::shared_mutex> lock(*::AllData::all_type_mutex[typ_id]);
908
907
  if (::AllData::type_attr_dict[typ_id].find(final_key) == ::AllData::type_attr_dict[typ_id].end()) {
909
908
  const char* type_name = get_name_from_tp_name((PyTypeObject*)typ);
@@ -915,19 +914,7 @@ type_delattr(PyObject* typ, const std::string& attr_name) noexcept
915
914
  }
916
915
  ::AllData::type_attr_dict[typ_id].erase(final_key);
917
916
  } else {
918
- if (::AllData::all_type_subclass_attr.find(final_id) == ::AllData::all_type_subclass_attr.end()) {
919
- ::AllData::all_type_subclass_attr[final_id] = {};
920
- }
921
- if (::AllData::all_type_subclass_attr[final_id].find(typ_id) == ::AllData::all_type_subclass_attr[final_id].end()) {
922
- ::AllData::all_type_subclass_attr[final_id][typ_id] = {};
923
- }
924
- if (::AllData::all_type_subclass_mutex.find(final_id) == ::AllData::all_type_subclass_mutex.end()) {
925
- ::AllData::all_type_subclass_mutex[final_id] = {};
926
- }
927
- if (::AllData::all_type_subclass_mutex[final_id].find(typ_id) == ::AllData::all_type_subclass_mutex[final_id].end()) {
928
- std::shared_ptr<std::shared_mutex> lock(new std::shared_mutex());
929
- ::AllData::all_type_subclass_mutex[final_id][typ_id] = lock;
930
- }
917
+ type_parenttype_create_mutex(final_id, typ_id);
931
918
  std::unique_lock<std::shared_mutex> lock(*::AllData::all_type_subclass_mutex[final_id][typ_id]);
932
919
  if (::AllData::all_type_subclass_attr[final_id][typ_id].find(final_key) == ::AllData::all_type_subclass_attr[final_id][typ_id].end()) {
933
920
  const char* type_name = get_name_from_tp_name((PyTypeObject*)typ);
@@ -1364,6 +1351,7 @@ typedef struct {
1364
1351
  static void PrivateAttr_object_init_private_dict(uintptr_t obj_id, uintptr_t type_id) noexcept;
1365
1352
  static int PrivateAttr_tp_setattro(PyObject* self, PyObject* name, PyObject* value) noexcept;
1366
1353
  static void PrivateAttr_tp_finalize(PyObject* self) noexcept;
1354
+ static void clear_object_related(uintptr_t id_self, uintptr_t typ_id) noexcept;
1367
1355
 
1368
1356
  static PyObject*
1369
1357
  PrivateAttr_tp_getattro(PyObject* self, PyObject* name) noexcept
@@ -1441,60 +1429,6 @@ PrivateAttr_tp_setattro(PyObject* self, PyObject* name, PyObject* value) noexcep
1441
1429
  return PyObject_GenericSetAttr(self, name, value);
1442
1430
  }
1443
1431
 
1444
- static void
1445
- PrivateAttr_tp_finalize(PyObject* self) noexcept
1446
- {
1447
- uintptr_t id_self = (uintptr_t)self;
1448
- PyTypeObject* typ = Py_TYPE(self);
1449
- uintptr_t typ_id = (uintptr_t)typ;
1450
- Py_ssize_t original_ref = Py_REFCNT(self);
1451
- if (::AllData::all_type_finalize.find(typ_id) != ::AllData::all_type_finalize.end()){
1452
- if (::AllData::all_type_finalize[typ_id]) {
1453
- ::AllData::all_type_finalize[typ_id](self);
1454
- ensure_tp(typ);
1455
- }
1456
- }
1457
- if (original_ref != Py_REFCNT(self)) {
1458
- return;
1459
- }
1460
- std::vector<uintptr_t> parent_ids;
1461
- if (::AllData::all_type_parent_id.find(typ_id) != ::AllData::all_type_parent_id.end()){
1462
- parent_ids = ::AllData::all_type_parent_id[typ_id];
1463
- }
1464
-
1465
- {
1466
- // first: clear ::AllData::all_object_attr and ::AllData::all_object_mutex on this typ_id
1467
- if (::AllData::all_object_attr.find(typ_id) != ::AllData::all_object_attr.end()){
1468
- auto& all_object_attr = ::AllData::all_object_attr[typ_id];
1469
- if (all_object_attr.find(id_self) != all_object_attr.end()){
1470
- all_object_attr.erase(id_self);
1471
- }
1472
- }
1473
- if (::AllData::all_object_mutex.find(typ_id) != ::AllData::all_object_mutex.end()){
1474
- auto& all_object_mutex = ::AllData::all_object_mutex[typ_id];
1475
- if (all_object_mutex.find(id_self) != all_object_mutex.end()){
1476
- all_object_mutex.erase(id_self);
1477
- }
1478
- }
1479
- // second: clear the above in parent types
1480
- for (auto& parent_id : parent_ids){
1481
- if (::AllData::all_object_attr.find(parent_id) != ::AllData::all_object_attr.end()){
1482
- auto& all_object_attr = ::AllData::all_object_attr[parent_id];
1483
- if (all_object_attr.find(id_self) != all_object_attr.end()){
1484
- all_object_attr.erase(id_self);
1485
- }
1486
- }
1487
- if (::AllData::all_object_mutex.find(parent_id) != ::AllData::all_object_mutex.end()){
1488
- auto& all_object_mutex = ::AllData::all_object_mutex[parent_id];
1489
- if (all_object_mutex.find(id_self) != all_object_mutex.end()){
1490
- all_object_mutex.erase(id_self);
1491
- }
1492
- }
1493
- }
1494
- clear_obj(id_self);
1495
- }
1496
- }
1497
-
1498
1432
  // shared helper for clearing object-related data (used by tp_clear and tp_finalize)
1499
1433
  static void
1500
1434
  clear_object_related(uintptr_t id_self, uintptr_t typ_id) noexcept
@@ -1606,6 +1540,61 @@ PrivateAttr_tp_traverse(PyObject* self, visitproc visit, void* arg) noexcept
1606
1540
  }
1607
1541
  }
1608
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
+
1609
1598
  // traverse per-object private attributes for this type and its parents
1610
1599
  std::vector<uintptr_t> parent_ids;
1611
1600
  if (::AllData::all_type_parent_id.find(typ_id) != ::AllData::all_type_parent_id.end()){
@@ -1656,11 +1645,99 @@ PrivateAttr_tp_clear(PyObject* self) noexcept
1656
1645
  if (orig) orig(self);
1657
1646
  }
1658
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
+
1659
1670
  // clear our per-object data
1660
1671
  clear_object_related(id_self, typ_id);
1661
1672
  return 0;
1662
1673
  }
1663
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
+
1664
1741
  // get_type_need_tp_* : the metaclass counterparts of get_need_tp_*.
1665
1742
  // get_need_tp_* -> find the original slots of *types created by* the
1666
1743
  // metaclasses (wrapped with the instance-level
@@ -1668,13 +1745,12 @@ PrivateAttr_tp_clear(PyObject* self) noexcept
1668
1745
  // get_type_need_tp_* -> find the original slots of the *metaclasses*
1669
1746
  // themselves (wrapped with the metaclass-level
1670
1747
  // PrivateAttrType_getattr / PrivateAttrType_setattr /
1671
- // register_finalize / PrivateAttrType_tp_traverse /
1748
+ // PrivateAttrType_tp_traverse /
1672
1749
  // PrivateAttrType_tp_clear).
1673
1750
  static getattrofunc get_type_need_tp_getattro(PyTypeObject* cls) noexcept;
1674
1751
  static setattrofunc get_type_need_tp_setattro(PyTypeObject* cls) noexcept;
1675
1752
  static traverseproc get_type_need_tp_traverse(PyTypeObject* cls) noexcept;
1676
1753
  static inquiry get_type_need_tp_clear(PyTypeObject* cls) noexcept;
1677
- static destructor get_type_need_tp_finalize(PyTypeObject* cls) noexcept;
1678
1754
 
1679
1755
  // metaclass-level (type object) traverse/clear: call original if any, and handle AllData-stored type attributes
1680
1756
  static int
@@ -1689,14 +1765,15 @@ PrivateAttrType_tp_traverse(PyObject* self, visitproc visit, void* arg) noexcept
1689
1765
  }
1690
1766
  PrivateAttrRecursionGuard guard((uintptr_t)self);
1691
1767
 
1692
- // Call saved/original traverse if exists
1693
- if (::AllData::all_type_traverse.find(typ_id) != ::AllData::all_type_traverse.end()) {
1694
- traverseproc orig = ::AllData::all_type_traverse[typ_id];
1695
- if (orig) {
1696
- int res = orig(self, visit, arg);
1697
- if (res) return res;
1698
- }
1699
- } else {
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.
1776
+ {
1700
1777
  traverseproc orig = get_type_need_tp_traverse(typ);
1701
1778
  if (orig) {
1702
1779
  int res = orig(self, visit, arg);
@@ -1704,6 +1781,24 @@ PrivateAttrType_tp_traverse(PyObject* self, visitproc visit, void* arg) noexcept
1704
1781
  }
1705
1782
  }
1706
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
+ PyTypeObject* meta = Py_TYPE(self);
1797
+ if (meta && (meta->tp_flags & Py_TPFLAGS_HEAPTYPE)) {
1798
+ Py_VISIT((PyObject*)meta);
1799
+ }
1800
+ }
1801
+
1707
1802
  // Traverse PyObjects stored in AllData for this type:
1708
1803
  // - direct type attributes ::AllData::type_attr_dict[typ_id]
1709
1804
  // - attributes stored under parents for this type ::AllData::all_type_subclass_attr[parent_id][typ_id]
@@ -1779,13 +1874,21 @@ PrivateAttrType_tp_clear(PyObject* self) noexcept
1779
1874
  }
1780
1875
  PrivateAttrRecursionGuard guard((uintptr_t)self);
1781
1876
 
1782
- // Call saved/original clear if exists
1783
- if (::AllData::all_type_clear.find(typ_id) != ::AllData::all_type_clear.end()) {
1784
- inquiry orig = ::AllData::all_type_clear[typ_id];
1785
- if (orig) orig(self);
1786
- } else {
1877
+ // Recover the original clear via get_type_need_tp_clear, skipping any
1878
+ // delegating default (subtype_clear / PrivateAttr_tp_clear / our own
1879
+ // wrapper). subtype_clear on a type object would clear the class's own
1880
+ // tp_dict (its namespace), which must never happen here; when the walker
1881
+ // finds no real clear, the AllData cleanup below still breaks the cycle by
1882
+ // clearing the type-level private attributes we stored (their PyObjectStorage
1883
+ // DECREFs release the references that keep the class's tp_dict / tp_mro /
1884
+ // ht_module alive), after which CPython's own type_clear runs on the class
1885
+ // once the collector re-visits it.
1886
+ {
1787
1887
  inquiry orig = get_type_need_tp_clear(typ);
1788
- if (orig) orig(self);
1888
+ if (orig) {
1889
+ int result = orig(self);
1890
+ if (result) return result;
1891
+ }
1789
1892
  }
1790
1893
 
1791
1894
  // Clear AllData entries associated with this type. PyObjectStorage destructor
@@ -1866,10 +1969,11 @@ static PyTypeObject PrivateAttrType = {
1866
1969
  (getattrofunc)PrivateAttrType_getattr, // tp_getattro
1867
1970
  (setattrofunc)PrivateAttrType_setattr, // tp_setattro
1868
1971
  0, // tp_as_buffer
1869
- Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, // tp_flags
1972
+ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE
1973
+ | Py_TPFLAGS_HAVE_GC, // tp_flags
1870
1974
  "metaclass for private attributes", // tp_doc
1871
- (traverseproc)PrivateAttrType_tp_traverse, // tp_travers
1872
- (inquiry)PrivateAttrType_tp_clear, // tp_clear
1975
+ PrivateAttrType_tp_traverse, // tp_travers
1976
+ PrivateAttrType_tp_clear, // tp_clear
1873
1977
  0, // tp_richcompare
1874
1978
  0, // tp_weaklistoffset
1875
1979
  0, // tp_iter
@@ -1939,13 +2043,7 @@ type_get_final_attr(uintptr_t type_id, const std::string& name) noexcept
1939
2043
  } else {
1940
2044
  key = default_random_string(type_id, name);
1941
2045
  }
1942
- if (::AllData::all_type_mutex.find(type_id) == ::AllData::all_type_mutex.end()) {
1943
- std::shared_ptr<std::shared_mutex> lock(new std::shared_mutex());
1944
- ::AllData::all_type_mutex[type_id] = lock;
1945
- }
1946
- if (::AllData::type_attr_dict.find(type_id) == ::AllData::type_attr_dict.end()) {
1947
- ::AllData::type_attr_dict[type_id] = {};
1948
- }
2046
+ type_create_mutex(type_id);
1949
2047
  std::shared_lock<std::shared_mutex> lock(*::AllData::all_type_mutex[type_id]);
1950
2048
  auto& item_set = ::AllData::type_attr_dict[type_id];
1951
2049
  if (item_set.find(key) != item_set.end()) {
@@ -1980,13 +2078,7 @@ type_get_final_attr(uintptr_t type_id, const std::string& name) noexcept
1980
2078
  } else {
1981
2079
  key = default_random_string(now_visited_id, name);
1982
2080
  }
1983
- if (::AllData::all_type_subclass_mutex.find(parent_id) == ::AllData::all_type_subclass_mutex.end()) {
1984
- ::AllData::all_type_subclass_mutex[parent_id] = {};
1985
- }
1986
- if (::AllData::all_type_subclass_mutex[parent_id].find(now_visited_id) == ::AllData::all_type_subclass_mutex[parent_id].end()) {
1987
- std::shared_ptr<std::shared_mutex> lock(new std::shared_mutex());
1988
- ::AllData::all_type_subclass_mutex[parent_id][now_visited_id] = lock;
1989
- }
2081
+ type_parenttype_create_mutex(parent_id, now_visited_id);
1990
2082
  std::shared_lock<std::shared_mutex> lock(*::AllData::all_type_subclass_mutex[parent_id][now_visited_id]);
1991
2083
  if (now_mro_dict[now_visited_id].find(key) != now_mro_dict[now_visited_id].end()) {
1992
2084
  PyObject* obj = now_mro_dict[now_visited_id][key];
@@ -2010,17 +2102,12 @@ type_get_final_attr(uintptr_t type_id, const std::string& name) noexcept
2010
2102
  } else {
2011
2103
  key = default_random_string(parent_id, name);
2012
2104
  }
2013
- if (::AllData::all_type_mutex.find(parent_id) == ::AllData::all_type_mutex.end()) {
2014
- std::shared_ptr<std::shared_mutex> lock(new std::shared_mutex());
2015
- ::AllData::all_type_mutex[parent_id] = lock;
2016
- }
2017
- if (::AllData::type_attr_dict.find(parent_id) != ::AllData::type_attr_dict.end()) {
2018
- auto& item_set = ::AllData::type_attr_dict[parent_id];
2019
- std::shared_lock<std::shared_mutex> lock(*::AllData::all_type_mutex[parent_id]);
2020
- if (item_set.find(key) != item_set.end()) {
2021
- PyObject* obj = item_set[key];
2022
- return obj;
2023
- }
2105
+ type_create_mutex(parent_id);
2106
+ auto& item_set = ::AllData::type_attr_dict[parent_id];
2107
+ std::shared_lock<std::shared_mutex> lock(*::AllData::all_type_mutex[parent_id]);
2108
+ if (item_set.find(key) != item_set.end()) {
2109
+ PyObject* obj = item_set[key];
2110
+ return obj;
2024
2111
  }
2025
2112
  }
2026
2113
  }
@@ -2497,7 +2584,10 @@ PrivateAttrType_preprocess(PyObject* args, PyObject* kwds, PrivateAttrCreationDa
2497
2584
  need_value = value;
2498
2585
  }
2499
2586
  if (data.private_attrs_vector_string.find(attr_name) != data.private_attrs_vector_string.end()) {
2500
- Py_INCREF(need_value);
2587
+ // PyObjectStorage assignment INCREFs the value (RAII); the
2588
+ // reference is released when need_remove_itself is cleared
2589
+ // (PrivateAttrCreationData::clear). No manual INCREF here,
2590
+ // otherwise the value leaks (+1) when the class is destroyed.
2501
2591
  data.need_remove_itself[attr_name] = need_value;
2502
2592
  PyDict_DelItem(data.attrs_copy, key);
2503
2593
  continue;
@@ -2510,7 +2600,7 @@ PrivateAttrType_preprocess(PyObject* args, PyObject* kwds, PrivateAttrCreationDa
2510
2600
  if (data.need_remove_subclass.find(need_remove_subclass_id) == data.need_remove_subclass.end()) {
2511
2601
  data.need_remove_subclass[need_remove_subclass_id] = {};
2512
2602
  }
2513
- Py_INCREF(need_value);
2603
+ // PyObjectStorage assignment INCREFs (RAII); no manual INCREF.
2514
2604
  data.need_remove_subclass[need_remove_subclass_id][attr_name] = need_value;
2515
2605
  PyDict_DelItem(data.attrs_copy, key);
2516
2606
  continue;
@@ -2567,18 +2657,16 @@ static getattrofunc get_need_tp_getattro(PyTypeObject* cls) noexcept;
2567
2657
  static setattrofunc get_need_tp_setattro(PyTypeObject* cls) noexcept;
2568
2658
  static traverseproc get_need_tp_traverse(PyTypeObject* cls) noexcept;
2569
2659
  static inquiry get_need_tp_clear(PyTypeObject* cls) noexcept;
2570
- static destructor get_need_tp_finalize(PyTypeObject* cls) noexcept;
2571
2660
 
2572
2661
  // instance-level traverse/clear for types that use private attributes
2573
2662
  static int PrivateAttr_tp_traverse(PyObject* self, visitproc visit, void* arg) noexcept;
2574
2663
  static int PrivateAttr_tp_clear(PyObject* self) noexcept;
2664
+ // shared helper to clear object related data (avoid double clear)
2665
+ static void clear_object_related(uintptr_t id_self, uintptr_t typ_id) noexcept;
2575
2666
  // metaclass-level wrappers (if needed)
2576
2667
  static int PrivateAttrType_tp_traverse(PyObject* self, visitproc visit, void* arg) noexcept;
2577
2668
  static int PrivateAttrType_tp_clear(PyObject* self) noexcept;
2578
2669
 
2579
- // shared helper to clear object related data (avoid double clear)
2580
- static void clear_object_related(uintptr_t id_self, uintptr_t typ_id) noexcept;
2581
-
2582
2670
  static void
2583
2671
  ensure_tp(PyTypeObject* type_instance) noexcept
2584
2672
  {
@@ -2657,20 +2745,12 @@ ensure_tp(PyTypeObject* type_instance) noexcept
2657
2745
  }
2658
2746
  {
2659
2747
  if (type_instance->tp_finalize != PrivateAttr_tp_finalize) {
2660
- if (!type_instance->tp_finalize && ::AllData::all_type_finalize.find(type_id) == ::AllData::all_type_finalize.end()) {
2661
- ::AllData::all_type_finalize[type_id] = get_need_tp_finalize(type_instance);
2748
+ if (!type_instance->tp_finalize && ::AllData::all_type_del.find(type_id) == ::AllData::all_type_del.end()) {
2749
+ ::AllData::all_type_del[type_id] = get_need_tp_finalize(type_instance);
2662
2750
  } else {
2663
- ::AllData::all_type_finalize[type_id] = type_instance->tp_finalize;
2751
+ ::AllData::all_type_del[type_id] = type_instance->tp_finalize;
2664
2752
  }
2665
2753
  type_instance->tp_finalize = PrivateAttr_tp_finalize;
2666
- } else if (::AllData::all_type_finalize.find(type_id) == ::AllData::all_type_finalize.end()) {
2667
- PyTypeObject* base = type_instance->tp_base;
2668
- uintptr_t base_id = (uintptr_t)(base);
2669
- if (::AllData::all_type_finalize.find(base_id) != ::AllData::all_type_finalize.end()) {
2670
- ::AllData::all_type_finalize[type_id] = ::AllData::all_type_finalize[base_id];
2671
- } else if (base && base->tp_finalize && base->tp_finalize != PrivateAttr_tp_finalize) {
2672
- ::AllData::all_type_finalize[type_id] = base->tp_finalize;
2673
- }
2674
2754
  }
2675
2755
  }
2676
2756
  }
@@ -3031,25 +3111,6 @@ get_need_tp_setattro(PyTypeObject* cls) noexcept
3031
3111
  return NULL;
3032
3112
  }
3033
3113
 
3034
- static destructor
3035
- get_need_tp_finalize(PyTypeObject* cls) noexcept
3036
- {
3037
- PyTypeObject* base = cls->tp_base;
3038
- while (base) {
3039
- if (base->tp_finalize != PrivateAttr_tp_finalize) {
3040
- return base->tp_finalize;
3041
- } else if (::AllData::all_type_finalize.find((uintptr_t)base) != ::AllData::all_type_finalize.end()) {
3042
- if (::AllData::all_type_finalize[(uintptr_t)base]) {
3043
- return ::AllData::all_type_finalize[(uintptr_t)base];
3044
- }
3045
- base = base->tp_base;
3046
- } else {
3047
- base = base->tp_base;
3048
- }
3049
- }
3050
- return NULL;
3051
- }
3052
-
3053
3114
  static traverseproc
3054
3115
  get_need_tp_traverse(PyTypeObject* cls) noexcept
3055
3116
  {
@@ -3092,10 +3153,6 @@ get_need_tp_clear(PyTypeObject* cls) noexcept
3092
3153
  return NULL;
3093
3154
  }
3094
3155
 
3095
- // NOTE: register_finalize is defined later in this file; forward-declare it
3096
- // here because get_type_need_tp_finalize() needs to compare against it.
3097
- static void register_finalize(PyObject* cls) noexcept;
3098
-
3099
3156
  // ========================================================================
3100
3157
  // get_type_need_tp_* : find the ORIGINAL slot implementation for METACLASS
3101
3158
  // slots.
@@ -3105,7 +3162,6 @@ static void register_finalize(PyObject* cls) noexcept;
3105
3162
  // wrappers:
3106
3163
  // tp_getattro -> PrivateAttrType_getattr
3107
3164
  // tp_setattro -> PrivateAttrType_setattr
3108
- // tp_finalize -> register_finalize
3109
3165
  // tp_traverse -> PrivateAttrType_tp_traverse
3110
3166
  // tp_clear -> PrivateAttrType_tp_clear
3111
3167
  //
@@ -3155,65 +3211,81 @@ get_type_need_tp_setattro(PyTypeObject* cls) noexcept
3155
3211
  return NULL;
3156
3212
  }
3157
3213
 
3158
- static destructor
3159
- get_type_need_tp_finalize(PyTypeObject* cls) noexcept
3160
- {
3161
- PyTypeObject* base = cls->tp_base;
3162
- while (base) {
3163
- if (base->tp_finalize != register_finalize) {
3164
- return base->tp_finalize;
3165
- } else if (::AllData::all_type_finalize.find((uintptr_t)base) != ::AllData::all_type_finalize.end()) {
3166
- if (::AllData::all_type_finalize[(uintptr_t)base]) {
3167
- return ::AllData::all_type_finalize[(uintptr_t)base];
3168
- }
3169
- base = base->tp_base;
3170
- } else {
3171
- base = base->tp_base;
3172
- }
3173
- }
3174
- return NULL;
3175
- }
3176
-
3177
3214
  static traverseproc
3178
3215
  get_type_need_tp_traverse(PyTypeObject* cls) noexcept
3179
3216
  {
3217
+ // cls is a type object (an instance of a metaclass). CPython only invokes
3218
+ // a real traverse on a tracked type object directly; otherwise it delegates
3219
+ // through subtype_traverse. So walk the metaclass base chain and skip every
3220
+ // delegating default (subtype_traverse, PrivateAttr_tp_traverse, our own
3221
+ // wrapper) AND NULL slots, returning the nearest real implementation.
3222
+ //
3223
+ // For a plain metaclass chain the walk is
3224
+ // cls -> ... -> PrivateAttrBase -> object -> (tp_base == NULL)
3225
+ // and object->tp_traverse is NULL, so the loop keeps walking to the end and
3226
+ // falls through to the PyType_Type.tp_traverse fallback below: for a type
3227
+ // object the ONLY correct real traverse is CPython's own type_traverse,
3228
+ // which is what makes the class's hard self-cycle (tp_dict / tp_mro /
3229
+ // tp_bases) visible to the cyclic GC. Without that fallback the class
3230
+ // becomes uncollectable. For a REGISTERED metaclass chain the saved-map
3231
+ // branch may find a real implementation before the fallback is reached.
3180
3232
  PyTypeObject* base = cls->tp_base;
3181
3233
  while (base) {
3182
- if (base->tp_traverse != PrivateAttrType_tp_traverse
3183
- && base->tp_traverse != ::AllData::captured_subtype_traverse) {
3184
- return base->tp_traverse;
3185
- } else if (::AllData::all_type_traverse.find((uintptr_t)base) != ::AllData::all_type_traverse.end()) {
3186
- traverseproc saved = ::AllData::all_type_traverse[(uintptr_t)base];
3187
- if (saved && saved != ::AllData::captured_subtype_traverse) {
3234
+ traverseproc slot = base->tp_traverse;
3235
+ if (slot
3236
+ && slot != PrivateAttrType_tp_traverse
3237
+ && slot != ::AllData::captured_subtype_traverse
3238
+ && slot != PrivateAttr_tp_traverse) {
3239
+ return slot;
3240
+ }
3241
+ auto it = ::AllData::all_type_traverse.find((uintptr_t)base);
3242
+ if (it != ::AllData::all_type_traverse.end()) {
3243
+ traverseproc saved = it->second;
3244
+ if (saved
3245
+ && saved != ::AllData::captured_subtype_traverse
3246
+ && saved != PrivateAttr_tp_traverse
3247
+ && saved != PrivateAttrType_tp_traverse) {
3188
3248
  return saved;
3189
3249
  }
3190
- base = base->tp_base;
3191
- } else {
3192
- base = base->tp_base;
3193
3250
  }
3251
+ base = base->tp_base;
3194
3252
  }
3195
- return NULL;
3253
+ // Fallback to CPython's real type_traverse (PyType_Type.tp_traverse). A
3254
+ // NULL slot is never a real implementation, so exhausting the chain here
3255
+ // means no custom traverse exists and the only correct original for a type
3256
+ // object is type_traverse itself.
3257
+ return PyType_Type.tp_traverse;
3196
3258
  }
3197
3259
 
3198
3260
  static inquiry
3199
3261
  get_type_need_tp_clear(PyTypeObject* cls) noexcept
3200
3262
  {
3263
+ // Same as get_type_need_tp_traverse: skip every delegating default
3264
+ // (subtype_clear, PrivateAttr_tp_clear, our own wrapper) AND NULL slots,
3265
+ // then fall back to CPython's real type_clear (PyType_Type.tp_clear) so a
3266
+ // plain metaclass chain still breaks the class's hard self-cycle.
3201
3267
  PyTypeObject* base = cls->tp_base;
3202
3268
  while (base) {
3203
- if (base->tp_clear != PrivateAttrType_tp_clear
3204
- && base->tp_clear != ::AllData::captured_subtype_clear) {
3205
- return base->tp_clear;
3206
- } else if (::AllData::all_type_clear.find((uintptr_t)base) != ::AllData::all_type_clear.end()) {
3207
- inquiry saved = ::AllData::all_type_clear[(uintptr_t)base];
3208
- if (saved && saved != ::AllData::captured_subtype_clear) {
3269
+ inquiry slot = base->tp_clear;
3270
+ if (slot
3271
+ && slot != PrivateAttrType_tp_clear
3272
+ && slot != ::AllData::captured_subtype_clear
3273
+ && slot != PrivateAttr_tp_clear) {
3274
+ return slot;
3275
+ }
3276
+ auto it = ::AllData::all_type_clear.find((uintptr_t)base);
3277
+ if (it != ::AllData::all_type_clear.end()) {
3278
+ inquiry saved = it->second;
3279
+ if (saved
3280
+ && saved != ::AllData::captured_subtype_clear
3281
+ && saved != PrivateAttr_tp_clear
3282
+ && saved != PrivateAttrType_tp_clear) {
3209
3283
  return saved;
3210
3284
  }
3211
- base = base->tp_base;
3212
- } else {
3213
- base = base->tp_base;
3214
3285
  }
3286
+ base = base->tp_base;
3215
3287
  }
3216
- return NULL;
3288
+ return PyType_Type.tp_clear;
3217
3289
  }
3218
3290
 
3219
3291
  static int
@@ -3308,10 +3380,17 @@ PrivateAttrType_finalize(PyObject* cls) noexcept
3308
3380
  if (::AllData::all_type_setattro.find(typ_id) != ::AllData::all_type_setattro.end()) {
3309
3381
  ::AllData::all_type_setattro.erase(typ_id);
3310
3382
  }
3311
- if (::AllData::all_type_finalize.find(typ_id) != ::AllData::all_type_finalize.end()) {
3312
- ::AllData::all_type_finalize.erase(typ_id);
3313
- }
3314
3383
  ::AllData::all_type_mutex.erase(typ_id);
3384
+ // Clear the per-type buckets of the object-attr tables (created in
3385
+ // PrivateAttrType_postprocess / PrivateAttr_object_init_private_dict);
3386
+ // otherwise every class that ever got instances leaves an empty bucket
3387
+ // behind and all_object_attr / all_object_mutex grow forever.
3388
+ if (::AllData::all_object_attr.find(typ_id) != ::AllData::all_object_attr.end()) {
3389
+ ::AllData::all_object_attr.erase(typ_id);
3390
+ }
3391
+ if (::AllData::all_object_mutex.find(typ_id) != ::AllData::all_object_mutex.end()) {
3392
+ ::AllData::all_object_mutex.erase(typ_id);
3393
+ }
3315
3394
  clear_obj(typ_id);
3316
3395
  }
3317
3396
 
@@ -3521,21 +3600,6 @@ postprocess_for_PrivateAttr(PyObject* /*self*/, PyObject* args) noexcept
3521
3600
  Py_RETURN_NONE;
3522
3601
  }
3523
3602
 
3524
- static void
3525
- register_finalize(PyObject* cls) noexcept
3526
- {
3527
- Py_ssize_t original_ref = Py_REFCNT(cls);
3528
- PyTypeObject* base = Py_TYPE(cls)->tp_base;
3529
- while (base && base->tp_finalize == register_finalize) {
3530
- base = base->tp_base;
3531
- }
3532
- if (base && base->tp_finalize) base->tp_finalize(cls);
3533
- if (Py_REFCNT(cls) != original_ref) {
3534
- return;
3535
- }
3536
- PrivateAttrType_finalize(cls);
3537
- }
3538
-
3539
3603
  #define METACLASS_WEAKREF_CALLBACK_REMOVE_ITEM(name) do {\
3540
3604
  if (::AllData::all_type_##name.find(id) != ::AllData::all_type_##name.end()) {\
3541
3605
  ::AllData::all_type_##name.erase(id);\
@@ -3551,6 +3615,13 @@ metaclass_weakref_callback(PyObject* self, PyObject* /*weakref*/) noexcept
3551
3615
  }
3552
3616
  uintptr_t id = (uintptr_t)pointer;
3553
3617
 
3618
+ // Clean up the class-level AllData owned by this registered metaclass
3619
+ // (type_attr_dict / all_type_subclass_attr / object buckets / caches).
3620
+ // This replaces the old tp_finalize hook (register_finalize) which has
3621
+ // been removed: a weakref callback fires on every death path (refcount and
3622
+ // cyclic GC) without making the metaclass "finalizable".
3623
+ PrivateAttrType_finalize((PyObject*)id);
3624
+
3554
3625
  std::unique_lock lock(::AllData::all_register_new_metaclass_mutex);
3555
3626
 
3556
3627
  auto it = ::AllData::all_register_type_weak_ref.find(id);
@@ -3562,7 +3633,7 @@ metaclass_weakref_callback(PyObject* self, PyObject* /*weakref*/) noexcept
3562
3633
  METACLASS_WEAKREF_CALLBACK_REMOVE_ITEM(setattro);
3563
3634
  METACLASS_WEAKREF_CALLBACK_REMOVE_ITEM(traverse);
3564
3635
  METACLASS_WEAKREF_CALLBACK_REMOVE_ITEM(clear);
3565
- METACLASS_WEAKREF_CALLBACK_REMOVE_ITEM(finalize);
3636
+ METACLASS_WEAKREF_CALLBACK_REMOVE_ITEM(del);
3566
3637
 
3567
3638
  Py_RETURN_NONE;
3568
3639
  }
@@ -3654,48 +3725,37 @@ register_metaclass_head(PyObject* metaclass) noexcept
3654
3725
  }
3655
3726
  ::AllData::all_register_type_weak_ref[id] = ref;
3656
3727
  PyTypeObject* mt = (PyTypeObject*)metaclass;
3728
+ // Save the ORIGINAL slots before wrapping. Always resolve through the
3729
+ // get_type_need_tp_* walkers so a delegating default (CPython's
3730
+ // subtype_traverse/subtype_clear, which a plain heap metaclass inherits)
3731
+ // is never stored as the "original". The walkers skip those defaults and
3732
+ // fall back to PyType_Type.tp_* (the real implementation for a type
3733
+ // object). If an entry already exists for this id, keep it (idempotent).
3657
3734
  // save and replace tp_getattro
3658
3735
  if (mt->tp_getattro != PrivateAttrType_getattr) {
3659
- if (!mt->tp_getattro && ::AllData::all_type_getattro.find(id) == ::AllData::all_type_getattro.end()) {
3736
+ if (::AllData::all_type_getattro.find(id) == ::AllData::all_type_getattro.end()) {
3660
3737
  ::AllData::all_type_getattro[id] = get_type_need_tp_getattro(mt);
3661
- } else {
3662
- ::AllData::all_type_getattro[id] = mt->tp_getattro;
3663
3738
  }
3664
3739
  mt->tp_getattro = PrivateAttrType_getattr;
3665
3740
  }
3666
3741
  // save and replace tp_setattro
3667
3742
  if (mt->tp_setattro != PrivateAttrType_setattr) {
3668
- if (!mt->tp_setattro && ::AllData::all_type_setattro.find(id) == ::AllData::all_type_setattro.end()) {
3743
+ if (::AllData::all_type_setattro.find(id) == ::AllData::all_type_setattro.end()) {
3669
3744
  ::AllData::all_type_setattro[id] = get_type_need_tp_setattro(mt);
3670
- } else {
3671
- ::AllData::all_type_setattro[id] = mt->tp_setattro;
3672
3745
  }
3673
3746
  mt->tp_setattro = PrivateAttrType_setattr;
3674
3747
  }
3675
- // save and replace tp_finalize
3676
- if (mt->tp_finalize != register_finalize) {
3677
- if (!mt->tp_finalize && ::AllData::all_type_finalize.find(id) == ::AllData::all_type_finalize.end()) {
3678
- ::AllData::all_type_finalize[id] = get_type_need_tp_finalize(mt);
3679
- } else {
3680
- ::AllData::all_type_finalize[id] = mt->tp_finalize;
3681
- }
3682
- mt->tp_finalize = register_finalize;
3683
- }
3684
3748
  // save and replace tp_traverse
3685
3749
  if (mt->tp_traverse != PrivateAttrType_tp_traverse) {
3686
- if (!mt->tp_traverse && ::AllData::all_type_traverse.find(id) == ::AllData::all_type_traverse.end()) {
3750
+ if (::AllData::all_type_traverse.find(id) == ::AllData::all_type_traverse.end()) {
3687
3751
  ::AllData::all_type_traverse[id] = get_type_need_tp_traverse(mt);
3688
- } else {
3689
- ::AllData::all_type_traverse[id] = mt->tp_traverse;
3690
3752
  }
3691
3753
  mt->tp_traverse = PrivateAttrType_tp_traverse;
3692
3754
  }
3693
3755
  // save and replace tp_clear
3694
3756
  if (mt->tp_clear != PrivateAttrType_tp_clear) {
3695
- if (!mt->tp_clear && ::AllData::all_type_clear.find(id) == ::AllData::all_type_clear.end()) {
3757
+ if (::AllData::all_type_clear.find(id) == ::AllData::all_type_clear.end()) {
3696
3758
  ::AllData::all_type_clear[id] = get_type_need_tp_clear(mt);
3697
- } else {
3698
- ::AllData::all_type_clear[id] = mt->tp_clear;
3699
3759
  }
3700
3760
  mt->tp_clear = PrivateAttrType_tp_clear;
3701
3761
  }
@@ -3744,43 +3804,31 @@ ensure_metaclass_tp(PyObject* /*self*/, PyObject* metaclass) noexcept
3744
3804
  uintptr_t id = (uintptr_t)metaclass;
3745
3805
  if (::AllData::all_register_type_weak_ref.find(id) != ::AllData::all_register_type_weak_ref.end()) {
3746
3806
  PyTypeObject* mt = (PyTypeObject*)metaclass;
3807
+ // Same as register_metaclass_head: always resolve the ORIGINAL slots
3808
+ // through the get_type_need_tp_* walkers (never store a delegating
3809
+ // default such as subtype_traverse/subtype_clear). Idempotent when an
3810
+ // entry already exists.
3747
3811
  if (mt->tp_getattro != PrivateAttrType_getattr) {
3748
- if (!mt->tp_getattro && ::AllData::all_type_getattro.find(id) == ::AllData::all_type_getattro.end()) {
3812
+ if (::AllData::all_type_getattro.find(id) == ::AllData::all_type_getattro.end()) {
3749
3813
  ::AllData::all_type_getattro[id] = get_type_need_tp_getattro(mt);
3750
- } else {
3751
- ::AllData::all_type_getattro[id] = mt->tp_getattro;
3752
3814
  }
3753
3815
  mt->tp_getattro = PrivateAttrType_getattr;
3754
3816
  }
3755
3817
  if (mt->tp_setattro != PrivateAttrType_setattr) {
3756
- if (!mt->tp_setattro && ::AllData::all_type_setattro.find(id) == ::AllData::all_type_setattro.end()) {
3818
+ if (::AllData::all_type_setattro.find(id) == ::AllData::all_type_setattro.end()) {
3757
3819
  ::AllData::all_type_setattro[id] = get_type_need_tp_setattro(mt);
3758
- } else {
3759
- ::AllData::all_type_setattro[id] = mt->tp_setattro;
3760
3820
  }
3761
3821
  mt->tp_setattro = PrivateAttrType_setattr;
3762
3822
  }
3763
- if (mt->tp_finalize != register_finalize) {
3764
- if (!mt->tp_finalize && ::AllData::all_type_finalize.find(id) == ::AllData::all_type_finalize.end()) {
3765
- ::AllData::all_type_finalize[id] = get_type_need_tp_finalize(mt);
3766
- } else {
3767
- ::AllData::all_type_finalize[id] = mt->tp_finalize;
3768
- }
3769
- mt->tp_finalize = register_finalize;
3770
- }
3771
3823
  if (mt->tp_traverse != PrivateAttrType_tp_traverse) {
3772
- if (!mt->tp_traverse && ::AllData::all_type_traverse.find(id) == ::AllData::all_type_traverse.end()) {
3824
+ if (::AllData::all_type_traverse.find(id) == ::AllData::all_type_traverse.end()) {
3773
3825
  ::AllData::all_type_traverse[id] = get_type_need_tp_traverse(mt);
3774
- } else {
3775
- ::AllData::all_type_traverse[id] = mt->tp_traverse;
3776
3826
  }
3777
3827
  mt->tp_traverse = PrivateAttrType_tp_traverse;
3778
3828
  }
3779
3829
  if (mt->tp_clear != PrivateAttrType_tp_clear) {
3780
- if (!mt->tp_clear && ::AllData::all_type_clear.find(id) == ::AllData::all_type_clear.end()) {
3830
+ if (::AllData::all_type_clear.find(id) == ::AllData::all_type_clear.end()) {
3781
3831
  ::AllData::all_type_clear[id] = get_type_need_tp_clear(mt);
3782
- } else {
3783
- ::AllData::all_type_clear[id] = mt->tp_clear;
3784
3832
  }
3785
3833
  mt->tp_clear = PrivateAttrType_tp_clear;
3786
3834
  }
@@ -4133,6 +4181,12 @@ atexit_register_clean_func(void) noexcept
4133
4181
  PyMODINIT_FUNC
4134
4182
  PyInit_private_attribute(void) noexcept
4135
4183
  {
4184
+ // check if AllData::store_module_self has first value
4185
+ if (AllData::store_module_self.size() >= 1) {
4186
+ PyObject* m = AllData::store_module_self[0];
4187
+ Py_INCREF(m);
4188
+ return m;
4189
+ }
4136
4190
  if (init_all_slots() <0 ||
4137
4191
  atexit_register_clean_func() < 0 ||
4138
4192
  PyType_Ready(&PrivateWrapType) < 0 ||
@@ -4142,19 +4196,28 @@ PyInit_private_attribute(void) noexcept
4142
4196
  PyType_Ready(&PrivateTempType) < 0) {
4143
4197
  return NULL;
4144
4198
  }
4199
+ // Eagerly create PrivateAttrBase so that CPython's subtype_traverse /
4200
+ // subtype_clear pointers are captured at import time (see
4201
+ // PrivateAttrCaptureGuard and ::AllData::captured_subtype_*), instead of
4202
+ // waiting for the first lazy attribute access to the module.
4203
+ PyObject* private_attr_base = PrivateModule_get_PrivateAttrBase(NULL, NULL);
4204
+ if (!private_attr_base) {
4205
+ return NULL;
4206
+ }
4145
4207
  PyObject* all = PyList_New(0);
4146
4208
  if (!all) {
4209
+ Py_DECREF(private_attr_base);
4147
4210
  return NULL;
4148
4211
  }
4149
4212
  PyObject* m = PyModule_Create(&def);
4150
4213
  if (!m) {
4151
4214
  Py_DECREF(all);
4215
+ Py_DECREF(private_attr_base);
4152
4216
  return NULL;
4153
4217
  }
4154
4218
  #ifdef Py_GIL_DISABLED
4155
4219
  PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
4156
4220
  #endif
4157
-
4158
4221
  PyModule_AddObject(m, "__all__", all);
4159
4222
  /* all contains:
4160
4223
  * PrivateWrapProxy
@@ -4176,16 +4239,8 @@ PyInit_private_attribute(void) noexcept
4176
4239
  PyList_AppendString(all, "ensure_metaclass");
4177
4240
  Py_SET_TYPE(m, &PrivateModuleType);
4178
4241
 
4179
- // Eagerly create PrivateAttrBase so that CPython's subtype_traverse /
4180
- // subtype_clear pointers are captured at import time (see
4181
- // PrivateAttrCaptureGuard and ::AllData::captured_subtype_*), instead of
4182
- // waiting for the first lazy attribute access to the module.
4183
- PyObject* private_attr_base = PrivateModule_get_PrivateAttrBase(NULL, NULL);
4184
- if (!private_attr_base) {
4185
- Py_DECREF(m);
4186
- return NULL;
4187
- }
4188
4242
  Py_DECREF(private_attr_base);
4243
+ AllData::store_module_self.push_back(m); // store module self
4189
4244
 
4190
4245
  return m;
4191
4246
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: private_attribute_cpp
3
- Version: 2.1.4
3
+ Version: 2.1.6
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
@@ -12,6 +12,7 @@ private_attribute_cpp.egg-info/not-zip-safe
12
12
  private_attribute_cpp.egg-info/top_level.txt
13
13
  tests/test_attr_classify_owner.py
14
14
  tests/test_class_private_attrs.py
15
+ tests/test_gc_collect.py
15
16
  tests/test_private_abc.py
16
17
  tests/test_private_attrs.py
17
18
  tests/test_private_func.py
@@ -18,7 +18,7 @@ readme = open('README.md').read()
18
18
 
19
19
  setup(
20
20
  name='private_attribute_cpp',
21
- version='2.1.4',
21
+ version='2.1.6',
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.',
@@ -0,0 +1,145 @@
1
+ """GC regression tests for the metaclass type_traverse/type_clear fix.
2
+
3
+ Validates that class objects created by the private-attribute machinery are
4
+ actually reclaimed by the cyclic GC (previously they leaked because the
5
+ metaclass traverse resolved to the instance-level delegating defaults instead
6
+ of CPython's real type_traverse/type_clear).
7
+
8
+ Run: PYTHONPATH=. python test_gc_collect.py
9
+ """
10
+ import gc
11
+ import sys
12
+ import unittest
13
+ import weakref
14
+
15
+ import private_attribute
16
+ from private_attribute import PrivateAttrBase
17
+
18
+
19
+ def make_class(i, name="C"):
20
+ class C(PrivateAttrBase):
21
+ __private_attrs__ = ("_x",)
22
+ def __init__(self):
23
+ self._x = i
24
+ def get(self):
25
+ return self._x
26
+ C.__name__ = name
27
+ return C
28
+
29
+
30
+ def alive_after_del_collect(maker, n):
31
+ """Create n classes, drop them, collect; return how many are still alive."""
32
+ refs = []
33
+ for i in range(n):
34
+ C = maker(i, f"Probe{i}")
35
+ refs.append(weakref.ref(C))
36
+ del C
37
+ for _ in range(3):
38
+ gc.collect()
39
+ return sum(1 for w in refs if w() is not None)
40
+
41
+
42
+ class TestClassCollect(unittest.TestCase):
43
+ def test_empty_class_collected(self):
44
+ def maker(i, name):
45
+ class E(PrivateAttrBase):
46
+ pass
47
+ E.__name__ = name
48
+ return E
49
+ self.assertEqual(alive_after_del_collect(maker, 20), 0)
50
+
51
+ def test_class_with_private_attrs_collected(self):
52
+ self.assertEqual(alive_after_del_collect(make_class, 20), 0)
53
+
54
+ def test_class_with_private_value_freed(self):
55
+ class Holder:
56
+ pass
57
+ refs = []
58
+ for i in range(20):
59
+ h = Holder()
60
+ class H(PrivateAttrBase):
61
+ __private_attrs__ = ("_cv",)
62
+ _cv = h
63
+ refs.append(weakref.ref(h))
64
+ del h, H
65
+ for _ in range(3):
66
+ gc.collect()
67
+ self.assertEqual(sum(1 for w in refs if w() is not None), 0)
68
+
69
+ def test_mutual_class_cycle_collected(self):
70
+ def cyc():
71
+ class A(PrivateAttrBase):
72
+ __private_attrs__ = ("_ref",)
73
+ _ref = None
74
+ def set_ref(cls, v):
75
+ cls._ref = v
76
+ class B(PrivateAttrBase):
77
+ __private_attrs__ = ("_ref",)
78
+ _ref = None
79
+ def set_ref(cls, v):
80
+ cls._ref = v
81
+ A.set_ref(A, B)
82
+ B.set_ref(B, A)
83
+ wa = weakref.ref(A)
84
+ wb = weakref.ref(B)
85
+ del A, B
86
+ for _ in range(3):
87
+ gc.collect()
88
+ return wa() is not None, wb() is not None
89
+ a, b = cyc()
90
+ self.assertFalse(a)
91
+ self.assertFalse(b)
92
+
93
+ def test_self_referencing_class_collected(self):
94
+ def cyc():
95
+ class S(PrivateAttrBase):
96
+ __private_attrs__ = ("_self_ref",)
97
+ _self_ref = None
98
+ def set_self(cls, v):
99
+ cls._self_ref = v
100
+ S.set_self(S, S)
101
+ w = weakref.ref(S)
102
+ del S
103
+ for _ in range(3):
104
+ gc.collect()
105
+ return w() is not None
106
+ self.assertFalse(cyc())
107
+
108
+ def test_nested_class_collected(self):
109
+ def nested():
110
+ class Outer(PrivateAttrBase):
111
+ __private_attrs__ = ("_inner",)
112
+ def make(self):
113
+ class Inner(PrivateAttrBase):
114
+ __private_attrs__ = ("_v",)
115
+ def __init__(self):
116
+ self._v = 1
117
+ self._inner = Inner
118
+ return Inner
119
+ o = Outer()
120
+ Inner = o.make()
121
+ wi = weakref.ref(Inner)
122
+ wo = weakref.ref(o)
123
+ del o, Inner
124
+ for _ in range(3):
125
+ gc.collect()
126
+ return wi() is not None, wo() is not None
127
+ inner, outer = nested()
128
+ self.assertFalse(inner)
129
+ self.assertFalse(outer)
130
+
131
+ def test_private_attr_semantics_still_work(self):
132
+ class C(PrivateAttrBase):
133
+ __private_attrs__ = ("_x",)
134
+ def __init__(self):
135
+ self._x = 7
136
+ def get(self):
137
+ return self._x
138
+ c = C()
139
+ self.assertEqual(c.get(), 7)
140
+ with self.assertRaises(AttributeError):
141
+ c._x
142
+
143
+
144
+ if __name__ == "__main__":
145
+ unittest.main()