Skip to content

Commit 014c2b5

Browse files
committed
fix bug with wrappers
Before, we replaced the class of a wrapper after its creation. This turned out the be problematic with the GC in python 3.13.
1 parent 344b45e commit 014c2b5

11 files changed

Lines changed: 256 additions & 95 deletions

python/deps/untypy/test/impl/test_simple.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def test_attributes(self):
5959
def m(x: A) -> None:
6060
self.assertEqual(x.foo, 42)
6161
x.foo = 43
62+
self.assertEqual(x.foo, 43)
6263

6364
m(a)
6465
self.assertEqual(a.foo, 43)

python/deps/untypy/untypy/util/wrapper.py

Lines changed: 103 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,12 @@
33
import collections
44
from untypy.error import UntypyError
55
from untypy.util.debug import debug
6+
import types
67

78
def _f():
89
yield 0
910
generatorType = type(_f())
1011

11-
class WyppWrapError(Exception):
12-
pass
13-
14-
def _readonly(self, *args, **kwargs):
15-
raise RuntimeError("Cannot modify ReadOnlyDict")
16-
17-
class ReadOnlyDict(dict):
18-
__setitem__ = _readonly
19-
__delitem__ = _readonly
20-
pop = _readonly
21-
popitem = _readonly
22-
clear = _readonly
23-
update = _readonly
24-
setdefault = _readonly
25-
26-
def patch(self, ty, extra):
27-
# SW (2024-10-18): With python 3.13 there is the behavior that extra is modified after patching
28-
# the object. I never found out who is doing the modification. By wrapping extra with
29-
# ReadOnlyDict, everything works. Strangely, no error occurs somewhere.
30-
self.__extra__ = ReadOnlyDict(extra)
31-
w = self.__wrapped__
32-
m = None
33-
if hasattr(w, '__module__'):
34-
m = getattr(w, '__module__')
35-
ty.__module__ = m
36-
try:
37-
self.__class__ = ty
38-
except TypeError as e:
39-
raise WyppWrapError(f'Cannot wrap {self.__wrapped__} of type {type(self.__wrapped__)} ' \
40-
f'at type {ty}. Original error: {e}')
41-
4212
class WrapperBase:
4313
def __eq__(self, other):
4414
if hasattr(other, '__wrapped__'):
@@ -58,8 +28,6 @@ def __patch__(self, ms, name=None, extra=None):
5828
ty = type(name, (cls,), ms)
5929
patch(self, ty, extra)
6030
def __repr__(self):
61-
#w = self.__wrapped__
62-
#return f"Wrapper(addr=0x{id(self):09x}, wrapped_addr=0x{id(w):09x}, wrapped={repr(w)}"
6331
return repr(self.__wrapped__)
6432
def __str__(self):
6533
return str(self.__wrapped__)
@@ -72,27 +40,6 @@ def __reduce__(self): return self.__wrapped__.__reduce__()
7240
def __reduce_ex__(self): return self.__wrapped__.__reduce_ex__()
7341
def __sizeof__(self): return self.__wrapped__.__sizeof__()
7442

75-
class ObjectWrapper(WrapperBase):
76-
def __init__(self, baseObject):
77-
self.__dict__ = baseObject.__dict__
78-
self.__wrapped__ = baseObject
79-
def __patch__(self, ms, name=None, extra=None):
80-
if extra is None:
81-
extra = {}
82-
cls = self.__class__
83-
if name is None:
84-
name = cls.__name__
85-
wrappedCls = type(self.__wrapped__)
86-
ty = type(name, (wrappedCls, cls), ms)
87-
patch(self, ty, extra)
88-
89-
class ABCObjectWrapper(abc.ABC, ObjectWrapper):
90-
pass
91-
92-
# Superclasses in reverse order.
93-
class ABCObjectWrapperRev(ObjectWrapper, abc.ABC):
94-
pass
95-
9643
# A wrapper for list such that the class is a subclass of the builtin list class.
9744
class ListWrapper(WrapperBase, list): # important: inherit from WrapperBase first
9845
def __new__(cls, content):
@@ -224,33 +171,43 @@ def __new__(cls, content):
224171
self.__wrapped__ = content
225172
return self
226173

227-
# These methods are not delegated to the wrapped object
228-
_blacklist = [
229-
'__class__', '__delattr__', '__dict__', '__dir__', '__doc__',
230-
'__getattribute__', '__get_attr_', '__init_subclass__'
231-
'__init__', '__new__', '__del__', '__repr__', '__setattr__', '__str__',
232-
'__hash__', '__eq__', '__patch__',
233-
'__class_getitem__', '__subclasshook__',
234-
'__firstlineno__', '__static_attributes__']
174+
class WyppWrapError(Exception):
175+
pass
176+
177+
def _readonly(self, *args, **kwargs):
178+
raise RuntimeError("Cannot modify ReadOnlyDict")
235179

236-
_extra = ['__next__']
180+
class ReadOnlyDict(dict):
181+
__setitem__ = _readonly
182+
__delitem__ = _readonly
183+
pop = _readonly
184+
popitem = _readonly
185+
clear = _readonly
186+
update = _readonly
187+
setdefault = _readonly
188+
189+
def patch(self, ty, extra):
190+
# SW (2024-10-18): With python 3.13 there is the behavior that extra is modified after patching
191+
# the object. I never found out who is doing the modification. By wrapping extra with
192+
# ReadOnlyDict, everything works. Strangely, no error occurs somewhere.
193+
self.__extra__ = ReadOnlyDict(extra)
194+
w = self.__wrapped__
195+
m = None
196+
if hasattr(w, '__module__'):
197+
m = getattr(w, '__module__')
198+
ty.__module__ = m
199+
try:
200+
self.__class__ = ty
201+
except TypeError as e:
202+
raise WyppWrapError(f'Cannot wrap {self.__wrapped__} of type {type(self.__wrapped__)} ' \
203+
f'at type {ty}. Original error: {e}')
237204

238205
# SimpleWrapper is a fallback for types that cannot be used as base types
239206
class SimpleWrapper(WrapperBase):
240-
def __init__(self, baseObject):
241-
self.__wrapped__ = baseObject
207+
def __init__(self):
208+
pass
242209
def __patch__(self, ms, name=None, extra=None):
243-
if extra is None:
244-
extra = {}
245-
cls = self.__class__
246-
if name is None:
247-
name = cls.__name__
248-
baseObject = self.__wrapped__
249-
for x in dir(baseObject) + _extra:
250-
if x not in ms and x not in _blacklist and hasattr(baseObject, x):
251-
ms[x] = getattr(baseObject, x)
252-
ty = type(name, (cls,), ms) #
253-
patch(self, ty, extra)
210+
pass
254211

255212
class ValuesViewWrapper(SimpleWrapper):
256213
pass
@@ -264,46 +221,99 @@ class KeysViewWrapper(SimpleWrapper):
264221
pass
265222
collections.abc.KeysView.register(KeysViewWrapper)
266223

224+
def wrapSimple(wrapped, methods, name, extra, cls=SimpleWrapper):
225+
if name is None:
226+
name = cls.__name__
227+
if extra is None:
228+
extra = {}
229+
for x in ['__next__', '__iter__']:
230+
if x not in methods and hasattr(wrapped, x):
231+
attr = getattr(wrapped, x)
232+
methods[x] = attr
233+
# Dynamically create a new class:
234+
# type(class_name, base_classes, class_dict)
235+
WrapperClass = type(
236+
name,
237+
(cls,),
238+
methods
239+
)
240+
if not name.startswith('WyppTypeCheck()') and hasattr(wrapped, '__module__'):
241+
WrapperClass.__module__ = getattr(wrapped, '__module__')
242+
w = WrapperClass()
243+
w.__wrapped__ = wrapped
244+
w.__extra__ = extra
245+
return w
246+
247+
def wrapObj(wrapped, methods, name, extra):
248+
class BaseWrapper(WrapperBase, wrapped.__class__):
249+
def __init__(self):
250+
self.__dict__ = wrapped.__dict__
251+
self.__wrapped__ = wrapped
252+
def __patch__(self, ms, name=None, extra=None):
253+
pass
254+
if name is None:
255+
name = 'ObjectWrapper'
256+
if extra is None:
257+
extra = {}
258+
# Dynamically create a new class:
259+
# type(class_name, base_classes, class_dict)
260+
WrapperClass = type(
261+
name,
262+
(BaseWrapper,),
263+
methods
264+
)
265+
if hasattr(wrapped, '__module__'):
266+
WrapperClass.__module__ = getattr(wrapped, '__module__')
267+
w = WrapperClass()
268+
w.__extra__ = extra
269+
return w
270+
267271
def wrap(obj, methods, name=None, extra=None, simple=False):
268272
if extra is None:
269273
extra = {}
274+
wrapper = None
270275
if simple:
271-
w = SimpleWrapper(obj)
276+
w = wrapSimple(obj, methods, name, extra)
277+
wrapper = 'SimpleWrapper'
272278
elif isinstance(obj, list):
273279
w = ListWrapper(obj)
280+
wrapper = 'ListWrapper'
274281
elif isinstance(obj, tuple):
275282
w = TupleWrapper(obj)
283+
wrapper = 'TupleWrapper'
276284
elif isinstance(obj, dict):
277285
w = DictWrapper(obj)
286+
wrapper = 'DictWrapper'
278287
elif isinstance(obj, str):
279288
w = StringWrapper(obj)
289+
wrapper = 'StringWrapper'
280290
elif isinstance(obj, set):
281291
w = SetWrapper(obj)
292+
wrapper = 'SetWrapper'
282293
elif isinstance(obj, collections.abc.ValuesView):
283-
w = ValuesViewWrapper(obj)
294+
w = wrapSimple(obj, methods, name, extra, ValuesViewWrapper)
295+
wrapper = 'ValuesViewWrapper'
284296
elif isinstance(obj, collections.abc.KeysView):
285-
w = KeysViewWrapper(obj)
297+
w = wrapSimple(obj, methods, name, extra, KeysViewWrapper)
298+
wrapper = 'KeysViewWrapper'
286299
elif isinstance(obj, collections.abc.ItemsView):
287-
w = ItemsViewWrapper(obj)
300+
w = wrapSimple(obj, methods, name, extra, ItemsViewWrapper)
301+
wrapper = 'ItemsViewWrapper'
288302
elif isinstance(obj, typing.Generic):
289-
w = SimpleWrapper(obj)
303+
w = wrapSimple(obj, methods, name, extra)
304+
wrapper = 'SimpleWrapper'
290305
elif isinstance(obj, generatorType):
291-
w = SimpleWrapper(obj)
292-
elif isinstance(obj, abc.ABC) and hasattr(obj, '__dict__'):
293-
try:
294-
w = ABCObjectWrapper(obj)
295-
except WyppWrapError:
296-
try:
297-
w = ABCObjectWrapperRev(obj)
298-
except WyppWrapError:
299-
w = SimpleWrapper(obj)
306+
w = wrapSimple(obj, methods, name, extra)
307+
wrapper = 'SimpleWrapper'
300308
elif hasattr(obj, '__dict__'):
301-
w = ObjectWrapper(obj)
309+
w = wrapObj(obj, methods, name, extra)
310+
wrapper = 'ObjectWrapper'
302311
else:
303-
w = SimpleWrapper(obj)
312+
w = wrapSimple(obj, methods, name, extra)
313+
wrapper = 'SimpleWrapper'
304314
w.__patch__(methods, name, extra)
305315
wname = name
306316
if wname is None:
307317
wname = str(type(w))
308-
debug(f"Wrapping {obj} at 0x{id(obj):09x} as {wname}, simple={simple}, wrapper=0x{id(w):09x}")
318+
debug(f"Wrapping {obj} at 0x{id(obj):09x} as {wname}, simple={simple}, wrapper=0x{id(w):09x} ({wrapper})")
309319
return w

python/fileTests

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,8 @@ checkWithOutputAux yes 0 test-data/testForwardTypeInRecord.py
289289
checkWithOutputAux yes 0 test-data/testForwardTypeInRecord2.py
290290
checkWithOutputAux yes 0 test-data/testUnionOfUnion.py
291291
checkWithOutputAux yes 1 test-data/testRecordTypes.py
292+
checkWithOutputAux yes 0 test-data/testDisappearingObject_01.py
293+
checkWithOutputAux yes 0 test-data/testDisappearingObject_02.py
292294

293295
function is_min_version()
294296
{

python/run

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
#!/bin/bash
22

3+
PY=python3.13
34
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
45

56
OPTS="--quiet"
67
# OPTS="--verbose"
78

8-
PYTHONPATH="$SCRIPT_DIR"/site-lib:"$PYTHONPATH" python3 "$SCRIPT_DIR"/src/runYourProgram.py \
9+
PYTHONPATH="$SCRIPT_DIR"/site-lib:"$PYTHONPATH" $PY "$SCRIPT_DIR"/src/runYourProgram.py \
910
--no-clear $OPTS "$@"
1011
exit $?

python/test-data/testDisappearingObject_01.err

Whitespace-only changes.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
['subdir']
2+
Directory('stefan')
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# This test comes from a bug reported by a student, 2025-05-8
2+
from __future__ import annotations
3+
from wypp import *
4+
import gc
5+
6+
class FileSystemEntry:
7+
def __init__(self, name: str):
8+
self.name = name
9+
def getName(self) -> str:
10+
return self.name
11+
def getContent(self) -> str:
12+
raise Exception('No content')
13+
def getChildren(self) -> list[FileSystemEntry]:
14+
return []
15+
def findChild(self, name: str) -> FileSystemEntry:
16+
for c in self.getChildren():
17+
if c.getName() == name:
18+
return c
19+
raise Exception('No child with name ' + name)
20+
def addChild(self, child: FileSystemEntry) -> None:
21+
raise Exception('Cannot add child to ' + repr(self))
22+
23+
class Directory(FileSystemEntry):
24+
def __init__(self, name: str, children: list[FileSystemEntry] = []):
25+
super().__init__(name)
26+
self.__children = children
27+
def getChildren(self) -> list[FileSystemEntry]:
28+
return self.__children
29+
def addChild(self, child: FileSystemEntry):
30+
self.__children.append(child)
31+
def __repr__(self):
32+
return 'Directory(' + repr(self.getName()) + ')'
33+
34+
class File:
35+
def __init__(self, name: str, content: str):
36+
raise ValueError()
37+
super().__init__(name)
38+
self.content = content
39+
def getContent(self) -> str:
40+
return self.content
41+
42+
class Link(FileSystemEntry):
43+
def __init__(self, name: str, linkTarget: FileSystemEntry):
44+
super().__init__(name)
45+
self.__linkTarget = linkTarget
46+
def getChildren(self) -> list[FileSystemEntry]:
47+
return self.__linkTarget.getChildren()
48+
def getContent(self) -> str:
49+
return self.__linkTarget.getContent()
50+
def addChild(self, child: FileSystemEntry):
51+
self.__linkTarget.addChild(child)
52+
def __repr__(self):
53+
return ('Link(' + repr(self.getName()) + ' -> ' +
54+
repr(self.__linkTarget) + ')')
55+
def getLinkTarget(self) -> FileSystemEntry:
56+
return self.__linkTarget
57+
58+
class CryptoFile:
59+
def __init__(self, name: str, content: str):
60+
raise ValueError()
61+
super().__init__(name)
62+
self.content = content
63+
def getContent(self) -> str:
64+
return 'CRYPTO_' + 'X'*len(self.content)
65+
def __repr__(self):
66+
return 'CryptoFile(' + repr(self.getName())
67+
68+
def test_link():
69+
stefan = Directory('stefan', [])
70+
wehr = Link('wehr', stefan)
71+
l1 = [x.getName() for x in wehr.getChildren()]
72+
wehr.addChild(Directory('subdir', []))
73+
l2 = [x.getName() for x in wehr.getChildren()]
74+
print(l2)
75+
print(stefan)
76+
77+
test_link()

python/test-data/testDisappearingObject_02.err

Whitespace-only changes.

python/test-data/testDisappearingObject_02.out

Whitespace-only changes.

0 commit comments

Comments
 (0)