private-attribute-cpp 2.1.3__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.3/private_attribute_cpp.egg-info → private_attribute_cpp-2.1.5}/PKG-INFO +5 -5
  2. {private_attribute_cpp-2.1.3 → private_attribute_cpp-2.1.5}/README.md +4 -4
  3. {private_attribute_cpp-2.1.3 → private_attribute_cpp-2.1.5}/private_attribute.cpp +229 -176
  4. {private_attribute_cpp-2.1.3 → private_attribute_cpp-2.1.5}/private_attribute.pyi +2 -2
  5. {private_attribute_cpp-2.1.3 → private_attribute_cpp-2.1.5/private_attribute_cpp.egg-info}/PKG-INFO +5 -5
  6. {private_attribute_cpp-2.1.3 → private_attribute_cpp-2.1.5}/private_attribute_cpp.egg-info/SOURCES.txt +1 -0
  7. {private_attribute_cpp-2.1.3 → private_attribute_cpp-2.1.5}/setup.py +1 -1
  8. private_attribute_cpp-2.1.5/tests/test_gc_collect.py +145 -0
  9. {private_attribute_cpp-2.1.3 → private_attribute_cpp-2.1.5}/LICENSE +0 -0
  10. {private_attribute_cpp-2.1.3 → private_attribute_cpp-2.1.5}/MANIFEST.in +0 -0
  11. {private_attribute_cpp-2.1.3 → private_attribute_cpp-2.1.5}/picosha2.h +0 -0
  12. {private_attribute_cpp-2.1.3 → private_attribute_cpp-2.1.5}/private_attribute_cpp.egg-info/dependency_links.txt +0 -0
  13. {private_attribute_cpp-2.1.3 → private_attribute_cpp-2.1.5}/private_attribute_cpp.egg-info/not-zip-safe +0 -0
  14. {private_attribute_cpp-2.1.3 → private_attribute_cpp-2.1.5}/private_attribute_cpp.egg-info/top_level.txt +0 -0
  15. {private_attribute_cpp-2.1.3 → private_attribute_cpp-2.1.5}/setup.cfg +0 -0
  16. {private_attribute_cpp-2.1.3 → private_attribute_cpp-2.1.5}/tests/test_attr_classify_owner.py +0 -0
  17. {private_attribute_cpp-2.1.3 → private_attribute_cpp-2.1.5}/tests/test_class_private_attrs.py +0 -0
  18. {private_attribute_cpp-2.1.3 → private_attribute_cpp-2.1.5}/tests/test_private_abc.py +0 -0
  19. {private_attribute_cpp-2.1.3 → private_attribute_cpp-2.1.5}/tests/test_private_attrs.py +0 -0
  20. {private_attribute_cpp-2.1.3 → private_attribute_cpp-2.1.5}/tests/test_private_func.py +0 -0
  21. {private_attribute_cpp-2.1.3 → private_attribute_cpp-2.1.5}/tests/test_private_inheritance.py +0 -0
  22. {private_attribute_cpp-2.1.3 → private_attribute_cpp-2.1.5}/tests/test_private_method.py +0 -0
  23. {private_attribute_cpp-2.1.3 → 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.3
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
@@ -75,7 +75,7 @@ print(obj.expensive_api_call(10)) # works with all decorators applied
75
75
  | 1 | PrivateWrapProxy | Decorator wrapper for arbitrary decorators | When needed |
76
76
  | 2 | private_func=callable | Custom hidden-name generator | Optional |
77
77
  | 3 | Pass private_func in class definition | Same as above | Optional |
78
- | 4 | \_\_private_attrs\_\_ list | Declare which attributes are private | Yes |
78
+ | 4 | \_\_private_attrs\_\_ list | Declare which attributes are private | Optional (changed in 2.1.4) |
79
79
  | 5 | @PrivateWrapProxy(...) | Make any decorator compatible with private attributes | When needed |
80
80
  | 6 | method.result.xxx chain + dummy wrap | Fix decorator order and name conflicts | When needed |
81
81
 
@@ -240,8 +240,8 @@ TypeError: Can't instantiate abstract class MyClass without an implementation fo
240
240
 
241
241
  ## Notes
242
242
 
243
- - All of the private attributes class must contain the `__private_attrs__` attribute.
244
- - The `__private_attrs__` attribute must be a sequence of strings.
243
+ - ~~All of the private attributes class must contain the `__private_attrs__` attribute.~~(changed in 2.1.4)
244
+ - The `__private_attrs__` attribute must be a sequence of strings or just one string.
245
245
  - You cannot define the name which in `__slots__` to `__private_attrs__`.
246
246
  - When you define `__slots__` and `__private_attrs__` in one class, the attributes in `__private_attrs__` can also be defined in the methods, even though they are not in `__slots__`.
247
247
  - All of the object that is the instance of the class "PrivateAttrBase" or its subclass are default to be unable to be pickled.
@@ -266,4 +266,4 @@ This package require the c++ module "[picosha2](https://github.com/okdshin/PicoS
266
266
 
267
267
  ## Support
268
268
 
269
- Now it doesn't support "PyPy".
269
+ Now it only support Cpython.
@@ -56,7 +56,7 @@ print(obj.expensive_api_call(10)) # works with all decorators applied
56
56
  | 1 | PrivateWrapProxy | Decorator wrapper for arbitrary decorators | When needed |
57
57
  | 2 | private_func=callable | Custom hidden-name generator | Optional |
58
58
  | 3 | Pass private_func in class definition | Same as above | Optional |
59
- | 4 | \_\_private_attrs\_\_ list | Declare which attributes are private | Yes |
59
+ | 4 | \_\_private_attrs\_\_ list | Declare which attributes are private | Optional (changed in 2.1.4) |
60
60
  | 5 | @PrivateWrapProxy(...) | Make any decorator compatible with private attributes | When needed |
61
61
  | 6 | method.result.xxx chain + dummy wrap | Fix decorator order and name conflicts | When needed |
62
62
 
@@ -221,8 +221,8 @@ TypeError: Can't instantiate abstract class MyClass without an implementation fo
221
221
 
222
222
  ## Notes
223
223
 
224
- - All of the private attributes class must contain the `__private_attrs__` attribute.
225
- - The `__private_attrs__` attribute must be a sequence of strings.
224
+ - ~~All of the private attributes class must contain the `__private_attrs__` attribute.~~(changed in 2.1.4)
225
+ - The `__private_attrs__` attribute must be a sequence of strings or just one string.
226
226
  - You cannot define the name which in `__slots__` to `__private_attrs__`.
227
227
  - When you define `__slots__` and `__private_attrs__` in one class, the attributes in `__private_attrs__` can also be defined in the methods, even though they are not in `__slots__`.
228
228
  - All of the object that is the instance of the class "PrivateAttrBase" or its subclass are default to be unable to be pickled.
@@ -247,4 +247,4 @@ This package require the c++ module "[picosha2](https://github.com/okdshin/PicoS
247
247
 
248
248
  ## Support
249
249
 
250
- Now it doesn't support "PyPy".
250
+ Now it only support Cpython.
@@ -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
  }
@@ -2333,10 +2312,23 @@ PrivateAttrType_preprocess(PyObject* args, PyObject* kwds, PrivateAttrCreationDa
2333
2312
  return false;
2334
2313
  }
2335
2314
 
2336
- PyObject* __private_attrs__ = PyDict_GetItemString(data.attrs, "__private_attrs__");
2337
- if (!__private_attrs__) {
2338
- PyErr_SetString(PyExc_TypeError, "'__private_attrs__' is needed for type 'PrivateAttrType'");
2339
- return false;
2315
+ PyObjectStorage __private_attrs__;
2316
+ if (PyDict_ContainsString(data.attrs, "__private_attrs__")) {
2317
+ __private_attrs__ = PyDict_GetItemString(data.attrs, "__private_attrs__");
2318
+ }
2319
+ else {
2320
+ __private_attrs__ = PyTuple_New(0);
2321
+ Py_DECREF(__private_attrs__);
2322
+ }
2323
+
2324
+ if (PyUnicode_Check(__private_attrs__)) {
2325
+ PyObject* new___private_attrs__ = PyTuple_New(1);
2326
+ if (!new___private_attrs__) {
2327
+ return false;
2328
+ }
2329
+ PyTuple_SET_ITEM(new___private_attrs__, 0, __private_attrs__);
2330
+ __private_attrs__ = new___private_attrs__;
2331
+ Py_DECREF(__private_attrs__);
2340
2332
  }
2341
2333
 
2342
2334
  if (!PySequence_Check(__private_attrs__)) {
@@ -2379,8 +2371,7 @@ PrivateAttrType_preprocess(PyObject* args, PyObject* kwds, PrivateAttrCreationDa
2379
2371
 
2380
2372
  for (const char** p = invalid_name; *p != NULL; p++) {
2381
2373
  if (strcmp(attr_str.c_str(), *p) == 0) {
2382
- std::string error_msg = "invalid attribute name: '" + std::string(*p) + "'";
2383
- PyErr_SetString(PyExc_TypeError, error_msg.c_str());
2374
+ PyErr_Format(PyExc_TypeError, "Invalid name '%s' in '__private_attrs__'", attr_str.c_str());
2384
2375
  return false;
2385
2376
  }
2386
2377
  }
@@ -2421,7 +2412,7 @@ PrivateAttrType_preprocess(PyObject* args, PyObject* kwds, PrivateAttrCreationDa
2421
2412
  }
2422
2413
  slot_seq = keys;
2423
2414
  } else {
2424
- slot_seq = PySequence_Fast(all_slots, "__slots__ must be a sequence");
2415
+ slot_seq = PySequence_Fast(all_slots, "__slots__ must be a string, dict or sequence");
2425
2416
  }
2426
2417
  if (!slot_seq) {
2427
2418
  return false;
@@ -2433,9 +2424,9 @@ PrivateAttrType_preprocess(PyObject* args, PyObject* kwds, PrivateAttrCreationDa
2433
2424
  PyObject* slot = PySequence_Fast_GET_ITEM(slot_seq, j);
2434
2425
  if (PyUnicode_Check(slot)) {
2435
2426
  const char* slot_cstr = PyUnicode_AsUTF8(slot);
2436
- if (data.private_attrs_vector_string.find((std::string)slot_cstr) != data.private_attrs_vector_string.end()){
2437
- std::string error_msg = "'__slots__' and '__private_attrs__' cannot have the same attribute name: '" + std::string(slot_cstr) + "'";
2438
- PyErr_SetString(PyExc_TypeError, error_msg.c_str());
2427
+ std::string final_name = real_class_name(slot_cstr, data.class_name);
2428
+ if (data.private_attrs_vector_string.find(final_name) != data.private_attrs_vector_string.end()){
2429
+ PyErr_Format(PyExc_TypeError, "'__slots__' and '__private_attrs__' cannot have the same attribute name: '%s'", final_name.c_str());
2439
2430
  Py_DECREF(slot_seq);
2440
2431
  return false;
2441
2432
  }
@@ -2485,7 +2476,10 @@ PrivateAttrType_preprocess(PyObject* args, PyObject* kwds, PrivateAttrCreationDa
2485
2476
  need_value = value;
2486
2477
  }
2487
2478
  if (data.private_attrs_vector_string.find(attr_name) != data.private_attrs_vector_string.end()) {
2488
- 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.
2489
2483
  data.need_remove_itself[attr_name] = need_value;
2490
2484
  PyDict_DelItem(data.attrs_copy, key);
2491
2485
  continue;
@@ -2498,7 +2492,7 @@ PrivateAttrType_preprocess(PyObject* args, PyObject* kwds, PrivateAttrCreationDa
2498
2492
  if (data.need_remove_subclass.find(need_remove_subclass_id) == data.need_remove_subclass.end()) {
2499
2493
  data.need_remove_subclass[need_remove_subclass_id] = {};
2500
2494
  }
2501
- Py_INCREF(need_value);
2495
+ // PyObjectStorage assignment INCREFs (RAII); no manual INCREF.
2502
2496
  data.need_remove_subclass[need_remove_subclass_id][attr_name] = need_value;
2503
2497
  PyDict_DelItem(data.attrs_copy, key);
2504
2498
  continue;
@@ -2556,17 +2550,18 @@ static setattrofunc get_need_tp_setattro(PyTypeObject* cls) noexcept;
2556
2550
  static traverseproc get_need_tp_traverse(PyTypeObject* cls) noexcept;
2557
2551
  static inquiry get_need_tp_clear(PyTypeObject* cls) noexcept;
2558
2552
  static destructor get_need_tp_finalize(PyTypeObject* cls) noexcept;
2553
+ static destructor get_need_tp_del(PyTypeObject* cls) noexcept;
2559
2554
 
2560
2555
  // instance-level traverse/clear for types that use private attributes
2561
2556
  static int PrivateAttr_tp_traverse(PyObject* self, visitproc visit, void* arg) noexcept;
2562
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;
2563
2561
  // metaclass-level wrappers (if needed)
2564
2562
  static int PrivateAttrType_tp_traverse(PyObject* self, visitproc visit, void* arg) noexcept;
2565
2563
  static int PrivateAttrType_tp_clear(PyObject* self) noexcept;
2566
2564
 
2567
- // shared helper to clear object related data (avoid double clear)
2568
- static void clear_object_related(uintptr_t id_self, uintptr_t typ_id) noexcept;
2569
-
2570
2565
  static void
2571
2566
  ensure_tp(PyTypeObject* type_instance) noexcept
2572
2567
  {
@@ -2661,6 +2656,24 @@ ensure_tp(PyTypeObject* type_instance) noexcept
2661
2656
  }
2662
2657
  }
2663
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
+ }
2664
2677
  }
2665
2678
 
2666
2679
  static void
@@ -3080,6 +3093,25 @@ get_need_tp_clear(PyTypeObject* cls) noexcept
3080
3093
  return NULL;
3081
3094
  }
3082
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
+
3083
3115
  // NOTE: register_finalize is defined later in this file; forward-declare it
3084
3116
  // here because get_type_need_tp_finalize() needs to compare against it.
3085
3117
  static void register_finalize(PyObject* cls) noexcept;
@@ -3299,7 +3331,20 @@ PrivateAttrType_finalize(PyObject* cls) noexcept
3299
3331
  if (::AllData::all_type_finalize.find(typ_id) != ::AllData::all_type_finalize.end()) {
3300
3332
  ::AllData::all_type_finalize.erase(typ_id);
3301
3333
  }
3334
+ if (::AllData::all_type_del.find(typ_id) != ::AllData::all_type_del.end()) {
3335
+ ::AllData::all_type_del.erase(typ_id);
3336
+ }
3302
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
+ }
3303
3348
  clear_obj(typ_id);
3304
3349
  }
3305
3350
 
@@ -3551,6 +3596,7 @@ metaclass_weakref_callback(PyObject* self, PyObject* /*weakref*/) noexcept
3551
3596
  METACLASS_WEAKREF_CALLBACK_REMOVE_ITEM(traverse);
3552
3597
  METACLASS_WEAKREF_CALLBACK_REMOVE_ITEM(clear);
3553
3598
  METACLASS_WEAKREF_CALLBACK_REMOVE_ITEM(finalize);
3599
+ METACLASS_WEAKREF_CALLBACK_REMOVE_ITEM(del);
3554
3600
 
3555
3601
  Py_RETURN_NONE;
3556
3602
  }
@@ -4121,6 +4167,12 @@ atexit_register_clean_func(void) noexcept
4121
4167
  PyMODINIT_FUNC
4122
4168
  PyInit_private_attribute(void) noexcept
4123
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
+ }
4124
4176
  if (init_all_slots() <0 ||
4125
4177
  atexit_register_clean_func() < 0 ||
4126
4178
  PyType_Ready(&PrivateWrapType) < 0 ||
@@ -4130,19 +4182,28 @@ PyInit_private_attribute(void) noexcept
4130
4182
  PyType_Ready(&PrivateTempType) < 0) {
4131
4183
  return NULL;
4132
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
+ }
4133
4193
  PyObject* all = PyList_New(0);
4134
4194
  if (!all) {
4195
+ Py_DECREF(private_attr_base);
4135
4196
  return NULL;
4136
4197
  }
4137
4198
  PyObject* m = PyModule_Create(&def);
4138
4199
  if (!m) {
4139
4200
  Py_DECREF(all);
4201
+ Py_DECREF(private_attr_base);
4140
4202
  return NULL;
4141
4203
  }
4142
4204
  #ifdef Py_GIL_DISABLED
4143
4205
  PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
4144
4206
  #endif
4145
-
4146
4207
  PyModule_AddObject(m, "__all__", all);
4147
4208
  /* all contains:
4148
4209
  * PrivateWrapProxy
@@ -4164,16 +4225,8 @@ PyInit_private_attribute(void) noexcept
4164
4225
  PyList_AppendString(all, "ensure_metaclass");
4165
4226
  Py_SET_TYPE(m, &PrivateModuleType);
4166
4227
 
4167
- // Eagerly create PrivateAttrBase so that CPython's subtype_traverse /
4168
- // subtype_clear pointers are captured at import time (see
4169
- // PrivateAttrCaptureGuard and ::AllData::captured_subtype_*), instead of
4170
- // waiting for the first lazy attribute access to the module.
4171
- PyObject* private_attr_base = PrivateModule_get_PrivateAttrBase(NULL, NULL);
4172
- if (!private_attr_base) {
4173
- Py_DECREF(m);
4174
- return NULL;
4175
- }
4176
4228
  Py_DECREF(private_attr_base);
4229
+ AllData::store_module_self.push_back(m); // store module self
4177
4230
 
4178
4231
  return m;
4179
4232
  }
@@ -15,12 +15,12 @@ class MyClass(PrivateAttrBase):
15
15
  return self._private_attr1
16
16
  ```
17
17
  """
18
- from typing import Any, TypeVar, Callable, TypedDict, Sequence, Generic
18
+ from typing import Any, TypeVar, Callable, TypedDict, Sequence, Generic, Optional
19
19
  from types import FunctionType
20
20
 
21
21
  # define the dict that must have a key "__private_attrs__" and value must be the sequence of strings
22
22
  class _PrivateAttrDict(TypedDict):
23
- __private_attrs__: Sequence[str]
23
+ __private_attrs__: Optional[Sequence[str]]
24
24
 
25
25
  _T = TypeVar('T')
26
26
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: private_attribute_cpp
3
- Version: 2.1.3
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
@@ -75,7 +75,7 @@ print(obj.expensive_api_call(10)) # works with all decorators applied
75
75
  | 1 | PrivateWrapProxy | Decorator wrapper for arbitrary decorators | When needed |
76
76
  | 2 | private_func=callable | Custom hidden-name generator | Optional |
77
77
  | 3 | Pass private_func in class definition | Same as above | Optional |
78
- | 4 | \_\_private_attrs\_\_ list | Declare which attributes are private | Yes |
78
+ | 4 | \_\_private_attrs\_\_ list | Declare which attributes are private | Optional (changed in 2.1.4) |
79
79
  | 5 | @PrivateWrapProxy(...) | Make any decorator compatible with private attributes | When needed |
80
80
  | 6 | method.result.xxx chain + dummy wrap | Fix decorator order and name conflicts | When needed |
81
81
 
@@ -240,8 +240,8 @@ TypeError: Can't instantiate abstract class MyClass without an implementation fo
240
240
 
241
241
  ## Notes
242
242
 
243
- - All of the private attributes class must contain the `__private_attrs__` attribute.
244
- - The `__private_attrs__` attribute must be a sequence of strings.
243
+ - ~~All of the private attributes class must contain the `__private_attrs__` attribute.~~(changed in 2.1.4)
244
+ - The `__private_attrs__` attribute must be a sequence of strings or just one string.
245
245
  - You cannot define the name which in `__slots__` to `__private_attrs__`.
246
246
  - When you define `__slots__` and `__private_attrs__` in one class, the attributes in `__private_attrs__` can also be defined in the methods, even though they are not in `__slots__`.
247
247
  - All of the object that is the instance of the class "PrivateAttrBase" or its subclass are default to be unable to be pickled.
@@ -266,4 +266,4 @@ This package require the c++ module "[picosha2](https://github.com/okdshin/PicoS
266
266
 
267
267
  ## Support
268
268
 
269
- Now it doesn't support "PyPy".
269
+ Now it only support Cpython.
@@ -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.3',
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()