From f61e59c4219d8f24b7ccaebdbebf28fe5227b4ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 08:33:44 +0000 Subject: [PATCH 1/2] sync Callback source with EWM16074_live_data_memory_leak branch Agent-Logs-Url: https://github.com/ekapadi/SNAPRed/sessions/c6109df5-9964-4e4b-bda1-67422fa55842 Co-authored-by: ekapadi <5505178+ekapadi@users.noreply.github.com> --- src/snapred/meta/Callback.py | 317 +++++++++++++++++++++++++------ tests/unit/meta/test_Callback.py | 54 ++++++ 2 files changed, 311 insertions(+), 60 deletions(-) diff --git a/src/snapred/meta/Callback.py b/src/snapred/meta/Callback.py index 5d5c45356..75b35b259 100644 --- a/src/snapred/meta/Callback.py +++ b/src/snapred/meta/Callback.py @@ -1,68 +1,265 @@ -def delegate(cls, attr_name, method_name): - def delegated(self, *vargs, **kwargs): - a = getattr(self, attr_name) - _set = getattr(self, "_set") - if not _set: - raise AttributeError("Callback not Populated") - m = getattr(a, method_name) - return m(*vargs, **kwargs) - - setattr(cls, method_name, delegated) - - -def callback(clazz): - ignore = [ - "_ignore", - "update", - "_set", - "get", - "__class__", - "_value", - "__getitem__", - "__new__", - "__init__", - "__getattr__", - "getattr", - "__getattribute__", - "__setattr__", - "__subclasscheck__", - ] - - class Callback(object): - _ignore = ignore - _set = False - _value: clazz = None - - def __init__(self): - pass +from functools import lru_cache +from typing import Any, Type - def update(self, value): - self._set = True - self._value = value - def get(self): +class CallbackMeta(type): + """ + Metaclass that automatically generates forwarding magic methods for Callback classes. + + This metaclass intercepts class creation and automatically adds magic methods + that forward operations to the wrapped value. This eliminates the need for + manually defining each magic method or dynamically adding them after class creation. + """ + + # Magic methods that should be forwarded to the wrapped value + # This comprehensive list includes arithmetic, comparison, container, and conversion operations + _FORWARDED_MAGIC_METHODS = { + # Arithmetic operations + '__add__', '__sub__', '__mul__', '__truediv__', '__floordiv__', '__mod__', + '__pow__', '__lshift__', '__rshift__', '__and__', '__xor__', '__or__', + '__radd__', '__rsub__', '__rmul__', '__rtruediv__', '__rfloordiv__', '__rmod__', + '__rpow__', '__rlshift__', '__rrshift__', '__rand__', '__rxor__', '__ror__', + '__iadd__', '__isub__', '__imul__', '__itruediv__', '__ifloordiv__', '__imod__', + '__ipow__', '__ilshift__', '__irshift__', '__iand__', '__ixor__', '__ior__', + '__neg__', '__pos__', '__abs__', '__invert__', '__round__', '__floor__', '__ceil__', + + # Comparison operations + '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', + + # Container operations (for lists, dicts, etc.) + '__len__', '__getitem__', '__setitem__', '__delitem__', '__iter__', '__next__', + '__contains__', '__reversed__', '__missing__', + + # Conversion operations + '__int__', '__float__', '__str__', '__repr__', '__bool__', '__hash__', '__index__', + '__complex__', '__bytes__', '__format__', + + # Context managers + '__enter__', '__exit__', + + # Callable + '__call__', + + # Pickling/copying + '__reduce__', '__reduce_ex__', '__copy__', '__deepcopy__', + + # Async operations + '__await__', '__aiter__', '__anext__', '__aenter__', '__aexit__', + } + + def __new__(mcs, name: str, bases: tuple, namespace: dict, **kwargs) -> type: + """ + Create a new Callback class with auto-generated magic methods. + + Args: + name: The class name + bases: Base classes + namespace: Class namespace (attributes and methods) + **kwargs: Additional keyword arguments (including _wrapped_type) + + Returns: + The newly created class + """ + # Get the wrapped type from kwargs or namespace + wrapped_type = kwargs.get('_wrapped_type') or namespace.get('_wrapped_type') + + # Generate forwarding magic methods + for method_name in mcs._FORWARDED_MAGIC_METHODS: + if method_name not in namespace: # Don't override explicitly defined methods + namespace[method_name] = mcs._make_forwarder(method_name) + + return super().__new__(mcs, name, bases, namespace) + + @staticmethod + def _make_forwarder(method_name: str): + """ + Create a magic method that forwards to the wrapped value. + + Args: + method_name: The name of the magic method to forward + + Returns: + A function that forwards the method call to _value + """ + def forwarder(self, *args, **kwargs): if not self._set: - raise AttributeError("Callback not Populated") - return self._value + raise AttributeError(f"Callback method '{method_name}' called before value is set") + # Use object.__getattribute__ to avoid recursion through __getattr__ + value = object.__getattribute__(self, '_value') + method_impl = getattr(value, method_name) + return method_impl(*args, **kwargs) + + forwarder.__name__ = method_name + forwarder.__doc__ = f"Forward {method_name} to the wrapped value" + return forwarder - def __getattr__(self, name): - if name in self._ignore: - return __getattr__(name) # noqa: F821 - if not self._set: - raise AttributeError("Callback not Populated") - return getattr(self._value, name) - def __getitem__(self, items): - if not self._set: - return self - return self._value.__getitem__(items) +class Callback(metaclass=CallbackMeta): + """ + Callback wrapper that defers access to a value until it's populated. + + This class uses a metaclass to automatically forward magic methods to the + wrapped value, enabling transparent operations on primitive types and objects. + + Attributes: + _wrapped_type: The type being wrapped (set by the factory function) + _set: Whether the callback has been populated with a value + _value: The wrapped value (None if not populated) + _ignore: Set of attribute names that should not be forwarded + """ + + # These attributes are handled directly by the Callback class + # and should not be forwarded to _value + _ignore = { + "_ignore", "update", "_set", "get", "__class__", "_value", + "__new__", "__init__", "__getattr__", "__getattribute__", + "__setattr__", "__subclasscheck__", "__instancecheck__", + "__repr__", "__str__" + } + + def __init__(self): + """Initialize an unpopulated callback.""" + self._set = False + self._value = None + + def update(self, value: Any) -> None: + """ + Populate the callback with a value. + + Args: + value: The value to wrap + """ + self._set = True + self._value = value + + def get(self) -> Any: + """ + Get the wrapped value, raising an error if not populated. + + Returns: + The wrapped value + + Raises: + AttributeError: If the callback has not been populated + """ + if not self._set: + raise AttributeError("Callback not Populated") + return self._value + + def __getattr__(self, name: str) -> Any: + """ + Forward attribute access to the wrapped value. + This is called for non-magic methods and attributes. + + Args: + name: The attribute name + + Returns: + The attribute value from the wrapped object + + Raises: + AttributeError: If the callback is not populated + """ + if name in self._ignore: + # Use object's implementation for ignored attributes + return object.__getattribute__(self, name) + + if not self._set: + raise AttributeError("Callback not Populated") + + # Forward to the wrapped value + return getattr(self._value, name) + + def __setattr__(self, name: str, value: Any) -> None: + """ + Control attribute setting to protect internal attributes. + + Args: + name: The attribute name + value: The attribute value + """ + # Allow setting internal attributes during initialization + if name in {'_ignore', '_set', '_value'} or name.startswith('_'): + object.__setattr__(self, name, value) + elif hasattr(self, '_set') and self._set and hasattr(self, '_value'): + # Forward to wrapped value if populated + setattr(self._value, name, value) + else: + # If not populated, set on self + object.__setattr__(self, name, value) + + def __repr__(self) -> str: + """String representation of the callback.""" + wrapped_type = getattr(self, '_wrapped_type', None) + type_name = wrapped_type.__name__ if wrapped_type else 'Unknown' + + if self._set: + return f"Callback({type_name}, value={self._value!r})" + else: + return f"Callback({type_name}, not populated)" + + def __str__(self) -> str: + """String conversion - forward to value if populated.""" + if self._set: + return str(self._value) + wrapped_type = getattr(self, '_wrapped_type', None) + type_name = wrapped_type.__name__ if wrapped_type else 'Unknown' + return f"" + - def __subclasscheck__(cls, subclass): - return clazz.__subclasscheck__(subclass) +@lru_cache(maxsize=None) +def _get_callback_class(clazz: Type) -> Type[Callback]: + """ + Internal cached function that creates or retrieves a cached Callback class for the given type. + + This function uses LRU caching to ensure that only one Callback class is created + per wrapped type, improving memory efficiency and enabling proper type checking. + + Args: + clazz: The type to wrap (e.g., int, str, Workspace) + + Returns: + A Callback class (not an instance) that wraps the given type + """ + # Create a new Callback subclass with the wrapped type set + # We use type() to dynamically create a subclass with _wrapped_type set + return type( + f'Callback[{clazz.__name__}]', + (Callback,), + { + '_wrapped_type': clazz, + '__module__': Callback.__module__, + } + ) - # Forward all methods to the _value, throw if not populated - for name in dir(clazz): - if name not in ignore: - delegate(Callback, "_value", name) - return Callback() +def callback(clazz: Type) -> Callback: + """ + Factory function that creates a new Callback instance for the given type. + + This function uses a cached class creation mechanism to ensure that only one + Callback class is created per wrapped type, but returns a new instance each time. + + Args: + clazz: The type to wrap (e.g., int, str, Workspace) + + Returns: + A new instance of a Callback class that wraps the given type + + Example: + >>> count = callback(int) + >>> count.update(5) + >>> result = count + 10 # Works! Returns 15 + >>> ws = callback(Workspace) + >>> ws.update(some_workspace) + >>> name = ws.name() # Forwarded to underlying workspace + >>> + >>> # Each call returns a new instance, but same class + >>> cb1 = callback(int) + >>> cb2 = callback(int) + >>> cb1 is not cb2 # Different instances + >>> cb1.__class__ is cb2.__class__ # Same class + """ + # Get the cached class and instantiate it + CallbackSubclass = _get_callback_class(clazz) + return CallbackSubclass() diff --git a/tests/unit/meta/test_Callback.py b/tests/unit/meta/test_Callback.py index b215b3e1b..df5fca2dc 100644 --- a/tests/unit/meta/test_Callback.py +++ b/tests/unit/meta/test_Callback.py @@ -50,3 +50,57 @@ def test_floatCallback(self): assert testCallback.__float__() == 123.456 assert testCallback == 123.456 assert testCallback + 1 == 124.456 + + def test_callback_instances(self): + """Test that callback() returns distinct instances but cached classes.""" + + # Create two callbacks for int + cb1 = callback(int) + cb2 = callback(int) + + # Create one callback for str + cb3 = callback(str) + + # Test 1: Different instances + print("Test 1: Different instances") + print(f" cb1 is cb2: {cb1 is cb2}") # Should be False + print(f" cb1 is not cb2: {cb1 is not cb2}") # Should be True + assert cb1 is not cb2, "cb1 and cb2 should be different instances" + + # Test 2: Same class for same type + print("\nTest 2: Same class for same type") + print(f" cb1.__class__ is cb2.__class__: {cb1.__class__ is cb2.__class__}") # Should be True + assert cb1.__class__ is cb2.__class__, "cb1 and cb2 should have the same class" + + # Test 3: Different classes for different types + print("\nTest 3: Different classes for different types") + print(f" cb1.__class__ is cb3.__class__: {cb1.__class__ is cb3.__class__}") # Should be False + assert cb1.__class__ is not cb3.__class__, "cb1 and cb3 should have different classes" + + # Test 4: Class names are correct + print("\nTest 4: Class names are correct") + print(f" cb1.__class__.__name__: {cb1.__class__.__name__}") # Should be Callback[int] + print(f" cb3.__class__.__name__: {cb3.__class__.__name__}") # Should be Callback[str] + assert cb1.__class__.__name__ == "Callback[int]", f"Expected 'Callback[int]', got '{cb1.__class__.__name__}'" + assert cb3.__class__.__name__ == "Callback[str]", f"Expected 'Callback[str]', got '{cb3.__class__.__name__}'" + + # Test 5: Functionality still works + print("\nTest 5: Functionality still works") + cb1.update(10) + cb2.update(20) + + print(f" cb1.get(): {cb1.get()}") # Should be 10 + print(f" cb2.get(): {cb2.get()}") # Should be 20 + assert cb1.get() == 10, f"Expected 10, got {cb1.get()}" + assert cb2.get() == 20, f"Expected 20, got {cb2.get()}" + + # Test 6: Magic methods work + print("\nTest 6: Magic methods work") + result1 = cb1 + 5 + result2 = cb2 + 5 + print(f" cb1 + 5: {result1}") # Should be 15 + print(f" cb2 + 5: {result2}") # Should be 25 + assert result1 == 15, f"Expected 15, got {result1}" + assert result2 == 25, f"Expected 25, got {result2}" + + print("\n✅ All tests passed!") From 6bd35b1e65a3be2ffc592caa06d663a4afc225e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 08:37:21 +0000 Subject: [PATCH 2/2] refactor test_Callback to match style and expand coverage Agent-Logs-Url: https://github.com/ekapadi/SNAPRed/sessions/c6109df5-9964-4e4b-bda1-67422fa55842 Co-authored-by: ekapadi <5505178+ekapadi@users.noreply.github.com> --- tests/unit/meta/test_Callback.py | 108 ++++++++++++++++++------------- 1 file changed, 62 insertions(+), 46 deletions(-) diff --git a/tests/unit/meta/test_Callback.py b/tests/unit/meta/test_Callback.py index df5fca2dc..db6db1ea2 100644 --- a/tests/unit/meta/test_Callback.py +++ b/tests/unit/meta/test_Callback.py @@ -51,56 +51,72 @@ def test_floatCallback(self): assert testCallback == 123.456 assert testCallback + 1 == 124.456 - def test_callback_instances(self): - """Test that callback() returns distinct instances but cached classes.""" - - # Create two callbacks for int + def test_callbackInstances(self): + # `callback()` returns distinct instances, but the underlying class is + # cached per wrapped type. cb1 = callback(int) cb2 = callback(int) - - # Create one callback for str cb3 = callback(str) - # Test 1: Different instances - print("Test 1: Different instances") - print(f" cb1 is cb2: {cb1 is cb2}") # Should be False - print(f" cb1 is not cb2: {cb1 is not cb2}") # Should be True - assert cb1 is not cb2, "cb1 and cb2 should be different instances" - - # Test 2: Same class for same type - print("\nTest 2: Same class for same type") - print(f" cb1.__class__ is cb2.__class__: {cb1.__class__ is cb2.__class__}") # Should be True - assert cb1.__class__ is cb2.__class__, "cb1 and cb2 should have the same class" - - # Test 3: Different classes for different types - print("\nTest 3: Different classes for different types") - print(f" cb1.__class__ is cb3.__class__: {cb1.__class__ is cb3.__class__}") # Should be False - assert cb1.__class__ is not cb3.__class__, "cb1 and cb3 should have different classes" - - # Test 4: Class names are correct - print("\nTest 4: Class names are correct") - print(f" cb1.__class__.__name__: {cb1.__class__.__name__}") # Should be Callback[int] - print(f" cb3.__class__.__name__: {cb3.__class__.__name__}") # Should be Callback[str] - assert cb1.__class__.__name__ == "Callback[int]", f"Expected 'Callback[int]', got '{cb1.__class__.__name__}'" - assert cb3.__class__.__name__ == "Callback[str]", f"Expected 'Callback[str]', got '{cb3.__class__.__name__}'" - - # Test 5: Functionality still works - print("\nTest 5: Functionality still works") + # Distinct instances ... + assert cb1 is not cb2 + # ... but a single cached class per wrapped type ... + assert cb1.__class__ is cb2.__class__ + # ... and a distinct class for each distinct wrapped type. + assert cb1.__class__ is not cb3.__class__ + + # Generated class names encode the wrapped type. + assert cb1.__class__.__name__ == "Callback[int]" + assert cb3.__class__.__name__ == "Callback[str]" + + # Independent state: updating one instance does not affect the other. cb1.update(10) cb2.update(20) + assert cb1.get() == 10 + assert cb2.get() == 20 + + # Forwarded magic methods continue to work for each instance. + assert cb1 + 5 == 15 + assert cb2 + 5 == 25 - print(f" cb1.get(): {cb1.get()}") # Should be 10 - print(f" cb2.get(): {cb2.get()}") # Should be 20 - assert cb1.get() == 10, f"Expected 10, got {cb1.get()}" - assert cb2.get() == 20, f"Expected 20, got {cb2.get()}" - - # Test 6: Magic methods work - print("\nTest 6: Magic methods work") - result1 = cb1 + 5 - result2 = cb2 + 5 - print(f" cb1 + 5: {result1}") # Should be 15 - print(f" cb2 + 5: {result2}") # Should be 25 - assert result1 == 15, f"Expected 15, got {result1}" - assert result2 == 25, f"Expected 25, got {result2}" - - print("\n✅ All tests passed!") + def test_strSet(self): + # Once populated, `str()` is forwarded to the wrapped value. + testCallback = callback(str) + testCallback.update("test") + assert str(testCallback) == "test" + + def test_listCallback(self): + testCallback = callback(list) + testCallback.update([1, 2, 3]) + assert testCallback.get() == [1, 2, 3] + assert len(testCallback) == 3 + assert testCallback[1] == 2 + assert 2 in testCallback + assert list(iter(testCallback)) == [1, 2, 3] + + def test_setItemForwarded(self): + testCallback = callback(list) + testCallback.update([1, 2, 3]) + testCallback[0] = 99 + assert testCallback.get() == [99, 2, 3] + + def test_setAttrForwardedWhenSet(self): + # When populated, setting a non-internal attribute is forwarded to the + # wrapped value. + class _Bag: + pass + + bag = _Bag() + testCallback = callback(_Bag) + testCallback.update(bag) + testCallback.name = "forwarded" + assert bag.name == "forwarded" + + def test_unsetMagicMethodThrows(self): + # A representative forwarded magic method should raise when the + # callback is not populated. + testCallback = callback(int) + with pytest.raises(AttributeError): + testCallback + 1 + with pytest.raises(AttributeError): + len(callback(list))