From 9436f82cff59ba02b52aa6e4b89a7b96829fa8d1 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 26 Oct 2023 11:53:08 -0700 Subject: [PATCH 01/38] Raise from the original error When single value node fails to find an attribute on its single value --- pyiron_workflow/function.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyiron_workflow/function.py b/pyiron_workflow/function.py index aaea64b7..16969546 100644 --- a/pyiron_workflow/function.py +++ b/pyiron_workflow/function.py @@ -607,7 +607,13 @@ def __getitem__(self, item): return self.single_value.__getitem__(item) def __getattr__(self, item): - return getattr(self.single_value, item) + try: + return getattr(self.single_value, item) + except Exception as e: + raise AttributeError( + f"Could not find {item} as an attribute of the single value " + f"{self.single_value}" + ) from e def __repr__(self): return self.single_value.__repr__() From 96306414a8e1d49bbca0a738ea4fca2f92dc7643 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 26 Oct 2023 12:11:16 -0700 Subject: [PATCH 02/38] Add the type hint to channel presentation --- pyiron_workflow/channels.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyiron_workflow/channels.py b/pyiron_workflow/channels.py index 4cfee805..cb3958ca 100644 --- a/pyiron_workflow/channels.py +++ b/pyiron_workflow/channels.py @@ -398,6 +398,7 @@ def to_dict(self) -> dict: d = super().to_dict() d["value"] = repr(self.value) d["ready"] = self.ready + d["type_hint"] = str(self.type_hint) return d From 359ad26ee1a88e3f51c3eb964c58fd6b5e6cac92 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 26 Oct 2023 13:38:16 -0700 Subject: [PATCH 03/38] Store the type hints right on the decorator-created classes --- pyiron_workflow/function.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pyiron_workflow/function.py b/pyiron_workflow/function.py index 16969546..d0ee99c1 100644 --- a/pyiron_workflow/function.py +++ b/pyiron_workflow/function.py @@ -322,6 +322,8 @@ def __init__( else: # If a callable node function is received, use it self.node_function = node_function + self._input_type_hints = get_type_hints(node_function) + self._output_type_hints = get_type_hints(node_function)["return"] super().__init__( label=label if label is not None else self.node_function.__name__, @@ -382,7 +384,7 @@ def outputs(self) -> Outputs: def _build_input_channels(self): channels = [] - type_hints = get_type_hints(self.node_function) + type_hints = self._input_type_hints for ii, (label, value) in enumerate(self._input_args.items()): is_self = False @@ -435,7 +437,7 @@ def _init_keywords(self): def _build_output_channels(self, *return_labels: str): try: - type_hints = get_type_hints(self.node_function)["return"] + type_hints = self._output_type_hints if len(return_labels) > 1: type_hints = get_args(type_hints) if not isinstance(type_hints, tuple): @@ -637,6 +639,8 @@ def function_node(output_labels=None): """ def as_node(node_function: callable): + hints = get_type_hints(node_function) + return_hint = hints["return"] if hasattr(hints, "return") else None return type( node_function.__name__.title().replace("_", ""), # fnc_name to CamelCase (Function,), # Define parentage @@ -647,6 +651,8 @@ def as_node(node_function: callable): output_labels=output_labels, ), "node_function": staticmethod(node_function), + "_input_type_hints": hints, + "_output_type_hints": return_hint, }, ) @@ -663,6 +669,8 @@ def single_value_node(output_labels=None): """ def as_single_value_node(node_function: callable): + hints = get_type_hints(node_function) + return_hint = hints["return"] if hasattr(hints, "return") else None return type( node_function.__name__.title().replace("_", ""), # fnc_name to CamelCase (SingleValue,), # Define parentage @@ -673,6 +681,8 @@ def as_single_value_node(node_function: callable): output_labels=output_labels, ), "node_function": staticmethod(node_function), + "_input_type_hints": hints, + "_output_type_hints": return_hint, }, ) From 5dffb5be8a8c2f8c5e07316fd81678480f21be13 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 26 Oct 2023 16:10:29 -0700 Subject: [PATCH 04/38] Store a reference to the import path And import and create node packages as requested based off these references --- pyiron_workflow/interfaces.py | 57 ++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/pyiron_workflow/interfaces.py b/pyiron_workflow/interfaces.py index 02d826de..a331c54e 100644 --- a/pyiron_workflow/interfaces.py +++ b/pyiron_workflow/interfaces.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from importlib import import_module from pyiron_base.interfaces.singleton import Singleton @@ -18,9 +18,6 @@ single_value_node, ) -if TYPE_CHECKING: - from pyiron_workflow.node import Node - class Creator(metaclass=Singleton): """ @@ -30,6 +27,8 @@ class Creator(metaclass=Singleton): """ def __init__(self): + self._node_packages = {} + self.Executor = Executor self.Function = Function @@ -56,20 +55,6 @@ def Workflow(self): self._workflow = Workflow return self._workflow - @property - def standard(self): - from pyiron_workflow.node_package import NodePackage - from pyiron_workflow.node_library.standard import nodes - - return NodePackage(*nodes) - - @property - def atomistics(self): - from pyiron_workflow.node_package import NodePackage - from pyiron_workflow.node_library.atomistics import nodes - - return NodePackage(*nodes) - @property def meta(self): if self._meta is None: @@ -78,16 +63,32 @@ def meta(self): self._meta = meta_nodes return self._meta - def register(self, domain: str, *nodes: list[type[Node]]): - raise NotImplementedError( - "Registering new node packages is currently not playing well with " - "executors. We hope to return this feature soon." - ) - # if domain in self.__dir__(): - # raise AttributeError(f"{domain} is already an attribute of {self}") - # from pyiron_workflow.node_package import NodePackage - # - # setattr(self, domain, NodePackage(*nodes)) + def __getattr__(self, item): + try: + module = import_module(self._node_packages[item]) + from pyiron_workflow.node_package import NodePackage + return NodePackage(*module.nodes) + except KeyError as e: + raise AttributeError( + f"{self.__class__.__name__} could not find attribute {item} -- did you " + f"forget to register node package to this key?" + ) from e + + def __getstate__(self): + return self.__dict__ + + def __setstate__(self, state): + self.__dict__ = state + + def register(self, domain: str, import_path: str): + if domain in self.__dir__(): + raise AttributeError(f"{domain} is already an attribute of {self}") + if domain in self._node_packages.keys(): + raise KeyError( + f"{domain} is already a registered node package, please choose a " + f"different domain to store these nodes under" + ) + self._node_packages[domain] = import_path class Wrappers(metaclass=Singleton): From f551f59b7630e094cd2bbae2955c0e38b8dced92 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 26 Oct 2023 16:22:21 -0700 Subject: [PATCH 05/38] Pre-register the normal packages For now at least, to keep behaviour the same --- pyiron_workflow/interfaces.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyiron_workflow/interfaces.py b/pyiron_workflow/interfaces.py index a331c54e..b3ec7e92 100644 --- a/pyiron_workflow/interfaces.py +++ b/pyiron_workflow/interfaces.py @@ -39,6 +39,9 @@ def __init__(self): self._workflow = None self._meta = None + self.register("standard", "pyiron_workflow.node_library.standard") + self.register("atomistics", "pyiron_workflow.node_library.atomistics") + @property def Macro(self): if self._macro is None: From 7ff73d7152971c9b8cea3cfc31706d2ffdb6fdbc Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 26 Oct 2023 16:26:44 -0700 Subject: [PATCH 06/38] Handle the absence of return hints more gracefully --- pyiron_workflow/function.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/pyiron_workflow/function.py b/pyiron_workflow/function.py index d0ee99c1..add96c66 100644 --- a/pyiron_workflow/function.py +++ b/pyiron_workflow/function.py @@ -322,8 +322,7 @@ def __init__( else: # If a callable node function is received, use it self.node_function = node_function - self._input_type_hints = get_type_hints(node_function) - self._output_type_hints = get_type_hints(node_function)["return"] + self._type_hints = get_type_hints(node_function) super().__init__( label=label if label is not None else self.node_function.__name__, @@ -384,7 +383,7 @@ def outputs(self) -> Outputs: def _build_input_channels(self): channels = [] - type_hints = self._input_type_hints + type_hints = self._type_hints for ii, (label, value) in enumerate(self._input_args.items()): is_self = False @@ -437,7 +436,7 @@ def _init_keywords(self): def _build_output_channels(self, *return_labels: str): try: - type_hints = self._output_type_hints + type_hints = self._type_hints["return"] if len(return_labels) > 1: type_hints = get_args(type_hints) if not isinstance(type_hints, tuple): @@ -639,8 +638,6 @@ def function_node(output_labels=None): """ def as_node(node_function: callable): - hints = get_type_hints(node_function) - return_hint = hints["return"] if hasattr(hints, "return") else None return type( node_function.__name__.title().replace("_", ""), # fnc_name to CamelCase (Function,), # Define parentage @@ -651,8 +648,7 @@ def as_node(node_function: callable): output_labels=output_labels, ), "node_function": staticmethod(node_function), - "_input_type_hints": hints, - "_output_type_hints": return_hint, + "_type_hints": get_type_hints(node_function), }, ) @@ -669,8 +665,6 @@ def single_value_node(output_labels=None): """ def as_single_value_node(node_function: callable): - hints = get_type_hints(node_function) - return_hint = hints["return"] if hasattr(hints, "return") else None return type( node_function.__name__.title().replace("_", ""), # fnc_name to CamelCase (SingleValue,), # Define parentage @@ -681,8 +675,7 @@ def as_single_value_node(node_function: callable): output_labels=output_labels, ), "node_function": staticmethod(node_function), - "_input_type_hints": hints, - "_output_type_hints": return_hint, + "_type_hints": get_type_hints(node_function), }, ) From 2960d6f4b79735838283f7f1e3b5e21b205dbc32 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 26 Oct 2023 16:37:31 -0700 Subject: [PATCH 07/38] Refactor the function node decorators So updates to Function and SingleValue happen in a single place --- pyiron_workflow/function.py | 51 +++++++++++++++---------------------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/pyiron_workflow/function.py b/pyiron_workflow/function.py index add96c66..25bd62ba 100644 --- a/pyiron_workflow/function.py +++ b/pyiron_workflow/function.py @@ -625,25 +625,16 @@ def __str__(self): ) -def function_node(output_labels=None): - """ - A decorator for dynamically creating node classes from functions. - - Decorates a function. - Returns a `Function` subclass whose name is the camel-case version of the function - node, and whose signature is modified to exclude the node function and output labels - (which are explicitly defined in the process of using the decorator). - - Optionally takes any keyword arguments of `Function`. - """ - +def _wrapper_factory( + parent_class: type[Function], output_labels: Optional[list[str]] +) -> callable: def as_node(node_function: callable): return type( node_function.__name__.title().replace("_", ""), # fnc_name to CamelCase - (Function,), # Define parentage + (parent_class,), # Define parentage { "__init__": partialmethod( - Function.__init__, + parent_class.__init__, None, output_labels=output_labels, ), @@ -655,6 +646,20 @@ def as_node(node_function: callable): return as_node +def function_node(output_labels=None): + """ + A decorator for dynamically creating node classes from functions. + + Decorates a function. + Returns a `Function` subclass whose name is the camel-case version of the function + node, and whose signature is modified to exclude the node function and output labels + (which are explicitly defined in the process of using the decorator). + + Optionally takes any keyword arguments of `Function`. + """ + return _wrapper_factory(parent_class=Function, output_labels=output_labels) + + def single_value_node(output_labels=None): """ A decorator for dynamically creating fast node classes from functions. @@ -663,20 +668,4 @@ def single_value_node(output_labels=None): Optionally takes any keyword arguments of `SingleValueNode`. """ - - def as_single_value_node(node_function: callable): - return type( - node_function.__name__.title().replace("_", ""), # fnc_name to CamelCase - (SingleValue,), # Define parentage - { - "__init__": partialmethod( - SingleValue.__init__, - None, - output_labels=output_labels, - ), - "node_function": staticmethod(node_function), - "_type_hints": get_type_hints(node_function), - }, - ) - - return as_single_value_node + return _wrapper_factory(parent_class=SingleValue, output_labels=output_labels) From b607c8720605e6a59591bf49cf25417f649c02e1 Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Thu, 26 Oct 2023 23:55:41 +0000 Subject: [PATCH 08/38] Format black --- pyiron_workflow/interfaces.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyiron_workflow/interfaces.py b/pyiron_workflow/interfaces.py index b3ec7e92..c58f1fcc 100644 --- a/pyiron_workflow/interfaces.py +++ b/pyiron_workflow/interfaces.py @@ -70,6 +70,7 @@ def __getattr__(self, item): try: module = import_module(self._node_packages[item]) from pyiron_workflow.node_package import NodePackage + return NodePackage(*module.nodes) except KeyError as e: raise AttributeError( From 804031677d5414be3a5512b4ca4f62dee05f1926 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 27 Oct 2023 14:07:27 -0700 Subject: [PATCH 09/38] Add a unit test for registration --- tests/static/__init__.py | 0 tests/static/demo_nodes.py | 13 +++++++++++++ tests/unit/test_interfaces.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 tests/static/__init__.py create mode 100644 tests/static/demo_nodes.py create mode 100644 tests/unit/test_interfaces.py diff --git a/tests/static/__init__.py b/tests/static/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/static/demo_nodes.py b/tests/static/demo_nodes.py new file mode 100644 index 00000000..2f3c3dab --- /dev/null +++ b/tests/static/demo_nodes.py @@ -0,0 +1,13 @@ +""" +A demo node package for the purpose of testing node package registration. +""" + +from pyiron_workflow import Workflow + + +@Workflow.wrap_as.single_value_node("sum") +def add(x: int, y: int) -> int: + return x + y + + +nodes = [add] diff --git a/tests/unit/test_interfaces.py b/tests/unit/test_interfaces.py new file mode 100644 index 00000000..d0e2b585 --- /dev/null +++ b/tests/unit/test_interfaces.py @@ -0,0 +1,32 @@ +from pathlib import Path +import sys +from unittest import TestCase, skipUnless + + +from pyiron_workflow.interfaces import Creator + + +@skipUnless( + sys.version_info[0] == 3 and sys.version_info[1] >= 10, "Only supported for 3.10+" +) +class TestCreator(TestCase): + def test_registration(self): + creator = Creator() + + with self.assertRaises( + AttributeError, + msg="Sanity check that the package isn't there yet and the test setup is " + "what we want" + ): + creator.demo_nodes + + path_to_tests = Path(__file__).parent.parent + sys.path.append(str(path_to_tests.resolve())) + creator.register("demo", "static.demo_nodes") + + node = creator.demo.Add(1, 2) + self.assertEqual( + 3, + node(), + msg="Node should get instantiated from creator and be operable" + ) From cb43cc8df7f5e7976cb75d7bb1c2693176016f8f Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 27 Oct 2023 14:08:57 -0700 Subject: [PATCH 10/38] Refactor tests --- tests/unit/test_interfaces.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_interfaces.py b/tests/unit/test_interfaces.py index d0e2b585..4b84f37e 100644 --- a/tests/unit/test_interfaces.py +++ b/tests/unit/test_interfaces.py @@ -10,21 +10,25 @@ sys.version_info[0] == 3 and sys.version_info[1] >= 10, "Only supported for 3.10+" ) class TestCreator(TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.creator = Creator() + path_to_tests = Path(__file__).parent.parent + sys.path.append(str(path_to_tests.resolve())) + # Now we can import from `static` + def test_registration(self): - creator = Creator() with self.assertRaises( AttributeError, msg="Sanity check that the package isn't there yet and the test setup is " "what we want" ): - creator.demo_nodes + self.creator.demo_nodes - path_to_tests = Path(__file__).parent.parent - sys.path.append(str(path_to_tests.resolve())) - creator.register("demo", "static.demo_nodes") + self.creator.register("demo", "static.demo_nodes") - node = creator.demo.Add(1, 2) + node = self.creator.demo.Add(1, 2) self.assertEqual( 3, node(), From b1dda613a543194a91d7a1897aaf5b7bdae881c1 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 27 Oct 2023 14:13:24 -0700 Subject: [PATCH 11/38] Allow re-registering the _same thing_ to the same place --- pyiron_workflow/interfaces.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pyiron_workflow/interfaces.py b/pyiron_workflow/interfaces.py index b3ec7e92..59d99e42 100644 --- a/pyiron_workflow/interfaces.py +++ b/pyiron_workflow/interfaces.py @@ -84,13 +84,17 @@ def __setstate__(self, state): self.__dict__ = state def register(self, domain: str, import_path: str): - if domain in self.__dir__(): - raise AttributeError(f"{domain} is already an attribute of {self}") if domain in self._node_packages.keys(): - raise KeyError( - f"{domain} is already a registered node package, please choose a " - f"different domain to store these nodes under" - ) + if import_path != self._node_packages[domain]: + raise KeyError( + f"{domain} is already a registered node package, please choose a " + f"different domain to store these nodes under" + ) + # Else we're just re-registering the same thing to the same place, + # which is fine + elif domain in self.__dir__(): + raise AttributeError(f"{domain} is already an attribute of {self}") + self._node_packages[domain] = import_path From 7b9837d60d9eba7a981e742635ecc60eb4f88375 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 27 Oct 2023 14:13:33 -0700 Subject: [PATCH 12/38] Expand tests --- tests/unit/test_interfaces.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/unit/test_interfaces.py b/tests/unit/test_interfaces.py index 4b84f37e..9825bd50 100644 --- a/tests/unit/test_interfaces.py +++ b/tests/unit/test_interfaces.py @@ -34,3 +34,23 @@ def test_registration(self): node(), msg="Node should get instantiated from creator and be operable" ) + + with self.subTest("Test re-registration"): + self.creator.register("demo", "static.demo_nodes") + # Same thing to the same location should be fine + + self.creator.register("a_key_other_than_demo", "static.demo_nodes") + # The same thing to another key is usually dumb, but totally permissible + + with self.assertRaises( + KeyError, + msg="Should not be able to register a new package to an existing domain" + ): + self.creator.register("demo", "pyiron_workflow.node_library.standard") + + with self.assertRaises( + AttributeError, + msg="Should not be able to register to existing fields" + ): + some_field = self.creator.dir()[0] + self.creator.register(some_field, "static.demo_nodes") From 7851e7b13cc579a588fb6d168f3f7b2ed899edcd Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 27 Oct 2023 14:20:25 -0700 Subject: [PATCH 13/38] Refactor: rename --- pyiron_workflow/interfaces.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyiron_workflow/interfaces.py b/pyiron_workflow/interfaces.py index e4df3b71..21066235 100644 --- a/pyiron_workflow/interfaces.py +++ b/pyiron_workflow/interfaces.py @@ -84,9 +84,9 @@ def __getstate__(self): def __setstate__(self, state): self.__dict__ = state - def register(self, domain: str, import_path: str): + def register(self, domain: str, package_identifier: str): if domain in self._node_packages.keys(): - if import_path != self._node_packages[domain]: + if package_identifier != self._node_packages[domain]: raise KeyError( f"{domain} is already a registered node package, please choose a " f"different domain to store these nodes under" @@ -96,7 +96,7 @@ def register(self, domain: str, import_path: str): elif domain in self.__dir__(): raise AttributeError(f"{domain} is already an attribute of {self}") - self._node_packages[domain] = import_path + self._node_packages[domain] = package_identifier class Wrappers(metaclass=Singleton): From 63f30c052f6bd81aa40f0358a65b3247f54f1d28 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 27 Oct 2023 14:24:15 -0700 Subject: [PATCH 14/38] Add docstring --- pyiron_workflow/interfaces.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pyiron_workflow/interfaces.py b/pyiron_workflow/interfaces.py index 21066235..39368cdc 100644 --- a/pyiron_workflow/interfaces.py +++ b/pyiron_workflow/interfaces.py @@ -84,7 +84,24 @@ def __getstate__(self): def __setstate__(self, state): self.__dict__ = state - def register(self, domain: str, package_identifier: str): + def register(self, domain: str, package_identifier: str) -> None: + """ + Add a new package of nodes under the provided attribute, e.g. after adding + nodes to the domain `"my_nodes"`, and instance of creator can call things like + `creator.my_nodes.some_node_that_is_there()` + + Args: + domain (str): + package_identifier (str): An identifier for the node package. (Right now + that's just a string version of the path to the module, e.g. + `pyiron_workflow.node_library.standard`.) + + Raises: + KeyError: If the domain already exists, but the identifier doesn't match + with the stored identifier. + AttributeError: If you try to register at a domain that is already another + method or attribute of the creator. + """ if domain in self._node_packages.keys(): if package_identifier != self._node_packages[domain]: raise KeyError( From 0328c2287f2373b295f3607224da7133757f9eea Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 27 Oct 2023 14:42:54 -0700 Subject: [PATCH 15/38] Fail early if the provided package identifier will fail --- pyiron_workflow/interfaces.py | 28 ++++++++++++++++++++++++++ tests/static/faulty_node_package.py | 13 ++++++++++++ tests/static/forgetful_node_package.py | 13 ++++++++++++ tests/static/not_a_node_package.py | 6 ++++++ tests/unit/test_interfaces.py | 27 +++++++++++++++++++++++++ 5 files changed, 87 insertions(+) create mode 100644 tests/static/faulty_node_package.py create mode 100644 tests/static/forgetful_node_package.py create mode 100644 tests/static/not_a_node_package.py diff --git a/pyiron_workflow/interfaces.py b/pyiron_workflow/interfaces.py index 39368cdc..6526f3c5 100644 --- a/pyiron_workflow/interfaces.py +++ b/pyiron_workflow/interfaces.py @@ -101,6 +101,7 @@ def register(self, domain: str, package_identifier: str) -> None: with the stored identifier. AttributeError: If you try to register at a domain that is already another method or attribute of the creator. + ValueError: If the identifier can't be parsed. """ if domain in self._node_packages.keys(): if package_identifier != self._node_packages[domain]: @@ -113,8 +114,35 @@ def register(self, domain: str, package_identifier: str) -> None: elif domain in self.__dir__(): raise AttributeError(f"{domain} is already an attribute of {self}") + self._verify_identifier(package_identifier) + self._node_packages[domain] = package_identifier + @staticmethod + def _verify_identifier(package_identifier: str): + """ + Logic for verifying whether new package identifiers will actually be usable for + creating node packages when their domain is called. Lets us fail early in + registration. + + Right now, we just make sure it's a string from which we can import a list of + nodes. + """ + from pyiron_workflow.node import Node + try: + module = import_module(package_identifier) + nodes = module.nodes + if not all(issubclass(node, Node) for node in nodes): + raise TypeError( + f"At least one node in {nodes} was not of the type {Node.__name__}" + ) + except Exception as e: + raise ValueError( + f"The package identifier is {package_identifier} is not valid. Please " + f"ensure it is an importable module with a list of {Node.__name__} " + f"objects stored in the variable `nodes`." + ) from e + class Wrappers(metaclass=Singleton): """ diff --git a/tests/static/faulty_node_package.py b/tests/static/faulty_node_package.py new file mode 100644 index 00000000..2679fce8 --- /dev/null +++ b/tests/static/faulty_node_package.py @@ -0,0 +1,13 @@ +""" +An incorrect node package for the purpose of testing node package registration. +""" + +from pyiron_workflow import Workflow + + +@Workflow.wrap_as.single_value_node("sum") +def add(x: int, y: int) -> int: + return x + y + + +nodes = [add, 42] # Not everything here is a node! diff --git a/tests/static/forgetful_node_package.py b/tests/static/forgetful_node_package.py new file mode 100644 index 00000000..8e471704 --- /dev/null +++ b/tests/static/forgetful_node_package.py @@ -0,0 +1,13 @@ +""" +An incorrect node package for the purpose of testing node package registration. +""" + +from pyiron_workflow import Workflow + + +@Workflow.wrap_as.single_value_node("sum") +def add(x: int, y: int) -> int: + return x + y + + +# nodes = [add] # Oops, we "forgot" to populate a `nodes` list diff --git a/tests/static/not_a_node_package.py b/tests/static/not_a_node_package.py new file mode 100644 index 00000000..27002ed5 --- /dev/null +++ b/tests/static/not_a_node_package.py @@ -0,0 +1,6 @@ +""" +A module that is not a node package at all for the purpose of testing node package +registration. +""" + +this_variable = "is not a list of Node objects called `nodes`" diff --git a/tests/unit/test_interfaces.py b/tests/unit/test_interfaces.py index 9825bd50..182094bc 100644 --- a/tests/unit/test_interfaces.py +++ b/tests/unit/test_interfaces.py @@ -54,3 +54,30 @@ def test_registration(self): ): some_field = self.creator.dir()[0] self.creator.register(some_field, "static.demo_nodes") + + with self.subTest("Test failure cases"): + n_initial_packages = len(self.creator._node_packages) + + with self.assertRaises( + ValueError, + msg="Mustn't allow importing from things that are not node packages" + ): + self.creator.register("not_even", "static.not_a_node_package") + + with self.assertRaises( + ValueError, + msg="Must require a `nodes` property in the module" + ): + self.creator.register("forgetful", "static.forgetful_node_package") + + with self.assertRaises( + ValueError, + msg="Must have only nodes in the iterable `nodes` property" + ): + self.creator.register("faulty", "static.faulty_node_package") + + self.assertEqual( + n_initial_packages, + len(self.creator._node_packages), + msg="Packages should not be getting added if exceptions are raised" + ) From f43958e03367ef79bef1f5fd12f0924ca2a255a9 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 27 Oct 2023 14:52:23 -0700 Subject: [PATCH 16/38] Refactor: extract the "just re-registering" logic So it can be extended when we support more sophisticated identifiers --- pyiron_workflow/interfaces.py | 43 +++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/pyiron_workflow/interfaces.py b/pyiron_workflow/interfaces.py index 6526f3c5..65a84fc0 100644 --- a/pyiron_workflow/interfaces.py +++ b/pyiron_workflow/interfaces.py @@ -91,7 +91,9 @@ def register(self, domain: str, package_identifier: str) -> None: `creator.my_nodes.some_node_that_is_there()` Args: - domain (str): + domain (str): The attribute name at which to register the new package. + (Note: no sanitizing is done here, so if you provide a string that + won't work as an attribute name, that's your problem.) package_identifier (str): An identifier for the node package. (Right now that's just a string version of the path to the module, e.g. `pyiron_workflow.node_library.standard`.) @@ -103,14 +105,12 @@ def register(self, domain: str, package_identifier: str) -> None: method or attribute of the creator. ValueError: If the identifier can't be parsed. """ - if domain in self._node_packages.keys(): - if package_identifier != self._node_packages[domain]: - raise KeyError( - f"{domain} is already a registered node package, please choose a " - f"different domain to store these nodes under" - ) - # Else we're just re-registering the same thing to the same place, - # which is fine + + if self._package_conflicts_with_existing(domain, package_identifier): + raise KeyError( + f"{domain} is already a registered node package, please choose a " + f"different domain to store these nodes under" + ) elif domain in self.__dir__(): raise AttributeError(f"{domain} is already an attribute of {self}") @@ -118,6 +118,31 @@ def register(self, domain: str, package_identifier: str) -> None: self._node_packages[domain] = package_identifier + def _package_conflicts_with_existing( + self, domain: str, package_identifier: str + ) -> bool: + """ + Check if the new package conflict with an existing package at the requested + domain; if there isn't one, or if the new and old packages are identical then + there is no conflict! + + Args: + domain (str): The domain at which the new package is attempting to register. + package_identifier (str): The identifier for the new package. + + Returns: + (bool): True iff there is a package already at that domain and it is not + the same as the new one. + """ + if domain in self._node_packages.keys(): + # If it's already here, it had better be the same package + return package_identifier != self._node_packages[domain] + # We can make "sameness" logic more complex as we allow more sophisticated + # identifiers + else: + # If it's not here already, it can't conflict! + return False + @staticmethod def _verify_identifier(package_identifier: str): """ From 3136905211d67b80b7e6e17b2b431f9014ecdc0d Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Fri, 27 Oct 2023 21:59:17 +0000 Subject: [PATCH 17/38] Format black --- pyiron_workflow/interfaces.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyiron_workflow/interfaces.py b/pyiron_workflow/interfaces.py index 65a84fc0..353c284a 100644 --- a/pyiron_workflow/interfaces.py +++ b/pyiron_workflow/interfaces.py @@ -154,6 +154,7 @@ def _verify_identifier(package_identifier: str): nodes. """ from pyiron_workflow.node import Node + try: module = import_module(package_identifier) nodes = module.nodes From 20fa4aeb8feb5ce5c701bc2e535273e0f7188a5e Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 30 Oct 2023 09:05:45 -0700 Subject: [PATCH 18/38] Version guard package registration For packages that rely on type hinting only available in >=3.10 --- pyiron_workflow/interfaces.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pyiron_workflow/interfaces.py b/pyiron_workflow/interfaces.py index 353c284a..f4551637 100644 --- a/pyiron_workflow/interfaces.py +++ b/pyiron_workflow/interfaces.py @@ -5,6 +5,7 @@ from __future__ import annotations from importlib import import_module +from sys import version_info from pyiron_base.interfaces.singleton import Singleton @@ -39,8 +40,13 @@ def __init__(self): self._workflow = None self._meta = None - self.register("standard", "pyiron_workflow.node_library.standard") - self.register("atomistics", "pyiron_workflow.node_library.atomistics") + if version_info[0] == 3 and version_info[1] >= 10: + # These modules use syntactic sugar for type hinting that is only supported + # in python >=3.10 + # If the CI skips testing on 3.9 gets dropped, we can think about removing + # this if-clause and just letting users of python <3.10 hit an error. + self.register("standard", "pyiron_workflow.node_library.standard") + self.register("atomistics", "pyiron_workflow.node_library.atomistics") @property def Macro(self): From 08d3dc0212d38e64fc26217e8065086805013131 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 30 Oct 2023 09:18:31 -0700 Subject: [PATCH 19/38] Explain intent for future devs --- pyiron_workflow/function.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pyiron_workflow/function.py b/pyiron_workflow/function.py index 25bd62ba..1136f1fc 100644 --- a/pyiron_workflow/function.py +++ b/pyiron_workflow/function.py @@ -628,6 +628,25 @@ def __str__(self): def _wrapper_factory( parent_class: type[Function], output_labels: Optional[list[str]] ) -> callable: + """ + An abstract base for making decorators that wrap a function as `Function` or its + children. + """ + # One really subtle thing is that we manually parse the function type hints right + # here and include these as a class-level attribute. + # This is because on (de)(cloud)pickling a function node, somehow the node function + # method attached to it gets its `__globals__` attribute changed; it retains stuff + # _inside_ the function, but loses imports it used from the _outside_ -- i.e. type + # hints! I (@liamhuber) don't deeply understand _why_ (de)pickling is modifying the + # __globals__ in this way, but the result is that type hints cannot be parsed after + # the change. + # The final piece of the puzzle here is that because the node function is a _class_ + # level attribute, if you (de)pickle a node, _new_ instances of that node wind up + # having their node function's `__globals__` trimmed down in this way! + # So to keep the type hint parsing working, we snag and interpret all the type hints + # at wrapping time, when we are guaranteed to have all the globals available, and + # also slap them on as a class-level attribute. These get safely packed and returned + # when (de)pickling so we can keep processing type hints without trouble. def as_node(node_function: callable): return type( node_function.__name__.title().replace("_", ""), # fnc_name to CamelCase From 8059b574564af3e1d241b78dfa6ee63e3d9af569 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 30 Oct 2023 09:21:14 -0700 Subject: [PATCH 20/38] Be more generous waiting for the timeout --- tests/unit/executors/test_cloudprocesspool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/executors/test_cloudprocesspool.py b/tests/unit/executors/test_cloudprocesspool.py index eb49333e..018bc418 100644 --- a/tests/unit/executors/test_cloudprocesspool.py +++ b/tests/unit/executors/test_cloudprocesspool.py @@ -155,7 +155,7 @@ def slow(): executor = CloudpickleProcessPoolExecutor() fs = executor.submit(f.run) self.assertEqual( - fs.result(timeout=30), + fs.result(timeout=60), fortytwo, msg="waiting long enough should get the result" ) From d058cd0f35a2cb2c15ec7ca5a45989c1b5a216ab Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 30 Oct 2023 09:30:06 -0700 Subject: [PATCH 21/38] Give a shortcut to node registration --- pyiron_workflow/composite.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 270992e0..c7083564 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -6,7 +6,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from functools import partial +from functools import partial, wraps from typing import Literal, Optional, TYPE_CHECKING from bidict import bidict @@ -422,6 +422,10 @@ def replace(self, owned_node: Node | str, replacement: Node | type[Node]) -> Nod self.starting_nodes.append(replacement) return owned_node + @wraps(Creator.register) + def register(self, domain: str, package_identifier: str) -> None: + self.create.register(domain=domain, package_identifier=package_identifier) + def __setattr__(self, key: str, node: Node): if isinstance(node, Node) and key != "parent": self.add(node, label=key) From 39dec3f9c189595455b707de05e18eb398fffd52 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 30 Oct 2023 09:34:06 -0700 Subject: [PATCH 22/38] Add an integration test for the thing that originally hurt us --- tests/integration/test_workflow.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/integration/test_workflow.py b/tests/integration/test_workflow.py index 834f66bf..7f0b1f17 100644 --- a/tests/integration/test_workflow.py +++ b/tests/integration/test_workflow.py @@ -171,3 +171,26 @@ def less_than_ten(value): out = wf(a=1, b=2) self.assertEqual(out.total, 11) + + def test_executor_and_creator_interaction(self): + """ + Make sure that submitting stuff to a parallel processor doesn't stop us from + using the same stuff on the main process. This can happen because the + (de)(cloud)pickle process messes with the `__globals__` attribute of the node + function, and since the node function is a class attribute the original node + gets updated on de-pickling. + We code around this, but lets make sure it stays working by adding a test! + Critical in this test is that the node used has complex type hints. + + C.f. `pyiron_workflow.function._wrapper_factory` for more detail. + """ + + wf = Workflow("depickle") + wf.create.register("atomistics", "pyiron_workflow.node_library.atomistics") + wf.structure = wf.create.atomistics.Bulk(name="Al") + wf.structure.executor = True + wf() + wf.structure.future.result() # Wait for it to finish + wf.structure.executor = False + wf.another_structure = wf.create.atomistics.Bulk(name="Cu") + wf() From 4289210fb660470ade57841896072d2c02710202 Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Mon, 30 Oct 2023 16:36:51 +0000 Subject: [PATCH 23/38] Format black --- pyiron_workflow/function.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyiron_workflow/function.py b/pyiron_workflow/function.py index 1136f1fc..058f79e7 100644 --- a/pyiron_workflow/function.py +++ b/pyiron_workflow/function.py @@ -632,6 +632,7 @@ def _wrapper_factory( An abstract base for making decorators that wrap a function as `Function` or its children. """ + # One really subtle thing is that we manually parse the function type hints right # here and include these as a class-level attribute. # This is because on (de)(cloud)pickling a function node, somehow the node function From bdcb1008eabf82c36c9a53ed9b1c18d44b30168a Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 30 Oct 2023 10:14:26 -0700 Subject: [PATCH 24/38] Make standard node package really standard --- pyiron_workflow/node_library/plotting.py | 24 ++++++++++++++++++++++++ pyiron_workflow/node_library/standard.py | 16 ++++------------ 2 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 pyiron_workflow/node_library/plotting.py diff --git a/pyiron_workflow/node_library/plotting.py b/pyiron_workflow/node_library/plotting.py new file mode 100644 index 00000000..eceaba3f --- /dev/null +++ b/pyiron_workflow/node_library/plotting.py @@ -0,0 +1,24 @@ +""" +For graphical representations of data. +""" + +from __future__ import annotations + +from typing import Optional + +import numpy as np + +from pyiron_workflow.function import single_value_node + + +@single_value_node(output_labels="fig") +def scatter( + x: Optional[list | np.ndarray] = None, y: Optional[list | np.ndarray] = None +): + from matplotlib import pyplot as plt + return plt.scatter(x, y) + + +nodes = [ + scatter, +] \ No newline at end of file diff --git a/pyiron_workflow/node_library/standard.py b/pyiron_workflow/node_library/standard.py index bf24fab5..caf3d5ad 100644 --- a/pyiron_workflow/node_library/standard.py +++ b/pyiron_workflow/node_library/standard.py @@ -1,22 +1,15 @@ +""" +Common-use nodes relying only on the standard library +""" + from __future__ import annotations from inspect import isclass -from typing import Optional - -import numpy as np -from matplotlib import pyplot as plt from pyiron_workflow.channels import NotData, OutputSignal from pyiron_workflow.function import SingleValue, single_value_node -@single_value_node(output_labels="fig") -def scatter( - x: Optional[list | np.ndarray] = None, y: Optional[list | np.ndarray] = None -): - return plt.scatter(x, y) - - @single_value_node() def user_input(user_input): return user_input @@ -52,7 +45,6 @@ def process_run_result(self, function_output): nodes = [ - scatter, user_input, If, ] From b00aedccd0385bf1093c8366d90aa06b18209b36 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 30 Oct 2023 10:14:48 -0700 Subject: [PATCH 25/38] Don't register atomistics by default --- pyiron_workflow/interfaces.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyiron_workflow/interfaces.py b/pyiron_workflow/interfaces.py index f4551637..92e1e9d7 100644 --- a/pyiron_workflow/interfaces.py +++ b/pyiron_workflow/interfaces.py @@ -46,7 +46,6 @@ def __init__(self): # If the CI skips testing on 3.9 gets dropped, we can think about removing # this if-clause and just letting users of python <3.10 hit an error. self.register("standard", "pyiron_workflow.node_library.standard") - self.register("atomistics", "pyiron_workflow.node_library.atomistics") @property def Macro(self): From 9e45028647e80ff5270a572577fe34da459684b5 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 30 Oct 2023 10:23:07 -0700 Subject: [PATCH 26/38] Purge atomistics module from unit tests --- tests/unit/test_workflow.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_workflow.py b/tests/unit/test_workflow.py index 0c0e09ae..54fa13c0 100644 --- a/tests/unit/test_workflow.py +++ b/tests/unit/test_workflow.py @@ -89,15 +89,16 @@ def test_node_removal(self): def test_node_packages(self): wf = Workflow("my_workflow") + wf.register("demo", "static.demo_nodes") # Test invocation - wf.create.atomistics.Bulk(cubic=True, element="Al") + wf.create.demo.Add(label="by_add") # Test invocation with attribute assignment - wf.engine = wf.create.atomistics.Lammps(structure=wf.bulk) + wf.by_assignment = wf.create.demo.Add() self.assertSetEqual( set(wf.nodes.keys()), - set(["bulk", "engine"]), + set(["by_add", "by_assignment"]), msg=f"Expected one node label generated automatically from the class and " f"the other from the attribute assignment, but got {wf.nodes.keys()}" ) From 0ffd8c9156fcf346231d419e7d4e48381961e69f Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 30 Oct 2023 10:24:05 -0700 Subject: [PATCH 27/38] Give the demo nodes complex typing --- tests/static/demo_nodes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/static/demo_nodes.py b/tests/static/demo_nodes.py index 2f3c3dab..6253fa7e 100644 --- a/tests/static/demo_nodes.py +++ b/tests/static/demo_nodes.py @@ -2,11 +2,14 @@ A demo node package for the purpose of testing node package registration. """ +from typing import Optional + from pyiron_workflow import Workflow @Workflow.wrap_as.single_value_node("sum") -def add(x: int, y: int) -> int: +def add(x: int, y: Optional[int] = None) -> int: + y = 0 if y is None else y return x + y From 3c2433b36373802a18e8fd0dd850a682eeafb639 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 30 Oct 2023 10:32:26 -0700 Subject: [PATCH 28/38] Make it easier to ensure you can import the static demo nodes --- pyiron_workflow/_tests.py | 15 +++++++++++++++ tests/unit/test_interfaces.py | 7 ++----- 2 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 pyiron_workflow/_tests.py diff --git a/pyiron_workflow/_tests.py b/pyiron_workflow/_tests.py new file mode 100644 index 00000000..bbebb826 --- /dev/null +++ b/pyiron_workflow/_tests.py @@ -0,0 +1,15 @@ +""" +Tools specifically for the test suite, not intended for general use. +""" + +from pathlib import Path +import sys + + +def ensure_tests_in_python_path(): + """So that you can import from the static module""" + path_to_tests = Path(__file__).parent.parent / "tests" + as_string = str(path_to_tests.resolve()) + + if as_string not in sys.path: + sys.path.append(as_string) diff --git a/tests/unit/test_interfaces.py b/tests/unit/test_interfaces.py index 182094bc..00da508a 100644 --- a/tests/unit/test_interfaces.py +++ b/tests/unit/test_interfaces.py @@ -1,8 +1,7 @@ -from pathlib import Path import sys from unittest import TestCase, skipUnless - +from pyiron_workflow._tests import ensure_tests_in_python_path from pyiron_workflow.interfaces import Creator @@ -13,9 +12,7 @@ class TestCreator(TestCase): @classmethod def setUpClass(cls) -> None: cls.creator = Creator() - path_to_tests = Path(__file__).parent.parent - sys.path.append(str(path_to_tests.resolve())) - # Now we can import from `static` + ensure_tests_in_python_path() def test_registration(self): From 33474ed909b207eb8b13c99c88a6dddc80e2fde1 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 30 Oct 2023 10:37:07 -0700 Subject: [PATCH 29/38] Use a node name that doesn't conflict with Composite methods --- tests/static/demo_nodes.py | 4 ++-- tests/unit/test_interfaces.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/static/demo_nodes.py b/tests/static/demo_nodes.py index 6253fa7e..f5c2fde9 100644 --- a/tests/static/demo_nodes.py +++ b/tests/static/demo_nodes.py @@ -8,9 +8,9 @@ @Workflow.wrap_as.single_value_node("sum") -def add(x: int, y: Optional[int] = None) -> int: +def optionally_add(x: int, y: Optional[int] = None) -> int: y = 0 if y is None else y return x + y -nodes = [add] +nodes = [optionally_add] diff --git a/tests/unit/test_interfaces.py b/tests/unit/test_interfaces.py index 00da508a..e53eb696 100644 --- a/tests/unit/test_interfaces.py +++ b/tests/unit/test_interfaces.py @@ -25,7 +25,7 @@ def test_registration(self): self.creator.register("demo", "static.demo_nodes") - node = self.creator.demo.Add(1, 2) + node = self.creator.demo.OptionallyAdd(1, 2) self.assertEqual( 3, node(), From 44e3a07727edcfae282d3be6f9912e3aa24fae6d Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 30 Oct 2023 10:42:00 -0700 Subject: [PATCH 30/38] Use demo instead of atomistics nodes --- tests/integration/test_workflow.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/integration/test_workflow.py b/tests/integration/test_workflow.py index 7f0b1f17..a6636441 100644 --- a/tests/integration/test_workflow.py +++ b/tests/integration/test_workflow.py @@ -2,6 +2,7 @@ import numpy as np +from pyiron_workflow._tests import ensure_tests_in_python_path from pyiron_workflow.channels import OutputSignal from pyiron_workflow.function import Function from pyiron_workflow.workflow import Workflow @@ -185,12 +186,13 @@ def test_executor_and_creator_interaction(self): C.f. `pyiron_workflow.function._wrapper_factory` for more detail. """ + ensure_tests_in_python_path() wf = Workflow("depickle") - wf.create.register("atomistics", "pyiron_workflow.node_library.atomistics") - wf.structure = wf.create.atomistics.Bulk(name="Al") - wf.structure.executor = True + wf.create.register("demo", "static.demo_nodes") + wf.before_pickling = wf.create.demo.OptionallyAdd(1) + wf.before_pickling.executor = True wf() - wf.structure.future.result() # Wait for it to finish - wf.structure.executor = False - wf.another_structure = wf.create.atomistics.Bulk(name="Cu") + wf.before_pickling.future.result() # Wait for it to finish + wf.before_pickling.executor = False + wf.after_pickling = wf.create.demo.OptionallyAdd(2, y=3) wf() From 19110a9e2177977c33a42ad9d39c42735d9a4ef1 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 30 Oct 2023 10:49:32 -0700 Subject: [PATCH 31/38] Make the registration shortcut a class method --- pyiron_workflow/composite.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index c7083564..3b1fce86 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -422,9 +422,10 @@ def replace(self, owned_node: Node | str, replacement: Node | type[Node]) -> Nod self.starting_nodes.append(replacement) return owned_node + @classmethod @wraps(Creator.register) - def register(self, domain: str, package_identifier: str) -> None: - self.create.register(domain=domain, package_identifier=package_identifier) + def register(cls, domain: str, package_identifier: str) -> None: + cls.create.register(domain=domain, package_identifier=package_identifier) def __setattr__(self, key: str, node: Node): if isinstance(node, Node) and key != "parent": From 664b06b9160a7fb89a570cc9a99491ab5838cec7 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 30 Oct 2023 10:49:45 -0700 Subject: [PATCH 32/38] Use the demo nodes instead of atomistics --- tests/integration/test_workflow.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/tests/integration/test_workflow.py b/tests/integration/test_workflow.py index a6636441..0dd6c06a 100644 --- a/tests/integration/test_workflow.py +++ b/tests/integration/test_workflow.py @@ -78,30 +78,27 @@ def numpy_sqrt(value=0): ) def test_for_loop(self): + ensure_tests_in_python_path() + Workflow.register("demo", "static.demo_nodes") + n = 5 bulk_loop = Workflow.create.meta.for_loop( - Workflow.create.atomistics.Bulk, + Workflow.create.demo.OptionallyAdd, n, - iterate_on=("a",), + iterate_on=("y",), )() + base = 42 + to_add = np.arange(n, dtype=int) out = bulk_loop( - name="Al", # Sent equally to each body node - A=np.linspace(3.9, 4.1, n).tolist(), # Distributed across body nodes + x=base, # Sent equally to each body node + Y=to_add.tolist(), # Distributed across body nodes ) self.assertTrue( - np.allclose( - [struct.cell.volume for struct in out.STRUCTURE], - [ - 14.829749999999995, - 15.407468749999998, - 15.999999999999998, - 16.60753125, - 17.230249999999995 - ] - ) + np.allclose([added for added in out.SUM], to_add + base), + msg="Output should be list result of each individiual result" ) def test_while_loop(self): From 7e20641a4a73878a01f37bac22cee9e6075e044b Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 30 Oct 2023 11:51:36 -0700 Subject: [PATCH 33/38] :bug: fix node name typo --- tests/unit/test_workflow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_workflow.py b/tests/unit/test_workflow.py index 54fa13c0..86b69c27 100644 --- a/tests/unit/test_workflow.py +++ b/tests/unit/test_workflow.py @@ -92,9 +92,9 @@ def test_node_packages(self): wf.register("demo", "static.demo_nodes") # Test invocation - wf.create.demo.Add(label="by_add") + wf.create.demo.OptionalAdd(label="by_add") # Test invocation with attribute assignment - wf.by_assignment = wf.create.demo.Add() + wf.by_assignment = wf.create.demo.OptionalAdd() self.assertSetEqual( set(wf.nodes.keys()), From 6189e02c6b85b5665ca29c230dea921530b09116 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 30 Oct 2023 12:18:37 -0700 Subject: [PATCH 34/38] :bug: when you fix it use the right damned name --- tests/unit/test_workflow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_workflow.py b/tests/unit/test_workflow.py index 86b69c27..7d82dbff 100644 --- a/tests/unit/test_workflow.py +++ b/tests/unit/test_workflow.py @@ -92,9 +92,9 @@ def test_node_packages(self): wf.register("demo", "static.demo_nodes") # Test invocation - wf.create.demo.OptionalAdd(label="by_add") + wf.create.demo.OptionallyAdd(label="by_add") # Test invocation with attribute assignment - wf.by_assignment = wf.create.demo.OptionalAdd() + wf.by_assignment = wf.create.demo.OptionallyAdd() self.assertSetEqual( set(wf.nodes.keys()), From 8722e332009e7913700ca425fbc73627447d72e5 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 30 Oct 2023 16:10:18 -0700 Subject: [PATCH 35/38] PEP8 whitespace --- tests/unit/test_workflow.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/test_workflow.py b/tests/unit/test_workflow.py index 7d82dbff..fc7b8775 100644 --- a/tests/unit/test_workflow.py +++ b/tests/unit/test_workflow.py @@ -86,7 +86,6 @@ def test_node_removal(self): msg="Removal should also remove from starting nodes" ) - def test_node_packages(self): wf = Workflow("my_workflow") wf.register("demo", "static.demo_nodes") From 00f8690b47e5f5a286629294aacb33da193d8c03 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 30 Oct 2023 16:44:02 -0700 Subject: [PATCH 36/38] Update and rerun example notebook --- notebooks/workflow_example.ipynb | 84 +++++++++++++++++++------------- 1 file changed, 50 insertions(+), 34 deletions(-) diff --git a/notebooks/workflow_example.ipynb b/notebooks/workflow_example.ipynb index 229760f8..defccc32 100644 --- a/notebooks/workflow_example.ipynb +++ b/notebooks/workflow_example.ipynb @@ -644,8 +644,8 @@ { "data": { "text/plain": [ - "array([0.91077351, 0.33860412, 0.59806048, 0.66528464, 0.80125293,\n", - " 0.31981677, 0.54395521, 0.4926537 , 0.52626431, 0.7848854 ])" + "array([0.49455794, 0.6789772 , 0.48470916, 0.43574953, 0.18030331,\n", + " 0.6059215 , 0.65871187, 0.42205006, 0.65062977, 0.5390317 ])" ] }, "execution_count": 22, @@ -654,7 +654,7 @@ }, { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] @@ -1209,7 +1209,7 @@ "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 28, @@ -1228,7 +1228,13 @@ "source": [ "# Example with pre-built nodes\n", "\n", - "Currently we have a handfull of pre-build nodes available for import from the `nodes` package. Let's use these to quickly put together a workflow for looking at some MD data." + "Currently we have a handfull of pre-build nodes available for import from the `nodes` package. Let's use these to quickly put together a workflow for looking at some MD data.\n", + "\n", + "To access prebuilt nodes we can `.create` them. This works both from the workflow class _and_ from a workflow instance. In the latter case, created nodes automatically take the creating workflow instance as their `parent`.\n", + "\n", + "There are a few of nodes that are always available under the `Workflow.create.standard` namespace, otherwise we need to register new node packages. This is done with the `register` method, which takes the domain (namespace/key/attribute/whatever you want to call it) under which you want to register the new nodes, and a string import path to a module that has a list of nodes under the name `nodes`, i.e. the module has the property `nodes: list[pyiron_workflow.nodes.Node]`. (This API is subject to change, as we work to improve usability and bring node packages more and more in line with \"FAIR\" principles.)\n", + "\n", + "You can make your own `.py` files with nodes for reuse this way, but `pyiron_workflow` also comes with a couple of packages. In this example we'll use atomistics and plotting:" ] }, { @@ -1240,7 +1246,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "a289b513c50d41989670c5b4ac9df823", + "model_id": "4f07c83c4a694b76847e9060c58c00d0", "version_major": 2, "version_minor": 0 }, @@ -1249,14 +1255,6 @@ "metadata": {}, "output_type": "display_data" }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:158: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", - " warn(\n" - ] - }, { "name": "stdout", "output_type": "stream", @@ -1267,7 +1265,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 29, @@ -1286,16 +1284,18 @@ } ], "source": [ + "wf.register(\"atomistics\", \"pyiron_workflow.node_library.atomistics\")\n", + "wf.register(\"plotting\", \"pyiron_workflow.node_library.plotting\")\n", + "\n", "wf = Workflow(\"with_prebuilt\")\n", "\n", "wf.structure = wf.create.atomistics.Bulk(cubic=True, name=\"Al\")\n", "wf.engine = wf.create.atomistics.Lammps(structure=wf.structure)\n", "wf.calc = wf.create.atomistics.CalcMd(job=wf.engine)\n", - "wf.plot = wf.create.standard.Scatter(\n", + "wf.plot = wf.create.plotting.Scatter(\n", " x=wf.calc.outputs.steps, \n", " y=wf.calc.outputs.temperature\n", ")\n", - "wf.structure > wf.engine > wf.calc > wf.plot\n", "\n", "out = wf.run()\n", "out.plot__fig" @@ -1330,27 +1330,27 @@ "clusterwith_prebuilt\n", "\n", "with_prebuilt: Workflow\n", - "\n", - "clusterwith_prebuiltInputs\n", + "\n", + "clusterwith_prebuiltOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Outputs\n", "\n", - "\n", - "clusterwith_prebuiltOutputs\n", + "\n", + "clusterwith_prebuiltInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Outputs\n", + "\n", + "Inputs\n", "\n", "\n", "\n", @@ -1519,7 +1519,7 @@ "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 30, @@ -1531,6 +1531,14 @@ "wf.draw(depth=0)" ] }, + { + "cell_type": "markdown", + "id": "b2990bbf-28fb-43e2-a01d-82377d12879c", + "metadata": {}, + "source": [ + "Note: the `draw` call returns a `graphviz.graphs.Digraphs` object; these get natively rendered alright in jupyter notebooks, as seen above, but you can also snag the object in a variable and do everything else graphviz allows, e.g. using the `render` method on the object to save it to file. Cf. the graphviz docs for details." + ] + }, { "cell_type": "markdown", "id": "d1f3b308-28b2-466b-8cf5-6bfd806c08ca", @@ -2939,7 +2947,7 @@ "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 36, @@ -2982,7 +2990,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:158: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:158: UserWarning: The channel ran was not connected to run, andthus could not disconnect from it.\n", " warn(\n" ] }, @@ -3057,6 +3065,14 @@ "id": "ed4a3a22-fc3a-44c9-9d4f-c65bc1288889", "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:158: UserWarning: The channel ran was not connected to run, andthus could not disconnect from it.\n", + " warn(\n" + ] + }, { "name": "stdout", "output_type": "stream", @@ -3083,7 +3099,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:158: UserWarning: The channel run was not connected to ran, andthus could not disconnect from it.\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:158: UserWarning: The channel ran was not connected to run, andthus could not disconnect from it.\n", " warn(\n" ] }, @@ -3137,7 +3153,7 @@ "\n", "Serialization doesn't exist yet.\n", "\n", - "What you _can_ do is `register` new lists of nodes (including macros) with the workflow, so feel free to build up your own `.py` files containing nodes you like to use for easy re-use.\n", + "What you _can_ do is `register` new lists of nodes (including macros) with the workflow, so feel free to build up your own `.py` files containing nodes you like to use for easy re-use. Registration is now discussed in the main body of the notebook, but the API may change significantly going forward.\n", "\n", "Serialization of workflows is still forthcoming, while for node registration flexibility and documentation is forthcoming but the basics are here already." ] @@ -3307,8 +3323,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "0.064 <= 0.2\n", - "Finally 0.064\n" + "0.012 <= 0.2\n", + "Finally 0.012\n" ] } ], From d8695aedcf343a2b9c74e6b9ad8387506d54d5e7 Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Mon, 30 Oct 2023 23:45:16 +0000 Subject: [PATCH 37/38] Format black --- pyiron_workflow/node_library/plotting.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyiron_workflow/node_library/plotting.py b/pyiron_workflow/node_library/plotting.py index eceaba3f..fd4e4355 100644 --- a/pyiron_workflow/node_library/plotting.py +++ b/pyiron_workflow/node_library/plotting.py @@ -16,9 +16,10 @@ def scatter( x: Optional[list | np.ndarray] = None, y: Optional[list | np.ndarray] = None ): from matplotlib import pyplot as plt + return plt.scatter(x, y) nodes = [ scatter, -] \ No newline at end of file +] From 23b88275b778ff76366345c83b4aee04ab4c4a3e Mon Sep 17 00:00:00 2001 From: liamhuber Date: Mon, 30 Oct 2023 16:57:43 -0700 Subject: [PATCH 38/38] Add a note on registration and macros --- pyiron_workflow/interfaces.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyiron_workflow/interfaces.py b/pyiron_workflow/interfaces.py index 92e1e9d7..d766c1e5 100644 --- a/pyiron_workflow/interfaces.py +++ b/pyiron_workflow/interfaces.py @@ -93,7 +93,12 @@ def register(self, domain: str, package_identifier: str) -> None: """ Add a new package of nodes under the provided attribute, e.g. after adding nodes to the domain `"my_nodes"`, and instance of creator can call things like - `creator.my_nodes.some_node_that_is_there()` + `creator.my_nodes.some_node_that_is_there()`. + + Note: If a macro is going to use a creator, the node registration should be + _inside_ the macro definition to make sure the node actually has access to + those nodes! It also needs to be _able_ to register those nodes, i.e. have + import access to that location, but we don't for that check that. Args: domain (str): The attribute name at which to register the new package.