From b42d8291b8ab0c476cfa5938aeba19603dfcd9d5 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 2 Oct 2025 13:05:13 -0700 Subject: [PATCH 01/14] Copy retrieve from bagofholding Signed-off-by: liamhuber --- pyiron_snippets/retrieve.py | 64 +++++++++++++++++++++++++++++++++++++ tests/unit/test_retrieve.py | 15 +++++++++ 2 files changed, 79 insertions(+) create mode 100644 pyiron_snippets/retrieve.py create mode 100644 tests/unit/test_retrieve.py diff --git a/pyiron_snippets/retrieve.py b/pyiron_snippets/retrieve.py new file mode 100644 index 0000000..356d026 --- /dev/null +++ b/pyiron_snippets/retrieve.py @@ -0,0 +1,64 @@ +""" +Helper functions for managing the relationship between strings and imports. +""" + +from __future__ import annotations + +import importlib +from typing import Any + +from bagofholding.exceptions import StringNotImportableError + + +def import_from_string(library_path: str) -> Any: + split_path = library_path.split(".", 1) + if len(split_path) == 1: + module_name, path = split_path[0], "" + else: + module_name, path = split_path + obj = importlib.import_module(module_name) + for k in path.split("."): + try: + obj = getattr(obj, k) + except AttributeError: + # Try importing as a submodule + # This can be necessary of an __init__.py is empty and nothing else has + # referenced the module yet + current_path = f"{obj.__name__}.{k}" + try: + obj = importlib.import_module(current_path) + except ImportError as e: + raise AttributeError( + f"module '{obj.__name__}' has no attribute '{k}'" + ) from e + return obj + + +def get_importable_string_from_string_reduction( + string_reduction: str, reduced_object: object +) -> str: + """ + Per the pickle docs: + + > If a string is returned, the string should be interpreted as the name of a global + variable. It should be the object’s local name relative to its module; the pickle + module searches the module namespace to determine the object’s module. This + behaviour is typically useful for singletons. + + To then import such an object from a non-local caller, we try scoping the string + with the module of the object which returned it. + """ + try: + import_from_string(string_reduction) + importable = string_reduction + except ModuleNotFoundError: + importable = reduced_object.__module__ + "." + string_reduction + try: + import_from_string(importable) + except (ModuleNotFoundError, AttributeError) as e: + raise StringNotImportableError( + f"Couldn't import {string_reduction} after scoping it as {importable}. " + f"Please contact the developers so we can figure out how to handle " + f"this edge case." + ) from e + return importable diff --git a/tests/unit/test_retrieve.py b/tests/unit/test_retrieve.py new file mode 100644 index 0000000..dd6353a --- /dev/null +++ b/tests/unit/test_retrieve.py @@ -0,0 +1,15 @@ +import unittest + +from pyiron_snippets import retrieve + + +class SomeClass: ... + + +class TestRetrieve(unittest.TestCase): + def test_get_importable_string_from_string_reduction(self): + obj = SomeClass() + with self.assertRaises(retrieve.StringNotImportableError): + retrieve.get_importable_string_from_string_reduction( + "this_is_not_a_reduction", obj + ) From 112300deb47f3594582e8b51804b8cde3d0521dc Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 2 Oct 2025 13:06:46 -0700 Subject: [PATCH 02/14] Fail more friendly In case the very first module bombs Signed-off-by: liamhuber --- pyiron_snippets/retrieve.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pyiron_snippets/retrieve.py b/pyiron_snippets/retrieve.py index 356d026..4ceba43 100644 --- a/pyiron_snippets/retrieve.py +++ b/pyiron_snippets/retrieve.py @@ -16,7 +16,17 @@ def import_from_string(library_path: str) -> Any: module_name, path = split_path[0], "" else: module_name, path = split_path - obj = importlib.import_module(module_name) + + try: + obj = importlib.import_module(module_name) + except ModuleNotFoundError as e: + raise ModuleNotFoundError( + f"The topmost entry of {library_path} could not be found. The most likely " + f"causes of this problem are a typo, or that the module is not yet in your " + f"system's PYTHONPATH. The latter can be checked from inside python with " + f"`import sys; print(sys.path)`." + ) from e + for k in path.split("."): try: obj = getattr(obj, k) From 91c657576e881b696447d27c474528d879a77cc0 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 2 Oct 2025 13:10:06 -0700 Subject: [PATCH 03/14] Re-parent exception class Signed-off-by: liamhuber --- pyiron_snippets/retrieve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyiron_snippets/retrieve.py b/pyiron_snippets/retrieve.py index 4ceba43..c52bae5 100644 --- a/pyiron_snippets/retrieve.py +++ b/pyiron_snippets/retrieve.py @@ -7,7 +7,7 @@ import importlib from typing import Any -from bagofholding.exceptions import StringNotImportableError +class StringNotImportableError(ImportError): ... def import_from_string(library_path: str) -> Any: From aa58dce8e455db970fccb2ca40a535a5d5fd60fb Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 2 Oct 2025 13:10:15 -0700 Subject: [PATCH 04/14] Modify return type Signed-off-by: liamhuber --- pyiron_snippets/retrieve.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyiron_snippets/retrieve.py b/pyiron_snippets/retrieve.py index c52bae5..7ff3d93 100644 --- a/pyiron_snippets/retrieve.py +++ b/pyiron_snippets/retrieve.py @@ -5,12 +5,13 @@ from __future__ import annotations import importlib -from typing import Any + class StringNotImportableError(ImportError): ... -def import_from_string(library_path: str) -> Any: +def import_from_string(library_path: str) -> object: + split_path = library_path.split(".", 1) if len(split_path) == 1: module_name, path = split_path[0], "" From ca51c34ac605f7e2618a91ad8417c687a2fa052b Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 2 Oct 2025 13:20:05 -0700 Subject: [PATCH 05/14] Add example Signed-off-by: liamhuber --- pyiron_snippets/retrieve.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pyiron_snippets/retrieve.py b/pyiron_snippets/retrieve.py index 7ff3d93..1484c1a 100644 --- a/pyiron_snippets/retrieve.py +++ b/pyiron_snippets/retrieve.py @@ -11,7 +11,26 @@ class StringNotImportableError(ImportError): ... def import_from_string(library_path: str) -> object: + """ + Import an object using a string of its python library location. + + Args: + library_path (str): The full module path to the desired object. + + Returns: + (object): The imported object. + Example: + >>> from pyiron_snippets import retrieve + >>> ThreadPoolExecutor = retrieve.import_from_string( + ... "concurrent.futures.ThreadPoolExecutor" + ... ) + >>> with ThreadPoolExecutor(max_workers=2) as executor: + ... future = executor.submit(pow, 2, 3) + ... print(future.result()) + 8 + + """ split_path = library_path.split(".", 1) if len(split_path) == 1: module_name, path = split_path[0], "" From 1408e07e56fcee809b5fbd164bda6be3649a3efb Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 2 Oct 2025 13:33:16 -0700 Subject: [PATCH 06/14] Break cleanly on empty strings Signed-off-by: liamhuber --- pyiron_snippets/retrieve.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyiron_snippets/retrieve.py b/pyiron_snippets/retrieve.py index 1484c1a..a01d6a2 100644 --- a/pyiron_snippets/retrieve.py +++ b/pyiron_snippets/retrieve.py @@ -31,6 +31,9 @@ def import_from_string(library_path: str) -> object: 8 """ + if not isinstance(library_path, str) and len(library_path) > 0: + raise ValueError(f"Expected a non-empty string, got '{library_path}' instead.") + split_path = library_path.split(".", 1) if len(split_path) == 1: module_name, path = split_path[0], "" From 63224e5b2484b1effad2bddb800f2fd4c429216c Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 2 Oct 2025 13:33:29 -0700 Subject: [PATCH 07/14] Break cleanly on pure modules Signed-off-by: liamhuber --- pyiron_snippets/retrieve.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyiron_snippets/retrieve.py b/pyiron_snippets/retrieve.py index a01d6a2..3fcd45e 100644 --- a/pyiron_snippets/retrieve.py +++ b/pyiron_snippets/retrieve.py @@ -51,6 +51,8 @@ def import_from_string(library_path: str) -> object: ) from e for k in path.split("."): + if k == "": + break try: obj = getattr(obj, k) except AttributeError: From 431770dfb38e43f935c0b31a9bb6a0ec867ee5cd Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 2 Oct 2025 14:13:37 -0700 Subject: [PATCH 08/14] Simplify exception stack Signed-off-by: liamhuber --- pyiron_snippets/retrieve.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pyiron_snippets/retrieve.py b/pyiron_snippets/retrieve.py index 3fcd45e..1e50a94 100644 --- a/pyiron_snippets/retrieve.py +++ b/pyiron_snippets/retrieve.py @@ -60,12 +60,7 @@ def import_from_string(library_path: str) -> object: # This can be necessary of an __init__.py is empty and nothing else has # referenced the module yet current_path = f"{obj.__name__}.{k}" - try: - obj = importlib.import_module(current_path) - except ImportError as e: - raise AttributeError( - f"module '{obj.__name__}' has no attribute '{k}'" - ) from e + obj = importlib.import_module(current_path) return obj From 8ad56637d5061c5bebbd5c36b871cee89a26e89c Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 2 Oct 2025 14:14:43 -0700 Subject: [PATCH 09/14] Add tests Claude Opus did the heavy lifting. It did a good job, but there was still some of fiddling around the edges and a couple places where it really misunderstood meaning. Still, sped things up. Signed-off-by: liamhuber --- tests/unit/test_retrieve.py | 312 +++++++++++++++++++++++++++++++++++- 1 file changed, 309 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_retrieve.py b/tests/unit/test_retrieve.py index dd6353a..f3e9ce3 100644 --- a/tests/unit/test_retrieve.py +++ b/tests/unit/test_retrieve.py @@ -1,15 +1,321 @@ +import importlib +import pathlib +import shutil +import sys +import textwrap import unittest -from pyiron_snippets import retrieve +from pyiron_snippets import retrieve, singleton class SomeClass: ... -class TestRetrieve(unittest.TestCase): - def test_get_importable_string_from_string_reduction(self): +class TestImportFromString(unittest.TestCase): + """Test cases for import_from_string function.""" + + def setUp(self): + """Add static test files to path for testing.""" + self.static_path = pathlib.Path(__file__).parent.parent / "static" + if str(self.static_path) not in sys.path: + sys.path.insert(0, str(self.static_path)) + + def tearDown(self): + """Clean up sys.path and modules.""" + if str(self.static_path) in sys.path: + sys.path.remove(str(self.static_path)) + keys_to_remove = [k for k in sys.modules if k.startswith("test_module")] + for key in keys_to_remove: + del sys.modules[key] + + def test_import_builtin_module(self): + """Test importing a standard library module.""" + result = retrieve.import_from_string("os") + import os + + self.assertIs(result, os) + + def test_import_builtin_function(self): + """Test importing a function from standard library.""" + result = retrieve.import_from_string("os.path.join") + from os.path import join + + self.assertIs(result, join) + + def test_import_builtin_class(self): + """Test importing a class from standard library.""" + result = retrieve.import_from_string("pathlib.Path") + from pathlib import Path + + self.assertIs(result, Path) + + def test_import_nested_attribute(self): + """Test importing deeply nested attributes.""" + result = retrieve.import_from_string("unittest.TestCase.assertEqual") + self.assertEqual(result, unittest.TestCase.assertEqual) + + def test_import_from_pyiron_snippets(self): + """Test importing from the pyiron_snippets package itself.""" + result = retrieve.import_from_string("pyiron_snippets.singleton.Singleton") + self.assertIs(result, singleton.Singleton) + + def test_import_nonexistent_module(self): + """Test that importing non-existent module raises ModuleNotFoundError.""" + with self.assertRaises(ModuleNotFoundError) as cm: + retrieve.import_from_string("nonexistent_module") + self.assertIn("nonexistent_module", str(cm.exception)) + self.assertIn("PYTHONPATH", str(cm.exception)) + + def test_import_nonexistent_attribute(self): + """Test that importing non-existent attribute raises AttributeError.""" + with self.assertRaises(ModuleNotFoundError) as cm: + retrieve.import_from_string("os.nonexistent_attr") + self.assertIn("nonexistent_attr", str(cm.exception)) + + def test_import_empty_string(self): + """Test edge case with empty string.""" + with self.assertRaises(ValueError): + retrieve.import_from_string("") + + def test_import_single_name(self): + """Test importing just a module name without any dots.""" + result = retrieve.import_from_string("sys") + import sys + + self.assertIs(result, sys) + + def test_import_from_uninitialized_submodule(self): + """Test importing from a submodule that hasn't been initialized yet.""" + test_pkg_dir = self.static_path / "test_module_uninit" + test_pkg_dir.mkdir(parents=True, exist_ok=True) + + (test_pkg_dir / "__init__.py").write_text("") + + submodule_content = textwrap.dedent( + """ + class UnInitClass: + value = 42 + """ + ).strip() + (test_pkg_dir / "submodule.py").write_text(submodule_content) + + try: + uninitialized = importlib.import_module("test_module_uninit") + self.assertNotIn("submodule", dir(uninitialized)) + result = retrieve.import_from_string( + "test_module_uninit.submodule.UnInitClass" + ) + self.assertEqual( + result.value, + 42, + msg="Even with an unitialized submodule, the class value still be importable", + ) + finally: + shutil.rmtree(test_pkg_dir) + + def test_import_class_method(self): + """Test importing a method from a class.""" + result = retrieve.import_from_string("pathlib.Path.exists") + from pathlib import Path + + self.assertEqual(result, Path.exists) + + +class TestGetImportableStringFromStringReduction(unittest.TestCase): + """Test cases for get_importable_string_from_string_reduction function.""" + + def test_already_importable_string(self): + """Test with a string that's already importable.""" + obj = pathlib.Path(".") + result = retrieve.get_importable_string_from_string_reduction( + "pathlib.Path", obj + ) + self.assertEqual(result, "pathlib.Path") + + def test_needs_module_scoping(self): + """Test with a string that needs module scoping.""" + # Using this test class as an example obj = SomeClass() + # If we just provide the class name, it should scope with module + result = retrieve.get_importable_string_from_string_reduction("SomeClass", obj) + self.assertEqual( + result, + "unit.test_retrieve.SomeClass", + msg="Note that the unit test folder has an __init__.py file, and is thus correctly interpreted as part of the module path", + ) + + def test_singleton_reduction(self): + """Test the singleton use case mentioned in docstring.""" + + class TestSingleton(metaclass=singleton.Singleton): ... + + obj = TestSingleton() + # Simulating what pickle might return for a singleton + # Since TestSingleton is local to this test, we need to handle it carefully with self.assertRaises(retrieve.StringNotImportableError): + retrieve.get_importable_string_from_string_reduction( + "NonExistentSingleton", obj + ) + + def test_invalid_reduction_string(self): + """Test with a completely invalid reduction string.""" + obj = SomeClass() + with self.assertRaises(retrieve.StringNotImportableError) as cm: retrieve.get_importable_string_from_string_reduction( "this_is_not_a_reduction", obj ) + self.assertIn("this_is_not_a_reduction", str(cm.exception)) + self.assertIn("edge case", str(cm.exception)) + + def test_reduction_with_nested_class(self): + """Test reduction with a nested class.""" + + class OuterClass: + class InnerClass: + pass + + obj = OuterClass.InnerClass() + with self.assertRaises( + retrieve.StringNotImportableError, + msg="This should raise an error because objects aren't " + "importable", + ): + retrieve.get_importable_string_from_string_reduction( + "InnerClass", + obj, + ) + + def test_reduction_from_stdlib(self): + """Test reduction from standard library objects.""" + from collections import OrderedDict + + obj = OrderedDict() + result = retrieve.get_importable_string_from_string_reduction( + "collections.OrderedDict", obj + ) + self.assertEqual(result, "collections.OrderedDict") + + def test_reduction_from_builtin_type(self): + """Test reduction from built-in objects.""" + result = retrieve.get_importable_string_from_string_reduction("int", int) + self.assertEqual(result, "builtins.int") + + def test_object_without_module_attribute(self): + """Test with an object that doesn't have __module__ attribute.""" + with self.assertRaises( + AttributeError, msg="1.__module__ should raise an error" + ): + retrieve.get_importable_string_from_string_reduction( + "ints have no module", 1 + ) + + +class TestIntegrationScenarios(unittest.TestCase): + """Integration tests for real-world scenarios.""" + + def setUp(self): + """Set up test environment.""" + self.static_path = pathlib.Path(__file__).parent.parent / "static" + if str(self.static_path) not in sys.path: + sys.path.insert(0, str(self.static_path)) + + def tearDown(self): + """Clean up test environment.""" + if str(self.static_path) in sys.path: + sys.path.remove(str(self.static_path)) + keys_to_remove = [k for k in sys.modules if k.startswith("test_package")] + for key in keys_to_remove: + del sys.modules[key] + + def test_complex_package_structure(self): + """Test with a complex package structure.""" + # Create a test package structure + pkg_dir = self.static_path / "test_package_complex" + sub_pkg_dir = pkg_dir / "subpackage" + sub_pkg_dir.mkdir(parents=True, exist_ok=True) + + (pkg_dir / "__init__.py").write_text( + "from .module1 import Class1\n__all__ = ['Class1']" + ) + (sub_pkg_dir / "__init__.py").write_text("") + + (pkg_dir / "module1.py").write_text( + textwrap.dedent( + """ + class Class1: + value = 'from_module1' + """ + ).strip() + ) + + (sub_pkg_dir / "module2.py").write_text( + textwrap.dedent( + """ + class Class2: + value = 'from_module2' + + class NestedClass: + nested_value = 'nested' + """ + ).strip() + ) + + try: + result1 = retrieve.import_from_string("test_package_complex.Class1") + self.assertEqual(result1.value, "from_module1") + + result2 = retrieve.import_from_string("test_package_complex.module1.Class1") + self.assertEqual(result2.value, "from_module1") + + result3 = retrieve.import_from_string( + "test_package_complex.subpackage.module2.Class2" + ) + self.assertEqual(result3.value, "from_module2") + + result4 = retrieve.import_from_string( + "test_package_complex.subpackage.module2.Class2.NestedClass" + ) + self.assertEqual(result4.nested_value, "nested") + + finally: + shutil.rmtree(pkg_dir) + + def test_circular_import_handling(self): + """Test that circular imports are handled gracefully.""" + pkg_dir = self.static_path / "test_circular" + pkg_dir.mkdir(parents=True, exist_ok=True) + + (pkg_dir / "__init__.py").write_text("") + (pkg_dir / "module_a.py").write_text( + textwrap.dedent( + """ + from .module_b import ClassB + + class ClassA: + related = ClassB + value = 'A' + """ + ).strip() + ) + (pkg_dir / "module_b.py").write_text( + textwrap.dedent( + """ + class ClassB: + value = 'B' + + # Circular import: + from .module_a import ClassA + """ + ).strip() + ) + + try: + with self.assertRaises(ImportError, msg="Circular imports never work"): + retrieve.import_from_string("test_circular.module_a.ClassA") + + finally: + shutil.rmtree(pkg_dir) + + +if __name__ == "__main__": + unittest.main() From 3a04efbdc20d7321906d606533b706466dd38f5a Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 2 Oct 2025 14:18:11 -0700 Subject: [PATCH 10/14] Add to README Signed-off-by: liamhuber --- docs/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/README.md b/docs/README.md index 0780ee4..6a4814b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -204,6 +204,25 @@ ERROR: name 'magic' is not defined Configures the logger and writes to `pyiron.log` +## Retrieve + +Tools for retrieving objects from strings. +Particularly useful when objects or references are serialized by reference to their library location. + +```python +>>> from pyiron_snippets import retrieve +>>> ThreadPoolExecutor = retrieve.import_from_string( +... "concurrent.futures.ThreadPoolExecutor" +... ) +>>> with ThreadPoolExecutor(max_workers=2) as executor: +... future = executor.submit(pow, 2, 3) +... print(future.result()) +8 + +``` + +Includes an extra tool, `get_importable_string_from_string_reduction` for singleton-pattern string reductions. + ## Retry If at first you don't succeed From 6feedcd23b4a4f3dd7d71bc9a74f78e03fbed36c Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 2 Oct 2025 18:20:50 -0700 Subject: [PATCH 11/14] Relax the path expectation PyCharm is treating the suite differently than the GitHub CI Signed-off-by: liamhuber --- tests/unit/test_retrieve.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_retrieve.py b/tests/unit/test_retrieve.py index f3e9ce3..1f1a4d7 100644 --- a/tests/unit/test_retrieve.py +++ b/tests/unit/test_retrieve.py @@ -138,10 +138,11 @@ def test_needs_module_scoping(self): obj = SomeClass() # If we just provide the class name, it should scope with module result = retrieve.get_importable_string_from_string_reduction("SomeClass", obj) - self.assertEqual( + self.assertIn( result, - "unit.test_retrieve.SomeClass", - msg="Note that the unit test folder has an __init__.py file, and is thus correctly interpreted as part of the module path", + ["unit.test_retrieve.SomeClass", "test_retrieve.SomeClass"], + msg="Note that the unit test folder has an __init__.py file, and is may " + "be interpreted as part of the module path, so either result is possible", ) def test_singleton_reduction(self): From ff62165e6c2f0d6a147b109c9100cdbdb0d6bcee Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 2 Oct 2025 18:27:16 -0700 Subject: [PATCH 12/14] Raise value error on nonsense And test it to complete coverage Signed-off-by: liamhuber --- pyiron_snippets/retrieve.py | 2 +- tests/unit/test_retrieve.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pyiron_snippets/retrieve.py b/pyiron_snippets/retrieve.py index 1e50a94..2634999 100644 --- a/pyiron_snippets/retrieve.py +++ b/pyiron_snippets/retrieve.py @@ -31,7 +31,7 @@ def import_from_string(library_path: str) -> object: 8 """ - if not isinstance(library_path, str) and len(library_path) > 0: + if (not isinstance(library_path, str)) or len(library_path) == 0: raise ValueError(f"Expected a non-empty string, got '{library_path}' instead.") split_path = library_path.split(".", 1) diff --git a/tests/unit/test_retrieve.py b/tests/unit/test_retrieve.py index 1f1a4d7..97134e2 100644 --- a/tests/unit/test_retrieve.py +++ b/tests/unit/test_retrieve.py @@ -120,6 +120,13 @@ def test_import_class_method(self): self.assertEqual(result, Path.exists) + def test_nonsense(self): + with self.assertRaises(ValueError): + retrieve.import_from_string("") + + with self.assertRaises(ValueError): + retrieve.import_from_string(42) + class TestGetImportableStringFromStringReduction(unittest.TestCase): """Test cases for get_importable_string_from_string_reduction function.""" From c60bf8f4d7454fcecf951b360b79fcd4e91f4bfa Mon Sep 17 00:00:00 2001 From: Liam Huber Date: Fri, 3 Oct 2025 06:08:24 -0700 Subject: [PATCH 13/14] Update retrieve.py Co-authored-by: Niklas Siemer <70580458+niklassiemer@users.noreply.github.com> --- pyiron_snippets/retrieve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyiron_snippets/retrieve.py b/pyiron_snippets/retrieve.py index 2634999..50aa68e 100644 --- a/pyiron_snippets/retrieve.py +++ b/pyiron_snippets/retrieve.py @@ -32,7 +32,7 @@ def import_from_string(library_path: str) -> object: """ if (not isinstance(library_path, str)) or len(library_path) == 0: - raise ValueError(f"Expected a non-empty string, got '{library_path}' instead.") + raise ValueError(f"Expected a non-empty string, got '{library_path}' of type {type(library_path)} instead.") split_path = library_path.split(".", 1) if len(split_path) == 1: From 46be80ed0b18153e3a53374ff7d889ce84867035 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Fri, 3 Oct 2025 06:33:58 -0700 Subject: [PATCH 14/14] Black Signed-off-by: liamhuber --- pyiron_snippets/retrieve.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyiron_snippets/retrieve.py b/pyiron_snippets/retrieve.py index 50aa68e..39926d0 100644 --- a/pyiron_snippets/retrieve.py +++ b/pyiron_snippets/retrieve.py @@ -32,7 +32,9 @@ def import_from_string(library_path: str) -> object: """ if (not isinstance(library_path, str)) or len(library_path) == 0: - raise ValueError(f"Expected a non-empty string, got '{library_path}' of type {type(library_path)} instead.") + raise ValueError( + f"Expected a non-empty string, got '{library_path}' of type {type(library_path)} instead." + ) split_path = library_path.split(".", 1) if len(split_path) == 1: