private-attribute-cpp 2.1.7__tar.gz → 2.1.9__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.7/private_attribute_cpp.egg-info → private_attribute_cpp-2.1.9}/PKG-INFO +1 -1
  2. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9}/private_attribute.cpp +129 -1
  3. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9}/private_attribute.pyi +15 -4
  4. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9/private_attribute_cpp.egg-info}/PKG-INFO +1 -1
  5. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9}/private_attribute_cpp.egg-info/SOURCES.txt +2 -1
  6. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9}/setup.py +9 -3
  7. private_attribute_cpp-2.1.9/tests/test_slots_cycle.py +59 -0
  8. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9}/LICENSE +0 -0
  9. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9}/MANIFEST.in +0 -0
  10. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9}/README.md +0 -0
  11. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9}/picosha2.h +0 -0
  12. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9}/private_attribute_cpp.egg-info/dependency_links.txt +0 -0
  13. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9}/private_attribute_cpp.egg-info/not-zip-safe +0 -0
  14. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9}/private_attribute_cpp.egg-info/top_level.txt +0 -0
  15. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9}/setup.cfg +0 -0
  16. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9}/tests/test_attr_classify_owner.py +0 -0
  17. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9}/tests/test_class_private_attrs.py +0 -0
  18. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9}/tests/test_gc_collect.py +0 -0
  19. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9}/tests/test_private_abc.py +0 -0
  20. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9}/tests/test_private_attrs.py +0 -0
  21. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9}/tests/test_private_func.py +0 -0
  22. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9}/tests/test_private_inheritance.py +0 -0
  23. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9}/tests/test_private_method.py +0 -0
  24. {private_attribute_cpp-2.1.7 → private_attribute_cpp-2.1.9}/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.7
3
+ Version: 2.1.9
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
@@ -3867,7 +3995,7 @@ PrivateModule_get_PrivateAttrBase(PyObject* /*self*/, void* /*closure*/) noexcep
3867
3995
  }
3868
3996
 
3869
3997
  static int
3870
- PyList_AppendString(PyObject* list, const char* str)
3998
+ PyList_AppendString(PyObject* list, const char* str) noexcept
3871
3999
  {
3872
4000
  PyObject* obj = PyUnicode_InternFromString(str);
3873
4001
  if (!obj) return -1;
@@ -15,14 +15,14 @@ class MyClass(PrivateAttrBase):
15
15
  return self._private_attr1
16
16
  ```
17
17
  """
18
- from typing import Any, TypeVar, Callable, TypedDict, Sequence, Generic, Optional
18
+ from typing import Any, TypeVar, Callable, TypedDict, Sequence, Generic, Optional, cast
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
23
  __private_attrs__: Optional[Sequence[str]]
24
24
 
25
- _T = TypeVar('T')
25
+ _T = TypeVar('_T')
26
26
 
27
27
  class _PrivateWrap(Generic[_T]):
28
28
  @property
@@ -38,9 +38,20 @@ class _PrivateWrap(Generic[_T]):
38
38
  def __getattr__(self, name: str) -> Any:
39
39
  return getattr(self.result, name)
40
40
 
41
- _TVar = TypeVar('TVar')
41
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
42
+ if callable(self.result):
43
+ return self.result(*args, **kwargs)
44
+ raise TypeError(f'{self.result!r} is not callable')
42
45
 
43
- class PrivateWrapProxy:
46
+ def __get__(self, instance: Any, owner: type) -> Any:
47
+ if hasattr(self.result, "__get__"):
48
+ descriptor = cast(Any, self.result)
49
+ return descriptor.__get__(instance, owner)
50
+ return self.result
51
+
52
+ _TVar = TypeVar('_TVar')
53
+
54
+ class PrivateWrapProxy(Generic[_TVar, _T]):
44
55
  """
45
56
  PrivateWrapProxy is a proxy for private attributes.
46
57
  Usage:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: private_attribute_cpp
3
- Version: 2.1.7
3
+ Version: 2.1.9
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.7',
27
+ version='2.1.9',
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()