private-attribute-cpp 2.1.1__tar.gz → 2.1.3__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 (22) hide show
  1. {private_attribute_cpp-2.1.1/private_attribute_cpp.egg-info → private_attribute_cpp-2.1.3}/PKG-INFO +1 -1
  2. {private_attribute_cpp-2.1.1 → private_attribute_cpp-2.1.3}/private_attribute.cpp +111 -18
  3. {private_attribute_cpp-2.1.1 → private_attribute_cpp-2.1.3/private_attribute_cpp.egg-info}/PKG-INFO +1 -1
  4. {private_attribute_cpp-2.1.1 → private_attribute_cpp-2.1.3}/private_attribute_cpp.egg-info/SOURCES.txt +1 -0
  5. {private_attribute_cpp-2.1.1 → private_attribute_cpp-2.1.3}/setup.py +1 -1
  6. private_attribute_cpp-2.1.3/tests/test_attr_classify_owner.py +137 -0
  7. {private_attribute_cpp-2.1.1 → private_attribute_cpp-2.1.3}/LICENSE +0 -0
  8. {private_attribute_cpp-2.1.1 → private_attribute_cpp-2.1.3}/MANIFEST.in +0 -0
  9. {private_attribute_cpp-2.1.1 → private_attribute_cpp-2.1.3}/README.md +0 -0
  10. {private_attribute_cpp-2.1.1 → private_attribute_cpp-2.1.3}/picosha2.h +0 -0
  11. {private_attribute_cpp-2.1.1 → private_attribute_cpp-2.1.3}/private_attribute.pyi +0 -0
  12. {private_attribute_cpp-2.1.1 → private_attribute_cpp-2.1.3}/private_attribute_cpp.egg-info/dependency_links.txt +0 -0
  13. {private_attribute_cpp-2.1.1 → private_attribute_cpp-2.1.3}/private_attribute_cpp.egg-info/not-zip-safe +0 -0
  14. {private_attribute_cpp-2.1.1 → private_attribute_cpp-2.1.3}/private_attribute_cpp.egg-info/top_level.txt +0 -0
  15. {private_attribute_cpp-2.1.1 → private_attribute_cpp-2.1.3}/setup.cfg +0 -0
  16. {private_attribute_cpp-2.1.1 → private_attribute_cpp-2.1.3}/tests/test_class_private_attrs.py +0 -0
  17. {private_attribute_cpp-2.1.1 → private_attribute_cpp-2.1.3}/tests/test_private_abc.py +0 -0
  18. {private_attribute_cpp-2.1.1 → private_attribute_cpp-2.1.3}/tests/test_private_attrs.py +0 -0
  19. {private_attribute_cpp-2.1.1 → private_attribute_cpp-2.1.3}/tests/test_private_func.py +0 -0
  20. {private_attribute_cpp-2.1.1 → private_attribute_cpp-2.1.3}/tests/test_private_inheritance.py +0 -0
  21. {private_attribute_cpp-2.1.1 → private_attribute_cpp-2.1.3}/tests/test_private_method.py +0 -0
  22. {private_attribute_cpp-2.1.1 → private_attribute_cpp-2.1.3}/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.1
3
+ Version: 2.1.3
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
@@ -125,6 +125,10 @@ public:
125
125
  PyObjectStorage(PyObject* obj) noexcept : obj(obj) {
126
126
  Py_XINCREF(obj);
127
127
  }
128
+ // support for PyCodeObject*
129
+ PyObjectStorage(PyCodeObject* obj) noexcept : obj(reinterpret_cast<PyObject*>(obj)) {
130
+ Py_XINCREF(obj);
131
+ }
128
132
  PyObjectStorage(const PyObjectStorage& other) noexcept : obj(other.obj) {
129
133
  Py_XINCREF(obj);
130
134
  }
@@ -155,13 +159,26 @@ public:
155
159
  obj = new_obj;
156
160
  return *this;
157
161
  }
158
- ~PyObjectStorage() noexcept {
162
+ PyObjectStorage& operator=(PyCodeObject* new_obj) noexcept {
163
+ Py_XINCREF(reinterpret_cast<PyObject*>(new_obj));
159
164
  Py_XDECREF(obj);
165
+ obj = reinterpret_cast<PyObject*>(new_obj);
166
+ return *this;
167
+ }
168
+ ~PyObjectStorage() noexcept {
169
+ // if python has exit we don't do anything
170
+ if (Py_IsInitialized()) {
171
+ Py_XDECREF(obj);
172
+ }
160
173
  }
161
174
 
162
175
  operator PyObject*() const noexcept {
163
176
  return obj;
164
177
  }
178
+
179
+ operator PyCodeObject*() const noexcept {
180
+ return reinterpret_cast<PyCodeObject*>(obj);
181
+ }
165
182
  PyObject* get() const noexcept {
166
183
  return obj;
167
184
  }
@@ -176,9 +193,9 @@ namespace {
176
193
  namespace {
177
194
  static std::unordered_map<uintptr_t, std::unordered_map<std::string, PyObjectStorage>> type_attr_dict;
178
195
  };
179
- static std::unordered_map<uintptr_t, std::unordered_map<uintptr_t, PyCodeObject*>> type_allowed_code_map;
196
+ static std::unordered_map<uintptr_t, std::unordered_map<uintptr_t, PyObjectStorage>> type_allowed_code_map;
180
197
  static std::unordered_map<uintptr_t, std::shared_ptr<std::shared_mutex>> all_type_mutex;
181
- static std::unordered_map<uintptr_t, PyObject*> type_need_call;
198
+ static std::unordered_map<uintptr_t, PyObjectStorage> type_need_call;
182
199
  static std::unordered_map<uintptr_t, std::unordered_set<TwoStringTuple>> all_type_attr_set;
183
200
  namespace {
184
201
  static std::unordered_map<uintptr_t, std::unordered_map<uintptr_t,
@@ -206,9 +223,34 @@ namespace {
206
223
  static std::unordered_map<uintptr_t, destructor> all_type_finalize;
207
224
 
208
225
  static std::shared_mutex all_register_new_metaclass_mutex;
209
- static std::unordered_map<uintptr_t, PyObject*> all_register_type_weak_ref;
226
+ static std::unordered_map<uintptr_t, PyObjectStorage> all_register_type_weak_ref;
210
227
  };
211
228
  };
229
+
230
+ static void
231
+ clean_all_storages() noexcept
232
+ {
233
+ AllData::cache.clear();
234
+ AllData::all_exist_name.clear();
235
+ AllData::obj_attr_keys.clear();
236
+ AllData::type_attr_dict.clear();
237
+ AllData::type_allowed_code_map.clear();
238
+ AllData::all_type_mutex.clear();
239
+ AllData::type_need_call.clear();
240
+ AllData::all_type_attr_set.clear();
241
+ AllData::all_object_attr.clear();
242
+ AllData::all_type_subclass_attr.clear();
243
+ AllData::all_object_mutex.clear();
244
+ AllData::all_type_subclass_mutex.clear();
245
+ AllData::all_type_parent_id.clear();
246
+ AllData::all_type_getattro.clear();
247
+ AllData::all_type_setattro.clear();
248
+ AllData::all_type_traverse.clear();
249
+ AllData::all_type_clear.clear();
250
+ AllData::all_type_finalize.clear();
251
+ AllData::all_register_type_weak_ref.clear();
252
+ }
253
+
212
254
  struct FinalObject
213
255
  {
214
256
  PyObjectStorage result;
@@ -275,11 +317,11 @@ attr_classify(uintptr_t typ_id, const std::string& name, PyCodeObject* code) noe
275
317
  chain.insert(chain.end(), parent_ids.begin(), parent_ids.end());
276
318
  }
277
319
 
278
- // Find the class whose code is currently running (the code owner).
320
+ // Find the class whose code is currently running (the code owner, parent first).
279
321
  uintptr_t code_class = 0;
280
- for (auto& node : chain) {
281
- if (is_class_code(node, code)) {
282
- code_class = node;
322
+ for (auto it = chain.rbegin(); it != chain.rend(); ++it) {
323
+ if (is_class_code(*it, code)) {
324
+ code_class = *it; // first hit from the base-most end
283
325
  break;
284
326
  }
285
327
  }
@@ -2059,7 +2101,7 @@ try_get_attr_string(PyObject* obj, const char* name) noexcept
2059
2101
  }
2060
2102
 
2061
2103
  static void
2062
- analyse_all_code(PyObject* obj, std::unordered_map<uintptr_t, PyCodeObject*>& map, std::unordered_set<uintptr_t>& _seen) noexcept
2104
+ analyse_all_code(PyObject* obj, std::unordered_map<uintptr_t, PyObjectStorage>& map, std::unordered_set<uintptr_t>& _seen) noexcept
2063
2105
  {
2064
2106
  uintptr_t obj_id = (uintptr_t)obj;
2065
2107
  if (_seen.find(obj_id) != _seen.end()) {
@@ -2067,8 +2109,7 @@ analyse_all_code(PyObject* obj, std::unordered_map<uintptr_t, PyCodeObject*>& ma
2067
2109
  }
2068
2110
  _seen.insert(obj_id);
2069
2111
  if (PyObject_TypeCheck(obj, &PyCode_Type)) {
2070
- Py_INCREF(obj);
2071
- map[(uintptr_t)obj] = (PyCodeObject*)obj;
2112
+ map[(uintptr_t)obj] = obj;
2072
2113
  PyObject* co_contain = try_get_attr_string(obj, "co_consts");
2073
2114
  if (co_contain && PySequence_Check(co_contain)) {
2074
2115
  Py_ssize_t len = PySequence_Length(co_contain);
@@ -2229,11 +2270,11 @@ need_analyse_type(PyObject* type) noexcept
2229
2270
  }
2230
2271
  std::shared_lock lock(::AllData::all_register_new_metaclass_mutex);
2231
2272
  for (auto& [id, metaclassref]: ::AllData::all_register_type_weak_ref){
2232
- if (!PyWeakref_CheckRef(metaclassref)) {
2273
+ if (!PyWeakref_CheckRef((PyObject*)metaclassref)) {
2233
2274
  continue;
2234
2275
  }
2235
2276
  #if PY_VERSION_HEX < 0x030D0000
2236
- PyObject* metaclass = PyWeakref_GET_OBJECT(metaclassref);
2277
+ PyObject* metaclass = PyWeakref_GET_OBJECT((PyObject*)metaclassref);
2237
2278
  if (type == metaclass) continue;
2238
2279
  if (PyObject_IsInstance(type, metaclass)) {
2239
2280
  return true;
@@ -3218,13 +3259,9 @@ PrivateAttrType_finalize(PyObject* cls) noexcept
3218
3259
  ::AllData::all_type_attr_set.erase(typ_id);
3219
3260
  }
3220
3261
  if (::AllData::type_allowed_code_map.find(typ_id) != ::AllData::type_allowed_code_map.end()) {
3221
- for (auto& [id, obj] : ::AllData::type_allowed_code_map[typ_id]) {
3222
- Py_XDECREF(obj);
3223
- }
3224
3262
  ::AllData::type_allowed_code_map.erase(typ_id);
3225
3263
  }
3226
3264
  if (::AllData::type_need_call.find(typ_id) != ::AllData::type_need_call.end()) {
3227
- Py_DECREF(::AllData::type_need_call[typ_id]);
3228
3265
  ::AllData::type_need_call.erase(typ_id);
3229
3266
  }
3230
3267
  if (::AllData::type_attr_dict.find(typ_id) != ::AllData::type_attr_dict.end()) {
@@ -3534,7 +3571,7 @@ is_class_of_registed_type(PyObject* metaclass) noexcept
3534
3571
  continue;
3535
3572
  }
3536
3573
  #if PY_VERSION_HEX < 0x030D0000
3537
- PyObject* instance = PyWeakref_GET_OBJECT(instanceref);
3574
+ PyObject* instance = PyWeakref_GET_OBJECT((PyObject*)instanceref);
3538
3575
  if (instance == metaclass) continue;
3539
3576
  if (PyObject_IsInstance(instance, metaclass)) {
3540
3577
  return true;
@@ -4026,10 +4063,66 @@ static PyModuleDef def = {
4026
4063
  NULL
4027
4064
  };
4028
4065
 
4066
+ static PyObject*
4067
+ AtExit_clean_storage(PyObject* /*self*/, PyObject* /*obj*/) noexcept
4068
+ {
4069
+ clean_all_storages();
4070
+ Py_RETURN_NONE;
4071
+ }
4072
+
4073
+ static PyMethodDef clean_storage = {
4074
+ "clean_storage",
4075
+ (PyCFunction)AtExit_clean_storage,
4076
+ METH_NOARGS,
4077
+ "Clean all storages"
4078
+ };
4079
+
4080
+ static int
4081
+ atexit_register_clean_func(void) noexcept
4082
+ {
4083
+ PyObject* atexit = PyImport_ImportModule("atexit");
4084
+ if (!atexit) {
4085
+ return -1;
4086
+ }
4087
+ PyObject* register_func = PyObject_GetAttrString(atexit, "register");
4088
+ if (!register_func) {
4089
+ Py_DECREF(atexit);
4090
+ return -1;
4091
+ }
4092
+
4093
+ PyObject* func = PyCFunction_New(&clean_storage, NULL);
4094
+ if (!func) {
4095
+ Py_DECREF(register_func);
4096
+ Py_DECREF(atexit);
4097
+ return -1;
4098
+ }
4099
+ PyObject* args = PyTuple_New(1);
4100
+ if (!args) {
4101
+ Py_DECREF(func);
4102
+ Py_DECREF(register_func);
4103
+ Py_DECREF(atexit);
4104
+ return -1;
4105
+ }
4106
+ PyTuple_SET_ITEM(args, 0, func);
4107
+ PyObject* result = PyObject_CallObject(register_func, args);
4108
+ if (result == NULL) {
4109
+ Py_DECREF(args);
4110
+ Py_DECREF(register_func);
4111
+ Py_DECREF(atexit);
4112
+ return -1;
4113
+ }
4114
+ Py_DECREF(args);
4115
+ Py_DECREF(register_func);
4116
+ Py_DECREF(atexit);
4117
+ Py_DECREF(result);
4118
+ return 0;
4119
+ }
4120
+
4029
4121
  PyMODINIT_FUNC
4030
4122
  PyInit_private_attribute(void) noexcept
4031
4123
  {
4032
4124
  if (init_all_slots() <0 ||
4125
+ atexit_register_clean_func() < 0 ||
4033
4126
  PyType_Ready(&PrivateWrapType) < 0 ||
4034
4127
  PyType_Ready(&PrivateWrapProxyType) < 0 ||
4035
4128
  PyType_Ready(&PrivateAttrType) < 0 ||
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: private_attribute_cpp
3
- Version: 2.1.1
3
+ Version: 2.1.3
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
@@ -10,6 +10,7 @@ private_attribute_cpp.egg-info/SOURCES.txt
10
10
  private_attribute_cpp.egg-info/dependency_links.txt
11
11
  private_attribute_cpp.egg-info/not-zip-safe
12
12
  private_attribute_cpp.egg-info/top_level.txt
13
+ tests/test_attr_classify_owner.py
13
14
  tests/test_class_private_attrs.py
14
15
  tests/test_private_abc.py
15
16
  tests/test_private_attrs.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.1',
21
+ version='2.1.3',
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,137 @@
1
+ """attr_classify ownership rule (v2.1.3).
2
+
3
+ When a code object is owned by several classes on the MRO chain (e.g. a
4
+ subclass aliases a parent method with `f = g`, or copies a parent method's
5
+ bytecode with `f.__code__ = g.__code__`), attr_classify must attribute the
6
+ code to the base-most (declaring, parent) class. Otherwise the parent code's
7
+ private attribute accesses get split across two storages: the tampered method
8
+ writes the subclass's storage while the untampered parent method reads the
9
+ parent's storage, corrupting the parent's behavior.
10
+
11
+ Key invariant: parent methods inc() (tampered by the child) and peek() (not
12
+ tampered) must ALWAYS see the same storage on a child instance.
13
+ """
14
+ import unittest
15
+
16
+ import private_attribute
17
+
18
+
19
+ class Parent(private_attribute.PrivateAttrBase):
20
+ __private_attrs__ = ["_n"]
21
+ _n = 0
22
+
23
+ def inc(self):
24
+ self._n += 1
25
+ return self._n
26
+
27
+ def peek(self):
28
+ return self._n
29
+
30
+
31
+ class TestAliasCodeOwnership(unittest.TestCase):
32
+ """`f = g` in the class body registers the same function object with both
33
+ classes; the aliased and the untampered parent methods must stay
34
+ consistent on a child instance."""
35
+
36
+ def test_alias_method_sees_same_storage_as_untampered(self):
37
+ class AliasChild(Parent):
38
+ __private_attrs__ = ["_n"]
39
+ _n = 100
40
+ f = Parent.inc # same function object in both class dicts
41
+
42
+ c = AliasChild()
43
+ v1 = c.f() # parent code, double-owned
44
+ v2 = c.peek() # parent code, single-owned
45
+ self.assertEqual(v1, v2)
46
+ c.f()
47
+ self.assertEqual(c.peek(), v2 + 1)
48
+ self.assertEqual(Parent().peek(), 0)
49
+
50
+ def test_alias_without_shadow_works(self):
51
+ class AliasChild2(Parent):
52
+ __private_attrs__ = []
53
+ f = Parent.inc
54
+
55
+ c = AliasChild2()
56
+ v1 = c.f()
57
+ self.assertEqual(v1, c.peek())
58
+
59
+
60
+ class TestCodeSwapOwnership(unittest.TestCase):
61
+ """`f.__code__ = g.__code__` inside the class body; the swapped method
62
+ must behave exactly like the parent's method (same storage)."""
63
+
64
+ def test_code_swap_sees_same_storage_as_untampered(self):
65
+ class CodeChild(Parent):
66
+ __private_attrs__ = ["_n"]
67
+ _n = 100
68
+
69
+ def f(self):
70
+ return None
71
+
72
+ f.__code__ = Parent.inc.__code__
73
+
74
+ c = CodeChild()
75
+ v1 = c.f()
76
+ v2 = c.peek()
77
+ self.assertEqual(v1, v2)
78
+ c.f()
79
+ self.assertEqual(c.peek(), v2 + 1)
80
+
81
+
82
+ class TestFallbackSemantics(unittest.TestCase):
83
+ """Per-subject fallback is preserved: an unwritten read resolves to the
84
+ subject's own class-level default, consistently for both methods."""
85
+
86
+ def test_unwritten_read_falls_back_per_subject(self):
87
+ class FreshChild(Parent):
88
+ __private_attrs__ = ["_n"]
89
+ _n = 100
90
+ f = Parent.inc
91
+
92
+ c = FreshChild()
93
+ self.assertEqual(c.peek(), 100)
94
+ v1 = c.f()
95
+ self.assertEqual(v1, c.peek())
96
+
97
+
98
+ class TestNormalUsageRegression(unittest.TestCase):
99
+ """Without tampering, nothing changes: child's own code uses child
100
+ storage, parent's code keeps its per-subject view."""
101
+
102
+ def test_child_own_code_uses_child_storage(self):
103
+ class NormChild(Parent):
104
+ __private_attrs__ = ["_n"]
105
+ _n = 100
106
+
107
+ def own_inc(self):
108
+ self._n += 1
109
+ return self._n
110
+
111
+ c = NormChild()
112
+ self.assertEqual(c.own_inc(), 101)
113
+ self.assertEqual(c.peek(), 100)
114
+ self.assertEqual(Parent().peek(), 0)
115
+
116
+
117
+ class TestIsolationIntact(unittest.TestCase):
118
+ """The v2.1.0 isolation rule still holds: the subclass's own code must
119
+ not touch a parent's private attribute."""
120
+
121
+ def test_subclass_own_code_denied(self):
122
+ class IsoParent(private_attribute.PrivateAttrBase):
123
+ __private_attrs__ = ["_secret"]
124
+ _secret = 1
125
+
126
+ class IsoChild(IsoParent):
127
+ __private_attrs__ = []
128
+
129
+ def read_secret(self):
130
+ return self._secret
131
+
132
+ with self.assertRaises(AttributeError):
133
+ IsoChild().read_secret()
134
+
135
+
136
+ if __name__ == "__main__":
137
+ unittest.main()