From d7c780179ccf55671f4b84463db56c5e781278a1 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 6 Feb 2024 15:03:19 -0800 Subject: [PATCH 01/18] Introduce and test an importability property --- pyiron_workflow/composite.py | 4 ++++ pyiron_workflow/node.py | 22 ++++++++++++++++++++ tests/static/demo_nodes.py | 8 +++++++- tests/unit/test_composite.py | 40 ++++++++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index e63bc00b..476b54e3 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -763,3 +763,7 @@ def _restore_signal_connections_from_strings( self._get_signals_input, self._get_signals_output, ) + + @property + def import_ready(self) -> bool: + return super().import_ready and all(node.import_ready for node in self) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 7a8e6759..6e5b479a 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -10,6 +10,7 @@ import warnings from abc import ABC, abstractmethod from concurrent.futures import Executor as StdLibExecutor, Future +from importlib import import_module from typing import Any, Literal, Optional, TYPE_CHECKING from pyiron_workflow.channels import ( @@ -231,6 +232,8 @@ class Node(HasToDict, ABC, metaclass=AbstractHasPost): connected. future (concurrent.futures.Future | None): A futures object, if the node is currently running or has already run using an executor. + import_ready (bool): Whether importing the node's class from its class's module + returns the same thing as its type. (Recursive on sub-nodes for composites.) inputs (pyiron_workflow.io.Inputs): **Abstract.** Children must define a property returning an :class:`Inputs` object. label (str): A name for the node. @@ -1224,3 +1227,22 @@ def tidy_working_directory(self): self._working_directory = None # Touching the working directory may have created it -- if it's there and # empty just clean it up + + @property + def import_ready(self) -> bool: + """ + Checks whether `importlib` can find this node's class, and if so whether the + imported object matches the node's type. + + Returns: + (bool): Whether the imported module and name of this node's class match + its type. + """ + try: + module = self.__class__.__module__ + class_ = getattr(import_module(module), self.__class__.__name__) + if module == "__main__": + warnings.warn(f"{self.label} is only defined in __main__") + return type(self) is class_ + except (ModuleNotFoundError, AttributeError): + return False diff --git a/tests/static/demo_nodes.py b/tests/static/demo_nodes.py index ae6c4d91..201ab514 100644 --- a/tests/static/demo_nodes.py +++ b/tests/static/demo_nodes.py @@ -27,4 +27,10 @@ def AddPlusOne(obj, other): return obj + other + 1 -nodes = [OptionallyAdd, AddThree, AddPlusOne] +def dynamic(x): + return x + 1 + + +Dynamic = Workflow.wrap_as.single_value_node()(dynamic) + +nodes = [OptionallyAdd, AddThree, AddPlusOne, Dynamic] diff --git a/tests/unit/test_composite.py b/tests/unit/test_composite.py index 4c4e4d3b..87321639 100644 --- a/tests/unit/test_composite.py +++ b/tests/unit/test_composite.py @@ -652,6 +652,46 @@ def test_graph_info(self): "from all depths." ) + def test_import_ready(self): + self.comp.register("static.demo_nodes", "demo") + + totally_findable = Composite.create.demo.OptionallyAdd() + self.assertTrue( + totally_findable.import_ready, + msg="The node class is well defined and in an importable module" + ) + bad_class = Composite.create.demo.dynamic() + self.assertFalse( + bad_class.import_ready, + msg="The node is in an importable location, but the imported object is not " + "the node class (but rather the node function)" + ) + og_module = totally_findable.__class__.__module__ + totally_findable.__class__.__module__ = "something I totally made up" + self.assertFalse( + totally_findable.import_ready, + msg="The node class is well defined, but the module is not in the python " + "path so import fails" + ) + totally_findable.__class__.__module__ = og_module # Fix what you broke + + self.assertTrue( + self.comp.import_ready, + msg="Sanity check on initial condition -- tests are in the path, so this " + "is importable" + ) + self.comp.totally_findable = totally_findable + print(self.comp.import_ready, self.comp.import_ready, self.comp.node_labels) + self.assertTrue( + self.comp.import_ready, + msg="Adding importable children should leave the parent import-ready" + ) + self.comp.bad_class = bad_class + self.assertFalse( + self.comp.import_ready, + msg="Adding un-importable children should make the parent not import ready" + ) + if __name__ == '__main__': unittest.main() From b361523a73cd167577e6b25f743a03f78f757a49 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 6 Feb 2024 15:03:37 -0800 Subject: [PATCH 02/18] Extend node package count --- tests/unit/test_node_package.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_node_package.py b/tests/unit/test_node_package.py index c74445fe..3abb7797 100644 --- a/tests/unit/test_node_package.py +++ b/tests/unit/test_node_package.py @@ -36,7 +36,7 @@ def test_nodes(self): def test_length(self): package = NodePackage("static.demo_nodes") - self.assertEqual(3, len(package)) + self.assertEqual(4, len(package)) if __name__ == '__main__': From bd388b1dbcacab1912dd04ef77ae70b8a7df6a1e Mon Sep 17 00:00:00 2001 From: liamhuber Date: Tue, 6 Feb 2024 15:09:02 -0800 Subject: [PATCH 03/18] Fail saving early if h5io won't be able to import your nodes --- pyiron_workflow/storage.py | 14 ++++++++++++++ tests/unit/test_workflow.py | 6 +++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/pyiron_workflow/storage.py b/pyiron_workflow/storage.py index 94937e14..e7aef538 100644 --- a/pyiron_workflow/storage.py +++ b/pyiron_workflow/storage.py @@ -18,6 +18,13 @@ ALLOWED_BACKENDS = ["h5io", "tinybase"] +class TypeNotFoundError(ImportError): + """ + Raised when you try to save a node, but importing its module and class give + something other than its type. + """ + + class StorageInterface: _TINYBASE_STORAGE_FILE_NAME = "project.h5" @@ -41,6 +48,13 @@ def save(self, backend: Literal["h5io", "tinybase"]): def _save(self, backend: Literal["h5io", "tinybase"]): if backend == "h5io": + if not self.node.import_ready: + raise TypeNotFoundError( + f"{self.node.label} cannot be saved with h5io because it (or one " + f"of its child nodes) has a type that cannot be imported. Did you " + f"dynamically define this node? Try using the node wrapper as a " + f"decorator instead." + ) h5io.write_hdf5( fname=self._h5io_storage_file_path, data=self.node, diff --git a/tests/unit/test_workflow.py b/tests/unit/test_workflow.py index 9db3d8e2..6b03bec0 100644 --- a/tests/unit/test_workflow.py +++ b/tests/unit/test_workflow.py @@ -6,6 +6,7 @@ from pyiron_workflow._tests import ensure_tests_in_python_path from pyiron_workflow.channels import NOT_DATA from pyiron_workflow.snippets.dotdict import DotDict +from pyiron_workflow.storage import TypeNotFoundError from pyiron_workflow.workflow import Workflow @@ -415,13 +416,12 @@ def UnimportableScope(x): wf.unimportable_scope = UnimportableScope() try: - wf.save(backend="h5io") with self.assertRaises( - AttributeError, + TypeNotFoundError, msg="Nodes must live in an importable scope to save with the h5io " "backend" ): - Workflow(wf.label, storage_backend="h5io") + wf.save(backend="h5io") finally: wf.remove_node(wf.unimportable_scope) wf.storage.delete() From 8ced849525ccea94461f8bbd40532a25bd9b089c Mon Sep 17 00:00:00 2001 From: Liam Huber Date: Wed, 7 Feb 2024 10:10:24 -0800 Subject: [PATCH 04/18] try...finally changing the classname Co-authored-by: Niklas Siemer <70580458+niklassiemer@users.noreply.github.com> --- tests/unit/test_composite.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/unit/test_composite.py b/tests/unit/test_composite.py index 87321639..bb748512 100644 --- a/tests/unit/test_composite.py +++ b/tests/unit/test_composite.py @@ -666,14 +666,17 @@ def test_import_ready(self): msg="The node is in an importable location, but the imported object is not " "the node class (but rather the node function)" ) - og_module = totally_findable.__class__.__module__ - totally_findable.__class__.__module__ = "something I totally made up" - self.assertFalse( - totally_findable.import_ready, - msg="The node class is well defined, but the module is not in the python " - "path so import fails" - ) - totally_findable.__class__.__module__ = og_module # Fix what you broke + with self.subTest(msg="Made up class"): + try: + og_module = totally_findable.__class__.__module__ + totally_findable.__class__.__module__ = "something I totally made up" + self.assertFalse( + totally_findable.import_ready, + msg="The node class is well defined, but the module is not in the python " + "path so import fails" + ) + finally: + totally_findable.__class__.__module__ = og_module # Fix what you broke self.assertTrue( self.comp.import_ready, From 6b0bf50d484547aed87a85ee61524627983c1312 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 7 Feb 2024 10:12:01 -0800 Subject: [PATCH 05/18] Slide var used in finally outside the try --- tests/unit/test_composite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_composite.py b/tests/unit/test_composite.py index bb748512..80c19843 100644 --- a/tests/unit/test_composite.py +++ b/tests/unit/test_composite.py @@ -667,8 +667,8 @@ def test_import_ready(self): "the node class (but rather the node function)" ) with self.subTest(msg="Made up class"): + og_module = totally_findable.__class__.__module__ try: - og_module = totally_findable.__class__.__module__ totally_findable.__class__.__module__ = "something I totally made up" self.assertFalse( totally_findable.import_ready, From 604a8cec685165173d232d34a61ff1b8a1b52eae Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 7 Feb 2024 10:12:14 -0800 Subject: [PATCH 06/18] Fix line length --- tests/unit/test_composite.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_composite.py b/tests/unit/test_composite.py index 80c19843..ee07eab2 100644 --- a/tests/unit/test_composite.py +++ b/tests/unit/test_composite.py @@ -672,8 +672,8 @@ def test_import_ready(self): totally_findable.__class__.__module__ = "something I totally made up" self.assertFalse( totally_findable.import_ready, - msg="The node class is well defined, but the module is not in the python " - "path so import fails" + msg="The node class is well defined, but the module is not in the " + "python path so import fails" ) finally: totally_findable.__class__.__module__ = og_module # Fix what you broke From 6d0903aeacd54de781a440d8d77384869d23bed7 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 7 Feb 2024 10:12:30 -0800 Subject: [PATCH 07/18] Remove debug print --- tests/unit/test_composite.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/test_composite.py b/tests/unit/test_composite.py index ee07eab2..00b077f4 100644 --- a/tests/unit/test_composite.py +++ b/tests/unit/test_composite.py @@ -684,7 +684,6 @@ def test_import_ready(self): "is importable" ) self.comp.totally_findable = totally_findable - print(self.comp.import_ready, self.comp.import_ready, self.comp.node_labels) self.assertTrue( self.comp.import_ready, msg="Adding importable children should leave the parent import-ready" From 519f176ca6018d14b3f7f0ab2ab4c7591f3355c6 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 7 Feb 2024 10:13:00 -0800 Subject: [PATCH 08/18] Fix test message --- tests/unit/test_composite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_composite.py b/tests/unit/test_composite.py index 00b077f4..03574259 100644 --- a/tests/unit/test_composite.py +++ b/tests/unit/test_composite.py @@ -666,7 +666,7 @@ def test_import_ready(self): msg="The node is in an importable location, but the imported object is not " "the node class (but rather the node function)" ) - with self.subTest(msg="Made up class"): + with self.subTest(msg="Made up module"): og_module = totally_findable.__class__.__module__ try: totally_findable.__class__.__module__ = "something I totally made up" From 284b9ca7b0c02af9b9aaa3bdbaa4b6604c4a341a Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 7 Feb 2024 10:25:02 -0800 Subject: [PATCH 09/18] Protect tinybase from object type-import type mismatches --- pyiron_workflow/storage.py | 14 +++++++------- tests/unit/test_workflow.py | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/pyiron_workflow/storage.py b/pyiron_workflow/storage.py index e7aef538..22c5d795 100644 --- a/pyiron_workflow/storage.py +++ b/pyiron_workflow/storage.py @@ -47,14 +47,14 @@ def save(self, backend: Literal["h5io", "tinybase"]): root.storage.save(backend=backend) def _save(self, backend: Literal["h5io", "tinybase"]): + if not self.node.import_ready: + raise TypeNotFoundError( + f"{self.node.label} cannot be saved with h5io because it (or one " + f"of its child nodes) has a type that cannot be imported. Did you " + f"dynamically define this node? Try using the node wrapper as a " + f"decorator instead." + ) if backend == "h5io": - if not self.node.import_ready: - raise TypeNotFoundError( - f"{self.node.label} cannot be saved with h5io because it (or one " - f"of its child nodes) has a type that cannot be imported. Did you " - f"dynamically define this node? Try using the node wrapper as a " - f"decorator instead." - ) h5io.write_hdf5( fname=self._h5io_storage_file_path, data=self.node, diff --git a/tests/unit/test_workflow.py b/tests/unit/test_workflow.py index 6b03bec0..f3a59a84 100644 --- a/tests/unit/test_workflow.py +++ b/tests/unit/test_workflow.py @@ -382,6 +382,20 @@ def test_storage_scopes(self): finally: wf.storage.delete() + with self.subTest("No unimportable nodes for either back-end"): + try: + wf.import_type_mismatch = wf.create.demo.dynamic() + for backend in ["h5io", "tinybase"]: + with self.subTest(backend): + with self.assertRaises( + TypeNotFoundError, + msg="Imported object is function but node type is node -- " + "should fail early on save" + ): + wf.save(backend=backend) + finally: + wf.remove_node(wf.import_type_mismatch) + wf.add_node(PlusOne(label="local_but_importable")) try: wf.save(backend="h5io") From 9af564a33a8f944dc17073a4399f3e744b7b6309 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 7 Feb 2024 10:25:15 -0800 Subject: [PATCH 10/18] Update the storage docs on Node --- pyiron_workflow/node.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 6e5b479a..44d2ecd0 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -157,6 +157,13 @@ class Node(HasToDict, ABC, metaclass=AbstractHasPost): - On instantiation, nodes will load automatically if they find saved content. - Discovered content can instead be deleted with a kwarg. - You can't load saved content _and_ run after instantiation at once. + - The nodes must be somewhere importable, and the imported object must match + the type of the node being saved. This basically just rules out one edge + case where a node class is defined like + `SomeFunctionNode = Workflow.wrap_as.function_node()(some_function)`, since + then the new class gets the name `some_function`, which when imported is + the _function_ "some_function" and not the desired class "SomeFunctionNode". + This is checked for at save-time and will cause a nice early failure. - [ALPHA ISSUE] If the source code (cells, `.py` files...) for a saved graph is altered between saving and loading the graph, there are no guarantees about the loaded state; depending on the nature of the changes everything may From a59e01ffcc1f69b3e04e62103584e70a9a912b6c Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 7 Feb 2024 10:35:37 -0800 Subject: [PATCH 11/18] Add a convenience method for reporting importability --- pyiron_workflow/composite.py | 5 +++++ pyiron_workflow/node.py | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 476b54e3..5a2adc70 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -767,3 +767,8 @@ def _restore_signal_connections_from_strings( @property def import_ready(self) -> bool: return super().import_ready and all(node.import_ready for node in self) + + def import_readiness_report(self, tabs=0): + super().import_readiness_report(tabs=tabs) + for node in self: + node.import_readiness_report(tabs=tabs + 1) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 6e5b479a..8bc7e12a 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -1246,3 +1246,9 @@ def import_ready(self) -> bool: return type(self) is class_ except (ModuleNotFoundError, AttributeError): return False + + def import_readiness_report(self, tabs=0): + tabspace = tabs * "\t" + print( + f"{tabspace}{self.label}: {'ok' if self.import_ready else 'NOT IMPORTABLE'}" + ) From 1a99af7c551afc936deca1bb8c86168c9c281f6d Mon Sep 17 00:00:00 2001 From: Liam Huber Date: Wed, 7 Feb 2024 12:29:30 -0800 Subject: [PATCH 12/18] Good catch by Niklas Co-authored-by: Niklas Siemer <70580458+niklassiemer@users.noreply.github.com> --- pyiron_workflow/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyiron_workflow/storage.py b/pyiron_workflow/storage.py index 22c5d795..cc977cff 100644 --- a/pyiron_workflow/storage.py +++ b/pyiron_workflow/storage.py @@ -49,7 +49,7 @@ def save(self, backend: Literal["h5io", "tinybase"]): def _save(self, backend: Literal["h5io", "tinybase"]): if not self.node.import_ready: raise TypeNotFoundError( - f"{self.node.label} cannot be saved with h5io because it (or one " + f"{self.node.label} cannot be saved because it (or one " f"of its child nodes) has a type that cannot be imported. Did you " f"dynamically define this node? Try using the node wrapper as a " f"decorator instead." From 87366936cd1c6403a6daa72b89abf8b9efa4ab05 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 7 Feb 2024 12:40:34 -0800 Subject: [PATCH 13/18] Return a string instead of printing --- pyiron_workflow/composite.py | 7 ++++--- pyiron_workflow/node.py | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 5a2adc70..e82cb5e0 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -768,7 +768,8 @@ def _restore_signal_connections_from_strings( def import_ready(self) -> bool: return super().import_ready and all(node.import_ready for node in self) - def import_readiness_report(self, tabs=0): - super().import_readiness_report(tabs=tabs) + def import_readiness_report(self, tabs=0, report_so_far=""): + report = super().import_readiness_report(tabs=tabs, report_so_far=report_so_far) for node in self: - node.import_readiness_report(tabs=tabs + 1) + report = node.import_readiness_report(tabs=tabs + 1, report_so_far=report) + return report diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 8bc7e12a..526a25c2 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -1247,8 +1247,8 @@ def import_ready(self) -> bool: except (ModuleNotFoundError, AttributeError): return False - def import_readiness_report(self, tabs=0): + def import_readiness_report(self, tabs=0, report_so_far=""): + newline = "\n" if len(report_so_far) > 0 else "" tabspace = tabs * "\t" - print( - f"{tabspace}{self.label}: {'ok' if self.import_ready else 'NOT IMPORTABLE'}" - ) + return report_so_far + f"{newline}{tabspace}{self.label}: " \ + f"{'ok' if self.import_ready else 'NOT IMPORTABLE'}" From 88a0f8a11894d81d0704750990e6ffb0b85ce66f Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 7 Feb 2024 12:40:47 -0800 Subject: [PATCH 14/18] Add the report to the error message --- pyiron_workflow/storage.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyiron_workflow/storage.py b/pyiron_workflow/storage.py index e7aef538..404cbf2d 100644 --- a/pyiron_workflow/storage.py +++ b/pyiron_workflow/storage.py @@ -53,7 +53,9 @@ def _save(self, backend: Literal["h5io", "tinybase"]): f"{self.node.label} cannot be saved with h5io because it (or one " f"of its child nodes) has a type that cannot be imported. Did you " f"dynamically define this node? Try using the node wrapper as a " - f"decorator instead." + f"decorator instead. \n" + f"Import readiness report: \n" + f"{self.node.import_readiness_report()}" ) h5io.write_hdf5( fname=self._h5io_storage_file_path, From efafe97a84128e493eb1b8a4472ebdaf3acaffb3 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 7 Feb 2024 12:42:07 -0800 Subject: [PATCH 15/18] Refactor: rename method --- pyiron_workflow/composite.py | 6 +++--- pyiron_workflow/node.py | 2 +- pyiron_workflow/storage.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index e82cb5e0..22a37ff9 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -768,8 +768,8 @@ def _restore_signal_connections_from_strings( def import_ready(self) -> bool: return super().import_ready and all(node.import_ready for node in self) - def import_readiness_report(self, tabs=0, report_so_far=""): - report = super().import_readiness_report(tabs=tabs, report_so_far=report_so_far) + def _report_import_readiness(self, tabs=0, report_so_far=""): + report = super()._report_import_readiness(tabs=tabs, report_so_far=report_so_far) for node in self: - report = node.import_readiness_report(tabs=tabs + 1, report_so_far=report) + report = node._report_import_readiness(tabs=tabs + 1, report_so_far=report) return report diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 526a25c2..d3eeefbc 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -1247,7 +1247,7 @@ def import_ready(self) -> bool: except (ModuleNotFoundError, AttributeError): return False - def import_readiness_report(self, tabs=0, report_so_far=""): + def _report_import_readiness(self, tabs=0, report_so_far=""): newline = "\n" if len(report_so_far) > 0 else "" tabspace = tabs * "\t" return report_so_far + f"{newline}{tabspace}{self.label}: " \ diff --git a/pyiron_workflow/storage.py b/pyiron_workflow/storage.py index 404cbf2d..02a7023c 100644 --- a/pyiron_workflow/storage.py +++ b/pyiron_workflow/storage.py @@ -55,7 +55,7 @@ def _save(self, backend: Literal["h5io", "tinybase"]): f"dynamically define this node? Try using the node wrapper as a " f"decorator instead. \n" f"Import readiness report: \n" - f"{self.node.import_readiness_report()}" + f"{self.node._report_import_readiness()}" ) h5io.write_hdf5( fname=self._h5io_storage_file_path, From 51ec2e4eaddc240b1eb4fa87aa1fa7fd6dade8ab Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 7 Feb 2024 12:43:29 -0800 Subject: [PATCH 16/18] Add back a property on the old name Which prints so it formats nicely without "\n"/"\t" characters when called --- pyiron_workflow/node.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index d3eeefbc..6a8e502a 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -1247,6 +1247,10 @@ def import_ready(self) -> bool: except (ModuleNotFoundError, AttributeError): return False + @property + def import_readiness_report(self): + print(self._report_import_readiness()) + def _report_import_readiness(self, tabs=0, report_so_far=""): newline = "\n" if len(report_so_far) > 0 else "" tabspace = tabs * "\t" From 8c463dcabe7792296fff972af830cae5cf3a2292 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 7 Feb 2024 12:56:35 -0800 Subject: [PATCH 17/18] :bug: Fix a string typo that snuck in on the github web merger Would have noticed this in the CI prior to merging the stack, but I've been completely ignoring the CI while we wait for the dependencies to actually release --- pyiron_workflow/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyiron_workflow/storage.py b/pyiron_workflow/storage.py index e6e2e2c8..545e6541 100644 --- a/pyiron_workflow/storage.py +++ b/pyiron_workflow/storage.py @@ -54,7 +54,7 @@ def _save(self, backend: Literal["h5io", "tinybase"]): f"dynamically define this node? Try using the node wrapper as a " f"decorator instead. \n" f"Import readiness report: \n" - f"{self.node._report_import_readiness()}"" + f"{self.node._report_import_readiness()}" ) if backend == "h5io": h5io.write_hdf5( From 8bbaf32eed7ed24bb8b8cd20084c018e08f4a29b Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Wed, 7 Feb 2024 20:58:10 +0000 Subject: [PATCH 18/18] Format black --- pyiron_workflow/composite.py | 4 +++- pyiron_workflow/node.py | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 22a37ff9..bef971dc 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -769,7 +769,9 @@ def import_ready(self) -> bool: return super().import_ready and all(node.import_ready for node in self) def _report_import_readiness(self, tabs=0, report_so_far=""): - report = super()._report_import_readiness(tabs=tabs, report_so_far=report_so_far) + report = super()._report_import_readiness( + tabs=tabs, report_so_far=report_so_far + ) for node in self: report = node._report_import_readiness(tabs=tabs + 1, report_so_far=report) return report diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index cf423dd6..b7096d41 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -1261,5 +1261,7 @@ def import_readiness_report(self): def _report_import_readiness(self, tabs=0, report_so_far=""): newline = "\n" if len(report_so_far) > 0 else "" tabspace = tabs * "\t" - return report_so_far + f"{newline}{tabspace}{self.label}: " \ - f"{'ok' if self.import_ready else 'NOT IMPORTABLE'}" + return ( + report_so_far + f"{newline}{tabspace}{self.label}: " + f"{'ok' if self.import_ready else 'NOT IMPORTABLE'}" + )