Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions plugboard/utils/export_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,24 @@ def _save_args_wrapper(method: _t.Callable, key: str) -> _t.Callable:
for k, p in inspect.signature(method).parameters.items()
if p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD and k != "self"
]
_reentrant_key = f"__{key}_active__"

@wraps(method)
def _wrapper(self: _t.Any, *args: _t.Any, **kwargs: _t.Any) -> None:
saved_kwargs = ExportMixin._convert_exportable_objs(kwargs)
saved_args = dict(zip(_positional_args[: len(args)], args))
setattr(self, key, {**getattr(self, key, {}), **saved_args, **saved_kwargs})
method(self, *args, **kwargs)
is_outermost = not getattr(self, _reentrant_key, False)
if is_outermost:
setattr(self, _reentrant_key, True)
saved_kwargs = ExportMixin._convert_exportable_objs(kwargs)
saved_args = dict(zip(_positional_args[: len(args)], args))
setattr(self, key, {**saved_args, **saved_kwargs})
try:
method(self, *args, **kwargs)
finally:
if is_outermost:
try:
delattr(self, _reentrant_key)
except AttributeError:
pass

return _wrapper

Expand Down
39 changes: 39 additions & 0 deletions tests/unit/test_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,45 @@ async def test_component_initial_values(initial_values: dict[str, _t.Iterable])
await component.io.close()


class ExportBase(Component):
io = IO(inputs=[], outputs=["out"])

def __init__(self, x: int = 1, **kwargs: _t.Unpack[ComponentArgsDict]) -> None:
super().__init__(**kwargs)
self.x = x

async def step(self) -> None:
pass


class ExportDerived(ExportBase):
io = IO(inputs=[], outputs=["out"])

def __init__(self, y: int = 2, **kwargs: _t.Unpack[ComponentArgsDict]) -> None:
super().__init__(x=y * 10, **kwargs)
self.y = y

async def step(self) -> None:
pass


def test_export_captures_only_outermost_init_args() -> None:
"""export() should record only the arguments passed to the outermost __init__."""
d = ExportDerived(name="d", y=3)
args = d.export()["args"]
# Only ExportDerived's own args should appear; ExportBase's derived 'x' must not
assert args == {"name": "d", "y": 3}
# Rebuilding from the exported args must succeed without TypeError
d2 = ExportDerived(**args)
assert d2.export()["args"] == args


def test_export_no_stale_flag_on_instance() -> None:
"""Re-entrancy flag must not persist on the instance after construction."""
d = ExportDerived(name="d", y=5)
assert not any(attr.endswith("_active__") for attr in vars(d))


@pytest.mark.asyncio
async def test_component_status() -> None:
"""Tests the status of a `Component` across its lifecycle."""
Expand Down
Loading