diff --git a/pyiron_workflow/mixin/single_output.py b/pyiron_workflow/mixin/single_output.py index 1e6dacfc..badabe68 100644 --- a/pyiron_workflow/mixin/single_output.py +++ b/pyiron_workflow/mixin/single_output.py @@ -15,7 +15,7 @@ ) -class AmbiguousOutputError(ValueError): +class AmbiguousOutputError(AttributeError): """Raised when searching for exactly one output, but multiple are found.""" @@ -59,11 +59,15 @@ def channel(self) -> OutputDataWithInjection: def __getattr__(self, item): try: return super().__getattr__(item) - except AttributeError as e1: - try: + except AttributeError as e: + if len(self.outputs) == 1: return getattr(self.channel, item) - except Exception as e2: - raise e2 from e1 + else: + raise AmbiguousOutputError( + f"Tried to access {item} on {self.label}, but failed. Delegating " + f"access to `.channel` was impossible because there is more than " + f"one output channel" + ) from e def __getitem__(self, item): return self.channel.__getitem__(item) diff --git a/tests/integration/test_workflow.py b/tests/integration/test_workflow.py index ca6ded70..fda6946e 100644 --- a/tests/integration/test_workflow.py +++ b/tests/integration/test_workflow.py @@ -156,7 +156,6 @@ def test_executors(self): # executorlib < 0.1 had an Executor with optional backend parameter (defaulting to SingleNodeExecutor) executors.append(Workflow.create.executorlib.Executor) - wf = Workflow("executed") wf.a = Workflow.create.standard.UserInput(42) # Regular wf.b = wf.a + 1 # Injected diff --git a/tests/unit/test_node.py b/tests/unit/test_node.py index 4a34435d..cefb5f23 100644 --- a/tests/unit/test_node.py +++ b/tests/unit/test_node.py @@ -50,6 +50,22 @@ def to_dict(self): pass +class TwOutputs(ANode): + """To de-abstract the class""" + + def _setup_node(self) -> None: + super()._setup_node() + self._outputs = OutputsWithInjection( + OutputDataWithInjection("y", self, type_hint=int), + OutputDataWithInjection("z", self, type_hint=int), + ) + + def process_run_result(self, run_output): + self.outputs.y.value = run_output + self.outputs.z.value = run_output + 1 + return self.outputs.y.value, self.outputs.z.value + + class TestNode(unittest.TestCase): def setUp(self): self.n1 = ANode(label="start", x=0) @@ -394,6 +410,33 @@ def test_single_value(self): ): node.channel # noqa: B018 + def test_injection_hasattr(self): + x = 2 + node = ANode(label="n") + node.set_input_values(x=x) + node.run() + + self.assertEqual( + node.value, + add_one(x), + msg="With a single output, we expect to access the channel attribute", + ) + + two_outputs = TwOutputs(label="to") + two_outputs.set_input_values(x=x) + two_outputs.run() + + with self.assertRaises( + AmbiguousOutputError, + msg="With two output channels, we should not be able to isolate a well defined value attribute because there is no single `channel` to look on", + ): + getattr(two_outputs, "value") # noqa: B009 + + self.assertFalse( + hasattr(two_outputs, "value"), + msg="As a corollary to the last assertion, hasattr should fail", + ) + def test_storage(self): self.assertIs( self.n1.outputs.y.value, NOT_DATA, msg="Sanity check on initial state"