private-attribute-cpp 2.1.4__tar.gz → 2.1.5__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.5}/PKG-INFO +1 -1
  2. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.5}/private_attribute.cpp +207 -166
  3. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.5/private_attribute_cpp.egg-info}/PKG-INFO +1 -1
  4. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.5}/private_attribute_cpp.egg-info/SOURCES.txt +1 -0
  5. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.5}/setup.py +1 -1
  6. private_attribute_cpp-2.1.5/tests/test_gc_collect.py +145 -0
  7. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.5}/LICENSE +0 -0
  8. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.5}/MANIFEST.in +0 -0
  9. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.5}/README.md +0 -0
  10. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.5}/picosha2.h +0 -0
  11. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.5}/private_attribute.pyi +0 -0
  12. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.5}/private_attribute_cpp.egg-info/dependency_links.txt +0 -0
  13. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.5}/private_attribute_cpp.egg-info/not-zip-safe +0 -0
  14. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.5}/private_attribute_cpp.egg-info/top_level.txt +0 -0
  15. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.5}/setup.cfg +0 -0
  16. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.5}/tests/test_attr_classify_owner.py +0 -0
  17. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.5}/tests/test_class_private_attrs.py +0 -0
  18. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.5}/tests/test_private_abc.py +0 -0
  19. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.5}/tests/test_private_attrs.py +0 -0
  20. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.5}/tests/test_private_func.py +0 -0
  21. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.5}/tests/test_private_inheritance.py +0 -0
  22. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.5}/tests/test_private_method.py +0 -0
  23. {private_attribute_cpp-2.1.4 → private_attribute_cpp-2.1.5}/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.5
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;
@@ -221,13 +222,64 @@ namespace {
221
222
  static inquiry captured_subtype_clear = NULL;
222
223
  // all type tp_finalizer map
223
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
+ static std::unordered_map<uintptr_t, destructor> all_type_del;
224
227
 
225
228
  static std::shared_mutex all_register_new_metaclass_mutex;
226
229
  static std::unordered_map<uintptr_t, PyObjectStorage> all_register_type_weak_ref;
230
+ static std::vector<PyObjectStorage> store_module_self;
227
231
  };
228
232
  };
229
233
 
230
- static void
234
+ static inline void
235
+ object_create_mutex(uintptr_t final_id, uintptr_t obj_id) noexcept
236
+ {
237
+ // get object_creatmutex_mutex
238
+ std::lock_guard<std::mutex> lock(AllData::object_creatmutex_mutex);
239
+ if (::AllData::all_object_attr[final_id].find(obj_id) == ::AllData::all_object_attr[final_id].end()) {
240
+ ::AllData::all_object_attr[final_id][obj_id] = {};
241
+ }
242
+ if (::AllData::all_object_mutex.find(final_id) == ::AllData::all_object_mutex.end()) {
243
+ ::AllData::all_object_mutex[final_id] = {};
244
+ }
245
+ if (::AllData::all_object_mutex[final_id].find(obj_id) == ::AllData::all_object_mutex[final_id].end()) {
246
+ std::shared_ptr<std::shared_mutex> lock(new std::shared_mutex());
247
+ ::AllData::all_object_mutex[final_id][obj_id] = lock;
248
+ }
249
+ }
250
+
251
+ static inline void
252
+ type_create_mutex(uintptr_t typ_id) noexcept {
253
+ std::lock_guard<std::mutex> lock(AllData::type_createmutex_mutex);
254
+ if (::AllData::type_attr_dict.find(typ_id) == ::AllData::type_attr_dict.end()) {
255
+ ::AllData::type_attr_dict[typ_id] = {};
256
+ }
257
+ if (::AllData::all_type_mutex.find(typ_id) == ::AllData::all_type_mutex.end()) {
258
+ std::shared_ptr<std::shared_mutex> lock(new std::shared_mutex());
259
+ ::AllData::all_type_mutex[typ_id] = lock;
260
+ }
261
+ }
262
+
263
+ static inline void
264
+ type_parenttype_create_mutex(uintptr_t final_id, uintptr_t typ_id) noexcept
265
+ {
266
+ std::lock_guard<std::mutex> lock(AllData::type_parenttype_createmutex_mutex);
267
+ if (::AllData::all_type_subclass_attr.find(final_id) == ::AllData::all_type_subclass_attr.end()) {
268
+ ::AllData::all_type_subclass_attr[final_id] = {};
269
+ }
270
+ if (::AllData::all_type_subclass_attr[final_id].find(typ_id) == ::AllData::all_type_subclass_attr[final_id].end()) {
271
+ ::AllData::all_type_subclass_attr[final_id][typ_id] = {};
272
+ }
273
+ if (::AllData::all_type_subclass_mutex.find(final_id) == ::AllData::all_type_subclass_mutex.end()) {
274
+ ::AllData::all_type_subclass_mutex[final_id] = {};
275
+ }
276
+ if (::AllData::all_type_subclass_mutex[final_id].find(typ_id) == ::AllData::all_type_subclass_mutex[final_id].end()) {
277
+ std::shared_ptr<std::shared_mutex> lock(new std::shared_mutex());
278
+ ::AllData::all_type_subclass_mutex[final_id][typ_id] = lock;
279
+ }
280
+ }
281
+
282
+ static inline void
231
283
  clean_all_storages() noexcept
232
284
  {
233
285
  AllData::cache.clear();
@@ -248,7 +300,9 @@ clean_all_storages() noexcept
248
300
  AllData::all_type_traverse.clear();
249
301
  AllData::all_type_clear.clear();
250
302
  AllData::all_type_finalize.clear();
303
+ AllData::all_type_del.clear();
251
304
  AllData::all_register_type_weak_ref.clear();
305
+ AllData::store_module_self.clear();
252
306
  }
253
307
 
254
308
  struct FinalObject
@@ -563,16 +617,7 @@ id_getattr(uintptr_t final_id, const std::string& attr_name, PyObject* obj, PyOb
563
617
  PyErr_SetString(PyExc_TypeError, "type not found");
564
618
  return NULL;
565
619
  }
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
- }
620
+ object_create_mutex(final_id, obj_id);
576
621
  PyObject* result = NULL;
577
622
  if (final_object.status != -1) {
578
623
  result = final_object.result;
@@ -699,16 +744,7 @@ id_setattr(uintptr_t final_id, const std::string& attr_name, PyObject* obj, PyOb
699
744
  PyErr_SetString(PyExc_TypeError, "type not found");
700
745
  return -1;
701
746
  }
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
- }
747
+ object_create_mutex(final_id, obj_id);
712
748
  // first: call __set__ method
713
749
  PyObject* result = NULL;
714
750
  if (final_object.status != -1) {
@@ -764,13 +800,7 @@ type_setattr(PyObject* typ, const std::string& attr_name, PyObject* value) noexc
764
800
  return -1;
765
801
  }
766
802
  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
- }
803
+ type_create_mutex(typ_id);
774
804
  {
775
805
  std::unique_lock<std::shared_mutex> lock(*::AllData::all_type_mutex[typ_id]);
776
806
  ::AllData::type_attr_dict[typ_id][final_key] = value;
@@ -779,19 +809,7 @@ type_setattr(PyObject* typ, const std::string& attr_name, PyObject* value) noexc
779
809
  } else {
780
810
  // The accessed class is a subclass that stores its own value for a
781
811
  // 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
- }
812
+ type_parenttype_create_mutex(final_id, typ_id);
795
813
  {
796
814
  std::unique_lock<std::shared_mutex> lock(*::AllData::all_type_subclass_mutex[final_id][typ_id]);
797
815
  ::AllData::all_type_subclass_attr[final_id][typ_id][final_key] = value;
@@ -830,16 +848,7 @@ id_delattr(uintptr_t final_id, const std::string& attr_name, PyObject* obj, PyOb
830
848
  PyErr_SetString(PyExc_TypeError, "type not found");
831
849
  return -1;
832
850
  }
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
- }
851
+ object_create_mutex(final_id, obj_id);
843
852
  // first: find attribute on type to find "__delete__"
844
853
  PyObject* result = NULL;
845
854
  if (final_object.status == 0) {
@@ -897,13 +906,7 @@ type_delattr(PyObject* typ, const std::string& attr_name) noexcept
897
906
  return -1;
898
907
  }
899
908
  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
- }
909
+ type_create_mutex(typ_id);
907
910
  std::unique_lock<std::shared_mutex> lock(*::AllData::all_type_mutex[typ_id]);
908
911
  if (::AllData::type_attr_dict[typ_id].find(final_key) == ::AllData::type_attr_dict[typ_id].end()) {
909
912
  const char* type_name = get_name_from_tp_name((PyTypeObject*)typ);
@@ -915,19 +918,7 @@ type_delattr(PyObject* typ, const std::string& attr_name) noexcept
915
918
  }
916
919
  ::AllData::type_attr_dict[typ_id].erase(final_key);
917
920
  } 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
- }
921
+ type_parenttype_create_mutex(final_id, typ_id);
931
922
  std::unique_lock<std::shared_mutex> lock(*::AllData::all_type_subclass_mutex[final_id][typ_id]);
932
923
  if (::AllData::all_type_subclass_attr[final_id][typ_id].find(final_key) == ::AllData::all_type_subclass_attr[final_id][typ_id].end()) {
933
924
  const char* type_name = get_name_from_tp_name((PyTypeObject*)typ);
@@ -1364,6 +1355,8 @@ typedef struct {
1364
1355
  static void PrivateAttr_object_init_private_dict(uintptr_t obj_id, uintptr_t type_id) noexcept;
1365
1356
  static int PrivateAttr_tp_setattro(PyObject* self, PyObject* name, PyObject* value) noexcept;
1366
1357
  static void PrivateAttr_tp_finalize(PyObject* self) noexcept;
1358
+ static void PrivateAttr_tp_del(PyObject* self) noexcept;
1359
+ static void clear_object_related(uintptr_t id_self, uintptr_t typ_id) noexcept;
1367
1360
 
1368
1361
  static PyObject*
1369
1362
  PrivateAttr_tp_getattro(PyObject* self, PyObject* name) noexcept
@@ -1444,55 +1437,53 @@ PrivateAttr_tp_setattro(PyObject* self, PyObject* name, PyObject* value) noexcep
1444
1437
  static void
1445
1438
  PrivateAttr_tp_finalize(PyObject* self) noexcept
1446
1439
  {
1447
- uintptr_t id_self = (uintptr_t)self;
1448
1440
  PyTypeObject* typ = Py_TYPE(self);
1449
1441
  uintptr_t typ_id = (uintptr_t)typ;
1450
- Py_ssize_t original_ref = Py_REFCNT(self);
1451
1442
  if (::AllData::all_type_finalize.find(typ_id) != ::AllData::all_type_finalize.end()){
1452
1443
  if (::AllData::all_type_finalize[typ_id]) {
1453
1444
  ::AllData::all_type_finalize[typ_id](self);
1454
1445
  ensure_tp(typ);
1455
1446
  }
1456
1447
  }
1457
- if (original_ref != Py_REFCNT(self)) {
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) {
1458
1474
  return;
1459
1475
  }
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
1476
 
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
- }
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);
1493
1483
  }
1494
- clear_obj(id_self);
1495
1484
  }
1485
+
1486
+ clear_object_related(id_self, typ_id);
1496
1487
  }
1497
1488
 
1498
1489
  // shared helper for clearing object-related data (used by tp_clear and tp_finalize)
@@ -1689,15 +1680,14 @@ PrivateAttrType_tp_traverse(PyObject* self, visitproc visit, void* arg) noexcept
1689
1680
  }
1690
1681
  PrivateAttrRecursionGuard guard((uintptr_t)self);
1691
1682
 
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 {
1700
- traverseproc orig = get_type_need_tp_traverse(typ);
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.
1689
+ {
1690
+ traverseproc orig = PyType_Type.tp_traverse; // == type_traverse
1701
1691
  if (orig) {
1702
1692
  int res = orig(self, visit, arg);
1703
1693
  if (res) return res;
@@ -1779,13 +1769,18 @@ PrivateAttrType_tp_clear(PyObject* self) noexcept
1779
1769
  }
1780
1770
  PrivateAttrRecursionGuard guard((uintptr_t)self);
1781
1771
 
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 {
1787
- inquiry orig = get_type_need_tp_clear(typ);
1788
- if (orig) orig(self);
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.
1778
+ {
1779
+ inquiry orig = PyType_Type.tp_clear; // == type_clear
1780
+ if (orig) {
1781
+ int result = orig(self);
1782
+ if (result) return result;
1783
+ }
1789
1784
  }
1790
1785
 
1791
1786
  // Clear AllData entries associated with this type. PyObjectStorage destructor
@@ -1866,10 +1861,11 @@ static PyTypeObject PrivateAttrType = {
1866
1861
  (getattrofunc)PrivateAttrType_getattr, // tp_getattro
1867
1862
  (setattrofunc)PrivateAttrType_setattr, // tp_setattro
1868
1863
  0, // tp_as_buffer
1869
- Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, // tp_flags
1864
+ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE
1865
+ | Py_TPFLAGS_HAVE_GC, // tp_flags
1870
1866
  "metaclass for private attributes", // tp_doc
1871
- (traverseproc)PrivateAttrType_tp_traverse, // tp_travers
1872
- (inquiry)PrivateAttrType_tp_clear, // tp_clear
1867
+ PrivateAttrType_tp_traverse, // tp_travers
1868
+ PrivateAttrType_tp_clear, // tp_clear
1873
1869
  0, // tp_richcompare
1874
1870
  0, // tp_weaklistoffset
1875
1871
  0, // tp_iter
@@ -1939,13 +1935,7 @@ type_get_final_attr(uintptr_t type_id, const std::string& name) noexcept
1939
1935
  } else {
1940
1936
  key = default_random_string(type_id, name);
1941
1937
  }
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
- }
1938
+ type_create_mutex(type_id);
1949
1939
  std::shared_lock<std::shared_mutex> lock(*::AllData::all_type_mutex[type_id]);
1950
1940
  auto& item_set = ::AllData::type_attr_dict[type_id];
1951
1941
  if (item_set.find(key) != item_set.end()) {
@@ -1980,13 +1970,7 @@ type_get_final_attr(uintptr_t type_id, const std::string& name) noexcept
1980
1970
  } else {
1981
1971
  key = default_random_string(now_visited_id, name);
1982
1972
  }
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
- }
1973
+ type_parenttype_create_mutex(parent_id, now_visited_id);
1990
1974
  std::shared_lock<std::shared_mutex> lock(*::AllData::all_type_subclass_mutex[parent_id][now_visited_id]);
1991
1975
  if (now_mro_dict[now_visited_id].find(key) != now_mro_dict[now_visited_id].end()) {
1992
1976
  PyObject* obj = now_mro_dict[now_visited_id][key];
@@ -2010,17 +1994,12 @@ type_get_final_attr(uintptr_t type_id, const std::string& name) noexcept
2010
1994
  } else {
2011
1995
  key = default_random_string(parent_id, name);
2012
1996
  }
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
- }
1997
+ type_create_mutex(parent_id);
1998
+ auto& item_set = ::AllData::type_attr_dict[parent_id];
1999
+ std::shared_lock<std::shared_mutex> lock(*::AllData::all_type_mutex[parent_id]);
2000
+ if (item_set.find(key) != item_set.end()) {
2001
+ PyObject* obj = item_set[key];
2002
+ return obj;
2024
2003
  }
2025
2004
  }
2026
2005
  }
@@ -2497,7 +2476,10 @@ PrivateAttrType_preprocess(PyObject* args, PyObject* kwds, PrivateAttrCreationDa
2497
2476
  need_value = value;
2498
2477
  }
2499
2478
  if (data.private_attrs_vector_string.find(attr_name) != data.private_attrs_vector_string.end()) {
2500
- Py_INCREF(need_value);
2479
+ // PyObjectStorage assignment INCREFs the value (RAII); the
2480
+ // reference is released when need_remove_itself is cleared
2481
+ // (PrivateAttrCreationData::clear). No manual INCREF here,
2482
+ // otherwise the value leaks (+1) when the class is destroyed.
2501
2483
  data.need_remove_itself[attr_name] = need_value;
2502
2484
  PyDict_DelItem(data.attrs_copy, key);
2503
2485
  continue;
@@ -2510,7 +2492,7 @@ PrivateAttrType_preprocess(PyObject* args, PyObject* kwds, PrivateAttrCreationDa
2510
2492
  if (data.need_remove_subclass.find(need_remove_subclass_id) == data.need_remove_subclass.end()) {
2511
2493
  data.need_remove_subclass[need_remove_subclass_id] = {};
2512
2494
  }
2513
- Py_INCREF(need_value);
2495
+ // PyObjectStorage assignment INCREFs (RAII); no manual INCREF.
2514
2496
  data.need_remove_subclass[need_remove_subclass_id][attr_name] = need_value;
2515
2497
  PyDict_DelItem(data.attrs_copy, key);
2516
2498
  continue;
@@ -2568,17 +2550,18 @@ static setattrofunc get_need_tp_setattro(PyTypeObject* cls) noexcept;
2568
2550
  static traverseproc get_need_tp_traverse(PyTypeObject* cls) noexcept;
2569
2551
  static inquiry get_need_tp_clear(PyTypeObject* cls) noexcept;
2570
2552
  static destructor get_need_tp_finalize(PyTypeObject* cls) noexcept;
2553
+ static destructor get_need_tp_del(PyTypeObject* cls) noexcept;
2571
2554
 
2572
2555
  // instance-level traverse/clear for types that use private attributes
2573
2556
  static int PrivateAttr_tp_traverse(PyObject* self, visitproc visit, void* arg) noexcept;
2574
2557
  static int PrivateAttr_tp_clear(PyObject* self) noexcept;
2558
+ static void PrivateAttr_tp_del(PyObject* self) noexcept;
2559
+ // shared helper to clear object related data (avoid double clear)
2560
+ static void clear_object_related(uintptr_t id_self, uintptr_t typ_id) noexcept;
2575
2561
  // metaclass-level wrappers (if needed)
2576
2562
  static int PrivateAttrType_tp_traverse(PyObject* self, visitproc visit, void* arg) noexcept;
2577
2563
  static int PrivateAttrType_tp_clear(PyObject* self) noexcept;
2578
2564
 
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
2565
  static void
2583
2566
  ensure_tp(PyTypeObject* type_instance) noexcept
2584
2567
  {
@@ -2673,6 +2656,24 @@ ensure_tp(PyTypeObject* type_instance) noexcept
2673
2656
  }
2674
2657
  }
2675
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
+ }
2676
+ }
2676
2677
  }
2677
2678
 
2678
2679
  static void
@@ -3092,6 +3093,25 @@ get_need_tp_clear(PyTypeObject* cls) noexcept
3092
3093
  return NULL;
3093
3094
  }
3094
3095
 
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
+
3095
3115
  // NOTE: register_finalize is defined later in this file; forward-declare it
3096
3116
  // here because get_type_need_tp_finalize() needs to compare against it.
3097
3117
  static void register_finalize(PyObject* cls) noexcept;
@@ -3311,7 +3331,20 @@ PrivateAttrType_finalize(PyObject* cls) noexcept
3311
3331
  if (::AllData::all_type_finalize.find(typ_id) != ::AllData::all_type_finalize.end()) {
3312
3332
  ::AllData::all_type_finalize.erase(typ_id);
3313
3333
  }
3334
+ if (::AllData::all_type_del.find(typ_id) != ::AllData::all_type_del.end()) {
3335
+ ::AllData::all_type_del.erase(typ_id);
3336
+ }
3314
3337
  ::AllData::all_type_mutex.erase(typ_id);
3338
+ // Clear the per-type buckets of the object-attr tables (created in
3339
+ // PrivateAttrType_postprocess / PrivateAttr_object_init_private_dict);
3340
+ // otherwise every class that ever got instances leaves an empty bucket
3341
+ // behind and all_object_attr / all_object_mutex grow forever.
3342
+ if (::AllData::all_object_attr.find(typ_id) != ::AllData::all_object_attr.end()) {
3343
+ ::AllData::all_object_attr.erase(typ_id);
3344
+ }
3345
+ if (::AllData::all_object_mutex.find(typ_id) != ::AllData::all_object_mutex.end()) {
3346
+ ::AllData::all_object_mutex.erase(typ_id);
3347
+ }
3315
3348
  clear_obj(typ_id);
3316
3349
  }
3317
3350
 
@@ -3563,6 +3596,7 @@ metaclass_weakref_callback(PyObject* self, PyObject* /*weakref*/) noexcept
3563
3596
  METACLASS_WEAKREF_CALLBACK_REMOVE_ITEM(traverse);
3564
3597
  METACLASS_WEAKREF_CALLBACK_REMOVE_ITEM(clear);
3565
3598
  METACLASS_WEAKREF_CALLBACK_REMOVE_ITEM(finalize);
3599
+ METACLASS_WEAKREF_CALLBACK_REMOVE_ITEM(del);
3566
3600
 
3567
3601
  Py_RETURN_NONE;
3568
3602
  }
@@ -4133,6 +4167,12 @@ atexit_register_clean_func(void) noexcept
4133
4167
  PyMODINIT_FUNC
4134
4168
  PyInit_private_attribute(void) noexcept
4135
4169
  {
4170
+ // check if AllData::store_module_self has first value
4171
+ if (AllData::store_module_self.size() >= 1) {
4172
+ PyObject* m = AllData::store_module_self[0];
4173
+ Py_INCREF(m);
4174
+ return m;
4175
+ }
4136
4176
  if (init_all_slots() <0 ||
4137
4177
  atexit_register_clean_func() < 0 ||
4138
4178
  PyType_Ready(&PrivateWrapType) < 0 ||
@@ -4142,19 +4182,28 @@ PyInit_private_attribute(void) noexcept
4142
4182
  PyType_Ready(&PrivateTempType) < 0) {
4143
4183
  return NULL;
4144
4184
  }
4185
+ // Eagerly create PrivateAttrBase so that CPython's subtype_traverse /
4186
+ // subtype_clear pointers are captured at import time (see
4187
+ // PrivateAttrCaptureGuard and ::AllData::captured_subtype_*), instead of
4188
+ // waiting for the first lazy attribute access to the module.
4189
+ PyObject* private_attr_base = PrivateModule_get_PrivateAttrBase(NULL, NULL);
4190
+ if (!private_attr_base) {
4191
+ return NULL;
4192
+ }
4145
4193
  PyObject* all = PyList_New(0);
4146
4194
  if (!all) {
4195
+ Py_DECREF(private_attr_base);
4147
4196
  return NULL;
4148
4197
  }
4149
4198
  PyObject* m = PyModule_Create(&def);
4150
4199
  if (!m) {
4151
4200
  Py_DECREF(all);
4201
+ Py_DECREF(private_attr_base);
4152
4202
  return NULL;
4153
4203
  }
4154
4204
  #ifdef Py_GIL_DISABLED
4155
4205
  PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
4156
4206
  #endif
4157
-
4158
4207
  PyModule_AddObject(m, "__all__", all);
4159
4208
  /* all contains:
4160
4209
  * PrivateWrapProxy
@@ -4176,16 +4225,8 @@ PyInit_private_attribute(void) noexcept
4176
4225
  PyList_AppendString(all, "ensure_metaclass");
4177
4226
  Py_SET_TYPE(m, &PrivateModuleType);
4178
4227
 
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
4228
  Py_DECREF(private_attr_base);
4229
+ AllData::store_module_self.push_back(m); // store module self
4189
4230
 
4190
4231
  return m;
4191
4232
  }
@@ -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.5
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.5',
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()