diff --git a/.github/workflows/github-action-test-js.yml b/.github/workflows/github-action-test-js.yml index dd83a9df..c368a083 100644 --- a/.github/workflows/github-action-test-js.yml +++ b/.github/workflows/github-action-test-js.yml @@ -17,9 +17,9 @@ jobs: node-version: [14.x, 16.x, 18.x, 20.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'npm' diff --git a/.github/workflows/github-action-test-python.yml b/.github/workflows/github-action-test-python.yml index 2d3f963b..db70dcc9 100644 --- a/.github/workflows/github-action-test-python.yml +++ b/.github/workflows/github-action-test-python.yml @@ -14,9 +14,9 @@ jobs: # You need to change to branch protection rules if you change the versions here python-version: [3.12.1, 3.13.0] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Test diff --git a/python/deps/untypy/test/impl/test_simple.py b/python/deps/untypy/test/impl/test_simple.py index 06cf04e4..6e70e37d 100644 --- a/python/deps/untypy/test/impl/test_simple.py +++ b/python/deps/untypy/test/impl/test_simple.py @@ -59,6 +59,7 @@ def test_attributes(self): def m(x: A) -> None: self.assertEqual(x.foo, 42) x.foo = 43 + self.assertEqual(x.foo, 43) m(a) self.assertEqual(a.foo, 43) diff --git a/python/deps/untypy/test/util_test/test_wrapper.py b/python/deps/untypy/test/util_test/test_wrapper.py index 04cc6edf..9d3d921d 100644 --- a/python/deps/untypy/test/util_test/test_wrapper.py +++ b/python/deps/untypy/test/util_test/test_wrapper.py @@ -77,12 +77,13 @@ def _test_api_complete(self, obj, ignore=[]): '__weakref__', '__wrapped__', '_DictWrapper__marker', '__setstate__', '__getstate__', '__firstlineno__', '__static_attributes__' ] + ignore - for x in dir(wrapped): - if x in blacklist: continue - m = getattr(wrapped, x) - if not hasattr(m, '__module__'): - self.fail(f'Attribute {x} not defined') - elif m.__module__ != expectedModule: + for x in dir(obj): + if x in blacklist: + continue + a = getattr(wrapped, x) + if not hasattr(a, '__module__'): + self.fail(f'Attribute {x} not defined. obj={obj}, a={a}, wrapped={wrapped}') + elif a.__module__ != expectedModule: self.fail(f'Attrribute {x} not defined in {expectedModule}') def test_list_api_complete(self): diff --git a/python/deps/untypy/untypy/util/wrapper.py b/python/deps/untypy/untypy/util/wrapper.py index 8d4eaa79..1ffe3eae 100644 --- a/python/deps/untypy/untypy/util/wrapper.py +++ b/python/deps/untypy/untypy/util/wrapper.py @@ -1,44 +1,11 @@ import typing -import abc import collections -from untypy.error import UntypyError from untypy.util.debug import debug def _f(): yield 0 generatorType = type(_f()) -class WyppWrapError(Exception): - pass - -def _readonly(self, *args, **kwargs): - raise RuntimeError("Cannot modify ReadOnlyDict") - -class ReadOnlyDict(dict): - __setitem__ = _readonly - __delitem__ = _readonly - pop = _readonly - popitem = _readonly - clear = _readonly - update = _readonly - setdefault = _readonly - -def patch(self, ty, extra): - # SW (2024-10-18): With python 3.13 there is the behavior that extra is modified after patching - # the object. I never found out who is doing the modification. By wrapping extra with - # ReadOnlyDict, everything works. Strangely, no error occurs somewhere. - self.__extra__ = ReadOnlyDict(extra) - w = self.__wrapped__ - m = None - if hasattr(w, '__module__'): - m = getattr(w, '__module__') - ty.__module__ = m - try: - self.__class__ = ty - except TypeError as e: - raise WyppWrapError(f'Cannot wrap {self.__wrapped__} of type {type(self.__wrapped__)} ' \ - f'at type {ty}. Original error: {e}') - class WrapperBase: def __eq__(self, other): if hasattr(other, '__wrapped__'): @@ -49,17 +16,7 @@ def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return hash(self.__wrapped__) - def __patch__(self, ms, name=None, extra=None): - if extra is None: - extra = {} - cls = self.__class__ - if name is None: - name = cls.__name__ - ty = type(name, (cls,), ms) - patch(self, ty, extra) def __repr__(self): - #w = self.__wrapped__ - #return f"Wrapper(addr=0x{id(self):09x}, wrapped_addr=0x{id(w):09x}, wrapped={repr(w)}" return repr(self.__wrapped__) def __str__(self): return str(self.__wrapped__) @@ -72,27 +29,6 @@ def __reduce__(self): return self.__wrapped__.__reduce__() def __reduce_ex__(self): return self.__wrapped__.__reduce_ex__() def __sizeof__(self): return self.__wrapped__.__sizeof__() -class ObjectWrapper(WrapperBase): - def __init__(self, baseObject): - self.__dict__ = baseObject.__dict__ - self.__wrapped__ = baseObject - def __patch__(self, ms, name=None, extra=None): - if extra is None: - extra = {} - cls = self.__class__ - if name is None: - name = cls.__name__ - wrappedCls = type(self.__wrapped__) - ty = type(name, (wrappedCls, cls), ms) - patch(self, ty, extra) - -class ABCObjectWrapper(abc.ABC, ObjectWrapper): - pass - -# Superclasses in reverse order. -class ABCObjectWrapperRev(ObjectWrapper, abc.ABC): - pass - # A wrapper for list such that the class is a subclass of the builtin list class. class ListWrapper(WrapperBase, list): # important: inherit from WrapperBase first def __new__(cls, content): @@ -224,33 +160,10 @@ def __new__(cls, content): self.__wrapped__ = content return self -# These methods are not delegated to the wrapped object -_blacklist = [ - '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', - '__getattribute__', '__get_attr_', '__init_subclass__' - '__init__', '__new__', '__del__', '__repr__', '__setattr__', '__str__', - '__hash__', '__eq__', '__patch__', - '__class_getitem__', '__subclasshook__', - '__firstlineno__', '__static_attributes__'] - -_extra = ['__next__'] - # SimpleWrapper is a fallback for types that cannot be used as base types class SimpleWrapper(WrapperBase): - def __init__(self, baseObject): - self.__wrapped__ = baseObject - def __patch__(self, ms, name=None, extra=None): - if extra is None: - extra = {} - cls = self.__class__ - if name is None: - name = cls.__name__ - baseObject = self.__wrapped__ - for x in dir(baseObject) + _extra: - if x not in ms and x not in _blacklist and hasattr(baseObject, x): - ms[x] = getattr(baseObject, x) - ty = type(name, (cls,), ms) # - patch(self, ty, extra) + def __init__(self, wrapped): + self.__wrapped__ = wrapped class ValuesViewWrapper(SimpleWrapper): pass @@ -264,46 +177,99 @@ class KeysViewWrapper(SimpleWrapper): pass collections.abc.KeysView.register(KeysViewWrapper) +def _wrap(wrapped, methods, mod, name, extra, cls): + if extra is None: + extra = {} + # Dynamically create a new class: + # type(class_name, base_classes, class_dict) + WrapperClass = type( + name, + (cls,), + methods + ) + WrapperClass.__module__ = mod + w = WrapperClass(wrapped) + w.__extra__ = extra + return w + +def wrapSimple(wrapped, methods, name, extra, cls=SimpleWrapper): + if name is None: + name = cls.__name__ + mod = None + else: + if hasattr(wrapped, '__module__'): + mod = wrapped.__module__ + else: + mod = None + for x in ['__next__', '__iter__']: + if x not in methods and hasattr(wrapped, x): + attr = getattr(wrapped, x) + methods[x] = attr + return _wrap(wrapped, methods, mod, name, extra, cls) + +def wrapObj(wrapped, methods, name, extra): + class BaseWrapper(WrapperBase, wrapped.__class__): + def __init__(self, wrapped): + self.__dict__ = wrapped.__dict__ + self.__wrapped__ = wrapped + if name is None: + name = 'ObjectWrapper' + if hasattr(wrapped, '__module__'): + mod = getattr(wrapped, '__module__') + else: + mod = None + return _wrap(wrapped, methods, mod, name, extra, BaseWrapper) + +def wrapBuiltin(wrapped, methods, name, extra, cls): + if name is None: + name = cls.__name__ + return _wrap(wrapped, methods, None, name, extra, cls) + def wrap(obj, methods, name=None, extra=None, simple=False): if extra is None: extra = {} + wrapper = None if simple: - w = SimpleWrapper(obj) + w = wrapSimple(obj, methods, name, extra) + wrapper = 'SimpleWrapper' elif isinstance(obj, list): - w = ListWrapper(obj) + w = wrapBuiltin(obj, methods, name, extra, ListWrapper) + wrapper = 'ListWrapper' elif isinstance(obj, tuple): - w = TupleWrapper(obj) + w = wrapBuiltin(obj, methods, name, extra, TupleWrapper) + wrapper = 'TupleWrapper' elif isinstance(obj, dict): - w = DictWrapper(obj) + w = wrapBuiltin(obj, methods, name, extra, DictWrapper) + wrapper = 'DictWrapper' elif isinstance(obj, str): - w = StringWrapper(obj) + w = wrapBuiltin(obj, methods, name, extra, StringWrapper) + wrapper = 'StringWrapper' elif isinstance(obj, set): - w = SetWrapper(obj) + w = wrapBuiltin(obj, methods, name, extra, SetWrapper) + wrapper = 'SetWrapper' elif isinstance(obj, collections.abc.ValuesView): - w = ValuesViewWrapper(obj) + w = wrapSimple(obj, methods, name, extra, ValuesViewWrapper) + wrapper = 'ValuesViewWrapper' elif isinstance(obj, collections.abc.KeysView): - w = KeysViewWrapper(obj) + w = wrapSimple(obj, methods, name, extra, KeysViewWrapper) + wrapper = 'KeysViewWrapper' elif isinstance(obj, collections.abc.ItemsView): - w = ItemsViewWrapper(obj) + w = wrapSimple(obj, methods, name, extra, ItemsViewWrapper) + wrapper = 'ItemsViewWrapper' elif isinstance(obj, typing.Generic): - w = SimpleWrapper(obj) + w = wrapSimple(obj, methods, name, extra) + wrapper = 'SimpleWrapper' elif isinstance(obj, generatorType): - w = SimpleWrapper(obj) - elif isinstance(obj, abc.ABC) and hasattr(obj, '__dict__'): - try: - w = ABCObjectWrapper(obj) - except WyppWrapError: - try: - w = ABCObjectWrapperRev(obj) - except WyppWrapError: - w = SimpleWrapper(obj) + w = wrapSimple(obj, methods, name, extra) + wrapper = 'SimpleWrapper' elif hasattr(obj, '__dict__'): - w = ObjectWrapper(obj) + w = wrapObj(obj, methods, name, extra) + wrapper = 'ObjectWrapper' else: - w = SimpleWrapper(obj) - w.__patch__(methods, name, extra) + w = wrapSimple(obj, methods, name, extra) + wrapper = 'SimpleWrapper' wname = name if wname is None: wname = str(type(w)) - debug(f"Wrapping {obj} at 0x{id(obj):09x} as {wname}, simple={simple}, wrapper=0x{id(w):09x}") + debug(f"Wrapping {obj} at 0x{id(obj):09x} as {wname}, simple={simple}, wrapper=0x{id(w):09x} ({wrapper})") return w diff --git a/python/fileTests b/python/fileTests index c88d21de..807f8523 100755 --- a/python/fileTests +++ b/python/fileTests @@ -289,6 +289,8 @@ checkWithOutputAux yes 0 test-data/testForwardTypeInRecord.py checkWithOutputAux yes 0 test-data/testForwardTypeInRecord2.py checkWithOutputAux yes 0 test-data/testUnionOfUnion.py checkWithOutputAux yes 1 test-data/testRecordTypes.py +checkWithOutputAux yes 0 test-data/testDisappearingObject_01.py +checkWithOutputAux yes 0 test-data/testDisappearingObject_02.py function is_min_version() { diff --git a/python/run b/python/run index 6eb6a50f..e61be5c5 100755 --- a/python/run +++ b/python/run @@ -1,10 +1,11 @@ #!/bin/bash +PY=python3.13 SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" OPTS="--quiet" # OPTS="--verbose" -PYTHONPATH="$SCRIPT_DIR"/site-lib:"$PYTHONPATH" python3 "$SCRIPT_DIR"/src/runYourProgram.py \ +PYTHONPATH="$SCRIPT_DIR"/site-lib:"$PYTHONPATH" $PY "$SCRIPT_DIR"/src/runYourProgram.py \ --no-clear $OPTS "$@" exit $? diff --git a/python/test-data/testDisappearingObject_01.err b/python/test-data/testDisappearingObject_01.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testDisappearingObject_01.out b/python/test-data/testDisappearingObject_01.out new file mode 100644 index 00000000..3252461b --- /dev/null +++ b/python/test-data/testDisappearingObject_01.out @@ -0,0 +1,2 @@ +['subdir'] +Directory('stefan') diff --git a/python/test-data/testDisappearingObject_01.py b/python/test-data/testDisappearingObject_01.py new file mode 100644 index 00000000..aabc05fa --- /dev/null +++ b/python/test-data/testDisappearingObject_01.py @@ -0,0 +1,77 @@ +# This test comes from a bug reported by a student, 2025-05-8 +from __future__ import annotations +from wypp import * +import gc + +class FileSystemEntry: + def __init__(self, name: str): + self.name = name + def getName(self) -> str: + return self.name + def getContent(self) -> str: + raise Exception('No content') + def getChildren(self) -> list[FileSystemEntry]: + return [] + def findChild(self, name: str) -> FileSystemEntry: + for c in self.getChildren(): + if c.getName() == name: + return c + raise Exception('No child with name ' + name) + def addChild(self, child: FileSystemEntry) -> None: + raise Exception('Cannot add child to ' + repr(self)) + +class Directory(FileSystemEntry): + def __init__(self, name: str, children: list[FileSystemEntry] = []): + super().__init__(name) + self.__children = children + def getChildren(self) -> list[FileSystemEntry]: + return self.__children + def addChild(self, child: FileSystemEntry): + self.__children.append(child) + def __repr__(self): + return 'Directory(' + repr(self.getName()) + ')' + +class File: + def __init__(self, name: str, content: str): + raise ValueError() + super().__init__(name) + self.content = content + def getContent(self) -> str: + return self.content + +class Link(FileSystemEntry): + def __init__(self, name: str, linkTarget: FileSystemEntry): + super().__init__(name) + self.__linkTarget = linkTarget + def getChildren(self) -> list[FileSystemEntry]: + return self.__linkTarget.getChildren() + def getContent(self) -> str: + return self.__linkTarget.getContent() + def addChild(self, child: FileSystemEntry): + self.__linkTarget.addChild(child) + def __repr__(self): + return ('Link(' + repr(self.getName()) + ' -> ' + + repr(self.__linkTarget) + ')') + def getLinkTarget(self) -> FileSystemEntry: + return self.__linkTarget + +class CryptoFile: + def __init__(self, name: str, content: str): + raise ValueError() + super().__init__(name) + self.content = content + def getContent(self) -> str: + return 'CRYPTO_' + 'X'*len(self.content) + def __repr__(self): + return 'CryptoFile(' + repr(self.getName()) + +def test_link(): + stefan = Directory('stefan', []) + wehr = Link('wehr', stefan) + l1 = [x.getName() for x in wehr.getChildren()] + wehr.addChild(Directory('subdir', [])) + l2 = [x.getName() for x in wehr.getChildren()] + print(l2) + print(stefan) + +test_link() diff --git a/python/test-data/testDisappearingObject_02.err b/python/test-data/testDisappearingObject_02.err new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testDisappearingObject_02.out b/python/test-data/testDisappearingObject_02.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testDisappearingObject_02.py b/python/test-data/testDisappearingObject_02.py new file mode 100644 index 00000000..97d8daca --- /dev/null +++ b/python/test-data/testDisappearingObject_02.py @@ -0,0 +1,68 @@ +# This test comes from a bug reported by a student, 2025-05-8 +from __future__ import annotations +from wypp import * +from abc import ABC, abstractmethod + +class FileSystemEntry(ABC): + def __init__(self, name: str): + self.__name = name + def getName(self) -> str: + return self.__name + def getContent(self) -> str: + raise Exception('No content') + def getChildren(self) -> list[FileSystemEntry]: + return [] + def findChild(self, name: str) -> FileSystemEntry: + for c in self.getChildren(): + if c.getName() == name: + return c + raise Exception('No child with name ' + name) + def addChild(self, child: FileSystemEntry) -> None: + raise Exception('Cannot add child to ' + repr(self)) + +class Directory(FileSystemEntry): + def __init__(self, name: str, children: list[FileSystemEntry] = []): + super().__init__(name) + self.__children = children[:] + def getChildren(self) -> list[FileSystemEntry]: + return self.__children[:] + def addChild(self, child: FileSystemEntry): + self.__children.append(child) + def __repr__(self): + return 'Directory(' + repr(self.getName()) + ')' + +class File(FileSystemEntry): + def __init__(self, name: str, content: str): + super().__init__(name) + self.__content = content + def getContent(self) -> str: + return self.__content + def __repr__(self): + return 'File(' + repr(self.getName()) + ')' + +class Link(FileSystemEntry): + def __init__(self, name: str, linkTarget: FileSystemEntry): + super().__init__(name) + self.__linkTarget = linkTarget + def getChildren(self) -> list[FileSystemEntry]: + return self.__linkTarget.getChildren() + def getContent(self) -> str: + return self.__linkTarget.getContent() + def addChild(self, child: FileSystemEntry): + self.__linkTarget.addChild(child) + def __repr__(self): + return ('Link(' + repr(self.getName()) + ' -> ' + + repr(self.__linkTarget) + ')') + def getLinkTarget(self) -> FileSystemEntry: + return self.__linkTarget + +def test_link(): + stefan = Directory('stefan', [File('notes.txt', 'Notiz')]) + wehr = Link('wehr', stefan) + l1 = [x.getName() for x in wehr.getChildren()] + wehr.addChild(Directory('subdir', [])) + l2 = [x.getName() for x in stefan.getChildren()] + l3 = [x.getName() for x in wehr.getChildren()] + + +test_link()