private-attribute-cpp 2.1.6__tar.gz → 2.1.8__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 (24) hide show
  1. {private_attribute_cpp-2.1.6/private_attribute_cpp.egg-info → private_attribute_cpp-2.1.8}/PKG-INFO +1 -1
  2. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8}/private_attribute.cpp +135 -8
  3. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8/private_attribute_cpp.egg-info}/PKG-INFO +1 -1
  4. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8}/private_attribute_cpp.egg-info/SOURCES.txt +2 -1
  5. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8}/setup.py +9 -3
  6. private_attribute_cpp-2.1.8/tests/test_slots_cycle.py +59 -0
  7. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8}/LICENSE +0 -0
  8. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8}/MANIFEST.in +0 -0
  9. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8}/README.md +0 -0
  10. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8}/picosha2.h +0 -0
  11. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8}/private_attribute.pyi +0 -0
  12. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8}/private_attribute_cpp.egg-info/dependency_links.txt +0 -0
  13. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8}/private_attribute_cpp.egg-info/not-zip-safe +0 -0
  14. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8}/private_attribute_cpp.egg-info/top_level.txt +0 -0
  15. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8}/setup.cfg +0 -0
  16. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8}/tests/test_attr_classify_owner.py +0 -0
  17. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8}/tests/test_class_private_attrs.py +0 -0
  18. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8}/tests/test_gc_collect.py +0 -0
  19. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8}/tests/test_private_abc.py +0 -0
  20. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8}/tests/test_private_attrs.py +0 -0
  21. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8}/tests/test_private_func.py +0 -0
  22. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8}/tests/test_private_inheritance.py +0 -0
  23. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8}/tests/test_private_method.py +0 -0
  24. {private_attribute_cpp-2.1.6 → private_attribute_cpp-2.1.8}/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.6
3
+ Version: 2.1.8
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
@@ -1512,6 +1512,92 @@ public:
1512
1512
  }
1513
1513
  };
1514
1514
 
1515
+ // ========================================================================
1516
+ // __slots__ traversal/clear (mirror of CPython's subtype_traverse slot loop).
1517
+ //
1518
+ // A private-attribute class's LIVE tp_traverse/tp_clear are replaced with
1519
+ // PrivateAttr_tp_traverse / PrivateAttr_tp_clear. The saved "original" is
1520
+ // CPython's subtype_traverse/subtype_clear, but because subtype_traverse
1521
+ // resolves the nearest base through the LIVE slot, it treats our wrapper as
1522
+ // the nearest "different" traverse and skips its own traverse_slots loop. The
1523
+ // __slots__ members (Py_T_OBJECT_EX entries in the heap type's member table)
1524
+ // therefore become invisible to the collector, so a cycle formed purely
1525
+ // through __slots__ members leaks.
1526
+ //
1527
+ // These helpers walk the member table for each type level whose live
1528
+ // traverse/clear slot is still a delegating default (subtype_traverse /
1529
+ // subtype_clear or our own wrapper), exactly mirroring the
1530
+ // `while (base->tp_traverse == subtype_traverse)` walk in CPython.
1531
+ // ========================================================================
1532
+
1533
+ // Locate the member table of a heap type in a version-independent way.
1534
+ // - 3.12+ : PyObject_GetItemData((PyObject*)type) is the public accessor
1535
+ // (_PyHeapType_GET_MEMBERS is defined as that since 3.12).
1536
+ // - <=3.11 : members float right after the PyHeapTypeObject header:
1537
+ // _PyHeapType_GET_MEMBERS(et) == (char*)et + Py_TYPE(et)->tp_basicsize.
1538
+ static PyMemberDef*
1539
+ PrivateAttr_get_tp_members(PyTypeObject* type) noexcept
1540
+ {
1541
+ if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) {
1542
+ return NULL;
1543
+ }
1544
+ #if PY_VERSION_HEX >= 0x030C0000
1545
+ return (PyMemberDef*)PyObject_GetItemData((PyObject*)type);
1546
+ #else
1547
+ return (PyMemberDef*)((char*)type + Py_TYPE(type)->tp_basicsize);
1548
+ #endif
1549
+ }
1550
+
1551
+ static int
1552
+ PrivateAttr_traverse_slots(PyTypeObject* type, PyObject* self,
1553
+ visitproc visit, void* arg) noexcept
1554
+ {
1555
+ if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) {
1556
+ return 0;
1557
+ }
1558
+ PyMemberDef* mp = PrivateAttr_get_tp_members(type);
1559
+ if (mp == NULL) {
1560
+ return 0;
1561
+ }
1562
+ Py_ssize_t n = Py_SIZE(type);
1563
+ for (Py_ssize_t i = 0; i < n; i++, mp++) {
1564
+ if (mp->type == T_OBJECT_EX) {
1565
+ char* addr = (char*)self + mp->offset;
1566
+ PyObject* obj = *(PyObject**)addr;
1567
+ if (obj != NULL) {
1568
+ int err = visit(obj, arg);
1569
+ if (err) {
1570
+ return err;
1571
+ }
1572
+ }
1573
+ }
1574
+ }
1575
+ return 0;
1576
+ }
1577
+
1578
+ static void
1579
+ PrivateAttr_clear_slots(PyTypeObject* type, PyObject* self) noexcept
1580
+ {
1581
+ if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) {
1582
+ return;
1583
+ }
1584
+ PyMemberDef* mp = PrivateAttr_get_tp_members(type);
1585
+ if (mp == NULL) {
1586
+ return;
1587
+ }
1588
+ Py_ssize_t n = Py_SIZE(type);
1589
+ for (Py_ssize_t i = 0; i < n; i++, mp++) {
1590
+ if (mp->type == T_OBJECT_EX && !(mp->flags & READONLY)) {
1591
+ char* addr = (char*)self + mp->offset;
1592
+ PyObject* obj = *(PyObject**)addr;
1593
+ if (obj != NULL) {
1594
+ *(PyObject**)addr = NULL;
1595
+ Py_DECREF(obj);
1596
+ }
1597
+ }
1598
+ }
1599
+ }
1600
+
1515
1601
  static int
1516
1602
  PrivateAttr_tp_traverse(PyObject* self, visitproc visit, void* arg) noexcept
1517
1603
  {
@@ -1540,6 +1626,29 @@ PrivateAttr_tp_traverse(PyObject* self, visitproc visit, void* arg) noexcept
1540
1626
  }
1541
1627
  }
1542
1628
 
1629
+ // __slots__ visit (mirror of subtype_traverse's slot-walking loop). See
1630
+ // PrivateAttr_traverse_slots above: once the LIVE tp_traverse is this
1631
+ // wrapper, subtype_traverse skips its own loop, so walk the member table
1632
+ // of this type and every delegating base explicitly. Stop at the first
1633
+ // base whose live traverse is a real implementation (it owns its slots).
1634
+ {
1635
+ PyTypeObject* base = typ;
1636
+ while (base) {
1637
+ if (base->tp_traverse == ::AllData::captured_subtype_traverse
1638
+ || base->tp_traverse == PrivateAttr_tp_traverse) {
1639
+ if (Py_SIZE(base)) {
1640
+ int err = PrivateAttr_traverse_slots(base, self, visit, arg);
1641
+ if (err) {
1642
+ return err;
1643
+ }
1644
+ }
1645
+ base = base->tp_base;
1646
+ } else {
1647
+ break;
1648
+ }
1649
+ }
1650
+ }
1651
+
1543
1652
  // Instance-dict visit (mirror of subtype_traverse's dictoffset block).
1544
1653
  // The saved "original" for a private-attribute class is subtype_traverse,
1545
1654
  // but because the class's LIVE tp_traverse has been replaced with this
@@ -1645,6 +1754,25 @@ PrivateAttr_tp_clear(PyObject* self) noexcept
1645
1754
  if (orig) orig(self);
1646
1755
  }
1647
1756
 
1757
+ // __slots__ clear (mirror of subtype_clear's clear_slots loop). Same
1758
+ // reasoning as the traverse side: subtype_clear can't run its own loop
1759
+ // once the LIVE tp_clear is this wrapper, so clear the __slots__ members
1760
+ // of this type and every delegating base explicitly.
1761
+ {
1762
+ PyTypeObject* base = typ;
1763
+ while (base) {
1764
+ if (base->tp_clear == ::AllData::captured_subtype_clear
1765
+ || base->tp_clear == PrivateAttr_tp_clear) {
1766
+ if (Py_SIZE(base)) {
1767
+ PrivateAttr_clear_slots(base, self);
1768
+ }
1769
+ base = base->tp_base;
1770
+ } else {
1771
+ break;
1772
+ }
1773
+ }
1774
+ }
1775
+
1648
1776
  // Instance-dict clear (mirror of subtype_clear's dict handling). Same
1649
1777
  // reasoning as the traverse side: subtype_clear can't run its dict block
1650
1778
  // once the LIVE tp_clear has been replaced, so clear the instance dict
@@ -1756,8 +1884,8 @@ static inquiry get_type_need_tp_clear(PyTypeObject* cls) noexcept;
1756
1884
  static int
1757
1885
  PrivateAttrType_tp_traverse(PyObject* self, visitproc visit, void* arg) noexcept
1758
1886
  {
1759
- PyTypeObject* typ = (PyTypeObject*)self;
1760
- uintptr_t typ_id = (uintptr_t)typ;
1887
+ PyTypeObject* typ = Py_TYPE(self);
1888
+ uintptr_t typ_id = (uintptr_t)self;
1761
1889
 
1762
1890
  // Re-entrancy guard: see PrivateAttrRecursionGuard above.
1763
1891
  if (!PrivateAttrRecursionGuard::try_enter((uintptr_t)self)) {
@@ -1793,9 +1921,8 @@ PrivateAttrType_tp_traverse(PyObject* self, visitproc visit, void* arg) noexcept
1793
1921
  // the visit when the metaclass is a heap type, matching subtype_traverse's
1794
1922
  // condition and avoiding the trivial/static type link.
1795
1923
  {
1796
- PyTypeObject* meta = Py_TYPE(self);
1797
- if (meta && (meta->tp_flags & Py_TPFLAGS_HEAPTYPE)) {
1798
- Py_VISIT((PyObject*)meta);
1924
+ if (typ->tp_flags & Py_TPFLAGS_HEAPTYPE) {
1925
+ Py_VISIT((PyObject*)typ);
1799
1926
  }
1800
1927
  }
1801
1928
 
@@ -1865,8 +1992,8 @@ PrivateAttrType_tp_traverse(PyObject* self, visitproc visit, void* arg) noexcept
1865
1992
  static int
1866
1993
  PrivateAttrType_tp_clear(PyObject* self) noexcept
1867
1994
  {
1868
- PyTypeObject* typ = (PyTypeObject*)self;
1869
- uintptr_t typ_id = (uintptr_t)typ;
1995
+ PyTypeObject* typ = Py_TYPE(self);
1996
+ uintptr_t typ_id = (uintptr_t)self;
1870
1997
 
1871
1998
  // Re-entrancy guard: see PrivateAttrRecursionGuard above.
1872
1999
  if (!PrivateAttrRecursionGuard::try_enter((uintptr_t)self)) {
@@ -3868,7 +3995,7 @@ PrivateModule_get_PrivateAttrBase(PyObject* /*self*/, void* /*closure*/) noexcep
3868
3995
  }
3869
3996
 
3870
3997
  static int
3871
- PyList_AppendString(PyObject* list, const char* str)
3998
+ PyList_AppendString(PyObject* list, const char* str) noexcept
3872
3999
  {
3873
4000
  PyObject* obj = PyUnicode_InternFromString(str);
3874
4001
  if (!obj) return -1;
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: private_attribute_cpp
3
- Version: 2.1.6
3
+ Version: 2.1.8
4
4
  Summary: A Python package that provides a way to define private attributes in C++ implementation.
5
5
  Home-page: https://github.com/Locked-chess-official/private_attribute_cpp
6
6
  Author: HuangHaoHua
@@ -18,4 +18,5 @@ tests/test_private_attrs.py
18
18
  tests/test_private_func.py
19
19
  tests/test_private_inheritance.py
20
20
  tests/test_private_method.py
21
- tests/test_private_wrap.py
21
+ tests/test_private_wrap.py
22
+ tests/test_slots_cycle.py
@@ -1,10 +1,15 @@
1
1
  from setuptools import setup, Extension
2
+ import os
2
3
  import sys
3
4
 
4
5
  if sys.platform == "win32":
5
- extra_compile_args = ['/std:c++17']
6
+ extra_compile_args = ['/std:c++17', '/GR-']
7
+ extra_link_args = []
6
8
  else:
7
- extra_compile_args = ['-std=c++17']
9
+ extra_compile_args = ['-std=c++17', '-fno-exceptions', '-fno-rtti']
10
+ extra_link_args = []
11
+ if os.environ.get('PRIVATE_ATTR_KEEP_DEBUG') != '1':
12
+ extra_compile_args.append('-g0')
8
13
 
9
14
  module = Extension(
10
15
  'private_attribute',
@@ -12,13 +17,14 @@ module = Extension(
12
17
  include_dirs=['.'],
13
18
  language='c++',
14
19
  extra_compile_args=extra_compile_args,
20
+ extra_link_args=extra_link_args,
15
21
  )
16
22
 
17
23
  readme = open('README.md').read()
18
24
 
19
25
  setup(
20
26
  name='private_attribute_cpp',
21
- version='2.1.6',
27
+ version='2.1.8',
22
28
  author="HuangHaoHua",
23
29
  author_email="13140752715@example.com",
24
30
  description='A Python package that provides a way to define private attributes in C++ implementation.',
@@ -0,0 +1,59 @@
1
+ """Regression test for __slots__ traversal in private-attribute types.
2
+
3
+ A private-attribute class whose __slots__ member participates in a reference
4
+ cycle must still be collected by the cyclic GC. The instance-level traverse
5
+ wrapper previously skipped the __slots__ storage entirely (it only visited the
6
+ instance __dict__ and AllData-stored private attributes), so a cycle formed
7
+ purely through __slots__ members leaked.
8
+ """
9
+ import gc
10
+ import unittest
11
+ import weakref
12
+
13
+ import private_attribute
14
+ from private_attribute import PrivateAttrBase
15
+
16
+
17
+ def _collect():
18
+ for _ in range(3):
19
+ gc.collect()
20
+
21
+
22
+ class TestSlotsCycleCollect(unittest.TestCase):
23
+ def test_slots_pair_cycle_collected(self):
24
+ class A(PrivateAttrBase):
25
+ __private_attrs__ = ("_x",)
26
+ __slots__ = ("peer", "__weakref__")
27
+
28
+ def __init__(self):
29
+ self._x = 1
30
+ self.peer = None
31
+
32
+ a = A()
33
+ b = A()
34
+ a.peer = b
35
+ b.peer = a
36
+ wa, wb = weakref.ref(a), weakref.ref(b)
37
+ del a, b
38
+ _collect()
39
+ self.assertIsNone(wa())
40
+ self.assertIsNone(wb())
41
+
42
+ def test_slots_self_cycle_collected(self):
43
+ class A(PrivateAttrBase):
44
+ __private_attrs__ = ("_x",)
45
+ __slots__ = ("self_ref", "__weakref__")
46
+
47
+ def __init__(self):
48
+ self._x = 1
49
+ self.self_ref = self
50
+
51
+ a = A()
52
+ w = weakref.ref(a)
53
+ del a
54
+ _collect()
55
+ self.assertIsNone(w())
56
+
57
+
58
+ if __name__ == "__main__":
59
+ unittest.main()