From db2f84d8dec6d16c3b0f5c2cb7eac358d46f2dfb Mon Sep 17 00:00:00 2001 From: Niklas Siemer <70580458+niklassiemer@users.noreply.github.com> Date: Mon, 23 Aug 2021 12:11:03 +0200 Subject: [PATCH 1/9] Monkey patch GUI elements --- pyiron_gui/__init__.py | 5 +++ pyiron_gui/monkey_patching.py | 67 +++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 pyiron_gui/monkey_patching.py diff --git a/pyiron_gui/__init__.py b/pyiron_gui/__init__.py index e1baad1..d54b078 100644 --- a/pyiron_gui/__init__.py +++ b/pyiron_gui/__init__.py @@ -1,9 +1,14 @@ __version__ = "0.1" __all__ = [] +# desired API from pyiron_gui.project.project import activate_gui from pyiron_gui.project.project_browser import ProjectBrowser, DataContainerGUI +# monkey patching +import pyiron_gui.monkey_patching + + from ._version import get_versions __version__ = get_versions()["version"] diff --git a/pyiron_gui/monkey_patching.py b/pyiron_gui/monkey_patching.py new file mode 100644 index 0000000..01beb34 --- /dev/null +++ b/pyiron_gui/monkey_patching.py @@ -0,0 +1,67 @@ +# coding: utf-8 +# Copyright (c) Max-Planck-Institut für Eisenforschung GmbH - Computational Materials Design (CM) Department +# Distributed under the terms of "New BSD License", see the LICENSE file. + +"""Adding gui functionality to pyiron classes via monkey patching on import of pyiron_gui. """ +import warnings + +from pyiron_gui.project.project_browser import ( + ProjectBrowser, + HasGroupsBrowser, + DataContainerGUI, +) +from pyiron_base.interfaces.has_groups import HasGroups +from pyiron_base import Project +from pyiron_base import DataContainer + +__author__ = "Niklas Siemer" +__copyright__ = ( + "Copyright 2021, Max-Planck-Institut für Eisenforschung GmbH - " + "Computational Materials Design (CM) Department" +) +__version__ = "0.1" +__maintainer__ = "Niklas Siemer" +__email__ = "siemer@mpie.de" +__status__ = "development" +__date__ = "July 06, 2022" + + +def safe_monkey_patch(cls, attr_name, value): + if hasattr(cls, attr_name): + warnings.warn( + f"Class {cls.__name__} already has attribute {attr_name} - Aborting monkey path of gui elements." + ) + else: + setattr(cls, attr_name, value) + + +def _has_groups_gui(self, box=None, refresh=False): + if self._has_groups_browser is None or refresh: + self._has_groups_browser = HasGroupsBrowser(self, box=box) + return self._has_groups_browser + + +safe_monkey_patch(HasGroups, "_has_groups_browser", None) +safe_monkey_patch(HasGroups, "gui", _has_groups_gui) + + +def _datacontainer_gui(self, box=None, refresh=False): + if self._datacontainer_gui is None or refresh: + self._datacontainer_gui = DataContainerGUI(self, box=box) + return self._datacontainer_browser + + +safe_monkey_patch(DataContainer, "_datacontainer_gui", _datacontainer_gui) +safe_monkey_patch(DataContainer, "gui", _datacontainer_gui) + + +def _pyiron_base_project_browser(self): + if self._project_browser is None: + self._project_browser = ProjectBrowser( + project=self, show_files=False, Vbox=None + ) + return self._project_browser + + +safe_monkey_patch(Project, "_project_browser", None) +safe_monkey_patch(Project, "browser", property(_pyiron_base_project_browser)) From 40641cde4aa9cde228375ab4db953ef82a68f6a2 Mon Sep 17 00:00:00 2001 From: Niklas Siemer <70580458+niklassiemer@users.noreply.github.com> Date: Fri, 29 Jul 2022 12:29:21 +0200 Subject: [PATCH 2/9] Update pyiron_gui/monkey_patching.py Co-authored-by: Marvin Poul --- pyiron_gui/monkey_patching.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyiron_gui/monkey_patching.py b/pyiron_gui/monkey_patching.py index 01beb34..d933fd1 100644 --- a/pyiron_gui/monkey_patching.py +++ b/pyiron_gui/monkey_patching.py @@ -51,7 +51,7 @@ def _datacontainer_gui(self, box=None, refresh=False): return self._datacontainer_browser -safe_monkey_patch(DataContainer, "_datacontainer_gui", _datacontainer_gui) +safe_monkey_patch(DataContainer, "_datacontainer_gui", None) safe_monkey_patch(DataContainer, "gui", _datacontainer_gui) From bda62d818e6e8eac17b6c2f5555267818f925fc8 Mon Sep 17 00:00:00 2001 From: Niklas Siemer <70580458+niklassiemer@users.noreply.github.com> Date: Wed, 17 Aug 2022 09:03:38 +0200 Subject: [PATCH 3/9] Secure reload of monkey patch --- pyiron_gui/monkey_patching.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyiron_gui/monkey_patching.py b/pyiron_gui/monkey_patching.py index d933fd1..820edf0 100644 --- a/pyiron_gui/monkey_patching.py +++ b/pyiron_gui/monkey_patching.py @@ -26,13 +26,15 @@ __date__ = "July 06, 2022" -def safe_monkey_patch(cls, attr_name, value): - if hasattr(cls, attr_name): +def safe_monkey_patch(cls, attr_name, func): + bound_method = getattr(cls, attr_name) if hasattr(cls, attr_name) else None + + if bound_method is not None and (bound_method.__module__ != func.__module__ or bound_method.__name__ != func.__name__): warnings.warn( f"Class {cls.__name__} already has attribute {attr_name} - Aborting monkey path of gui elements." ) else: - setattr(cls, attr_name, value) + setattr(cls, attr_name, func) def _has_groups_gui(self, box=None, refresh=False): From bc317da1cd2556295b8e962b244656e8d967e963 Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Mon, 12 Sep 2022 13:47:48 +0000 Subject: [PATCH 4/9] Format black --- pyiron_gui/monkey_patching.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyiron_gui/monkey_patching.py b/pyiron_gui/monkey_patching.py index 820edf0..3f15f64 100644 --- a/pyiron_gui/monkey_patching.py +++ b/pyiron_gui/monkey_patching.py @@ -29,7 +29,10 @@ def safe_monkey_patch(cls, attr_name, func): bound_method = getattr(cls, attr_name) if hasattr(cls, attr_name) else None - if bound_method is not None and (bound_method.__module__ != func.__module__ or bound_method.__name__ != func.__name__): + if bound_method is not None and ( + bound_method.__module__ != func.__module__ + or bound_method.__name__ != func.__name__ + ): warnings.warn( f"Class {cls.__name__} already has attribute {attr_name} - Aborting monkey path of gui elements." ) From 71a0e42ebb63400cdb6fc7672dc260ebd1e0f030 Mon Sep 17 00:00:00 2001 From: Niklas Siemer <70580458+niklassiemer@users.noreply.github.com> Date: Wed, 6 Jul 2022 12:24:44 +0200 Subject: [PATCH 5/9] monkey patch order: specific first --- pyiron_gui/monkey_patching.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pyiron_gui/monkey_patching.py b/pyiron_gui/monkey_patching.py index 3f15f64..9b7541b 100644 --- a/pyiron_gui/monkey_patching.py +++ b/pyiron_gui/monkey_patching.py @@ -40,16 +40,6 @@ def safe_monkey_patch(cls, attr_name, func): setattr(cls, attr_name, func) -def _has_groups_gui(self, box=None, refresh=False): - if self._has_groups_browser is None or refresh: - self._has_groups_browser = HasGroupsBrowser(self, box=box) - return self._has_groups_browser - - -safe_monkey_patch(HasGroups, "_has_groups_browser", None) -safe_monkey_patch(HasGroups, "gui", _has_groups_gui) - - def _datacontainer_gui(self, box=None, refresh=False): if self._datacontainer_gui is None or refresh: self._datacontainer_gui = DataContainerGUI(self, box=box) @@ -70,3 +60,13 @@ def _pyiron_base_project_browser(self): safe_monkey_patch(Project, "_project_browser", None) safe_monkey_patch(Project, "browser", property(_pyiron_base_project_browser)) + + +def _has_groups_gui(self, box=None, refresh=False): + if self._has_groups_browser is None or refresh: + self._has_groups_browser = HasGroupsBrowser(self, box=box) + return self._has_groups_browser + + +safe_monkey_patch(HasGroups, "_has_groups_browser", None) +safe_monkey_patch(HasGroups, "gui", _has_groups_gui) From 01fae61460e190fcb9748d81bbc154f81a6c8a78 Mon Sep 17 00:00:00 2001 From: Niklas Siemer <70580458+niklassiemer@users.noreply.github.com> Date: Wed, 21 Sep 2022 17:12:09 +0200 Subject: [PATCH 6/9] Enable testing of _update_project, again. --- tests/project/test_browser.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/project/test_browser.py b/tests/project/test_browser.py index d0ed123..d38e4fb 100644 --- a/tests/project/test_browser.py +++ b/tests/project/test_browser.py @@ -329,7 +329,6 @@ def test__on_click_file(self): browser._select_node('NotAFileName.dat') self.assertIsNone(browser.data, msg=f"Expected browser.data to be None, but got {browser.data}") - @unittest.skip("Wrong ToyJob is returned by project['toy_job_name']") def test__update_project(self): browser = self.browser.copy() browser._update_project('testjob') @@ -471,7 +470,6 @@ def __init__(self, path): self.browser._project = DummyProj('/some/path/') self.assertEqual(['/', '/some', '/some/path'], self.browser._gen_pathbox_path_list()) - @unittest.skip("Wrong ToyJob is returned by project['toy_job_name']") def test__update_project(self): browser = self.browser.copy() path = join(browser.path, 'testjob') From 8b94998293c9821aa1a76f206fa3ed7c4f20ad7c Mon Sep 17 00:00:00 2001 From: Niklas Siemer <70580458+niklassiemer@users.noreply.github.com> Date: Thu, 22 Sep 2022 17:48:12 +0200 Subject: [PATCH 7/9] Secure and test monkey patching --- pyiron_gui/__init__.py | 6 ++- pyiron_gui/monkey_patching.py | 69 +++++++++++++++++++++++++------- tests/test_monkey_patching.py | 74 +++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 15 deletions(-) create mode 100644 tests/test_monkey_patching.py diff --git a/pyiron_gui/__init__.py b/pyiron_gui/__init__.py index d54b078..417d223 100644 --- a/pyiron_gui/__init__.py +++ b/pyiron_gui/__init__.py @@ -3,7 +3,11 @@ # desired API from pyiron_gui.project.project import activate_gui -from pyiron_gui.project.project_browser import ProjectBrowser, DataContainerGUI +from pyiron_gui.project.project_browser import ( + ProjectBrowser, + DataContainerGUI, + HasGroupsBrowser, +) # monkey patching import pyiron_gui.monkey_patching diff --git a/pyiron_gui/monkey_patching.py b/pyiron_gui/monkey_patching.py index 9b7541b..7d52f87 100644 --- a/pyiron_gui/monkey_patching.py +++ b/pyiron_gui/monkey_patching.py @@ -4,6 +4,7 @@ """Adding gui functionality to pyiron classes via monkey patching on import of pyiron_gui. """ import warnings +from typing import Union from pyiron_gui.project.project_browser import ( ProjectBrowser, @@ -26,28 +27,70 @@ __date__ = "July 06, 2022" -def safe_monkey_patch(cls, attr_name, func): - bound_method = getattr(cls, attr_name) if hasattr(cls, attr_name) else None +def _safe_monkey_patch_method(cls, method_name, func): + method = getattr(cls, method_name) if hasattr(cls, method_name) else None + if method is not None and ( + method.__module__ != func.__module__ + or method.__name__ != func.__name__ + ): + warnings.warn( + f"Class {cls.__name__} already has attribute {method_name} - Aborting monkey patch of gui elements." + f"Method {method} in {method.__module__} with name {method.__name__} planned to replaced by {func} in " + f"{func.__module__} with name {func.__name__}" + ) + return False + else: + setattr(cls, method_name, func) + return True + - if bound_method is not None and ( - bound_method.__module__ != func.__module__ - or bound_method.__name__ != func.__name__ +def _safe_monkey_patch_property(cls, property_name, prop): + method = getattr(cls, property_name) if hasattr(cls, property_name) else None + if method is not None and ( + method.fget.__module__ != prop.fget.__module__ + or method.fget.__name__ != prop.fget.__name__ ): warnings.warn( - f"Class {cls.__name__} already has attribute {attr_name} - Aborting monkey path of gui elements." + f"Class {cls.__name__} already has attribute {property_name} - Aborting monkey patch of gui elements." + f"Method {method} in {method.__module__} with name {method.__name__} planned to replaced by {prop} in " + f"{prop.__module__} with name {prop.__name__}" + ) + return False + else: + setattr(cls, property_name, prop) + return True + + +def safe_monkey_patch(cls: type, func_or_property_name: str, func_or_property, attr_name: Union[str, None] = None, attr_val=None): + attribute_or_bound_method = getattr(cls, func_or_property_name) if hasattr(cls, func_or_property_name) else None + if hasattr(cls, func_or_property_name) and type(func_or_property) != type(attribute_or_bound_method): + warnings.warn( + f"Class {cls.__name__} already has attribute {func_or_property_name} - Aborting monkey patch " + f"of gui elements. type(attribute_or_bound_method) = {type(attribute_or_bound_method)} " + f"{attribute_or_bound_method}, type(func_or_property) = {type(func_or_property)}." ) + return + + if callable(func_or_property): + success = _safe_monkey_patch_method(cls, func_or_property_name, func_or_property) + if success and attr_name is not None: + setattr(cls, attr_name, attr_val) + elif isinstance(func_or_property, property): + success = _safe_monkey_patch_property(cls, func_or_property_name, func_or_property) + if success and attr_name is not None: + setattr(cls, attr_name, attr_val) else: - setattr(cls, attr_name, func) + warnings.warn(f'{func_or_property_name} not added since provided func_or_property ({func_or_property} ' + f'is neither callable nor property') def _datacontainer_gui(self, box=None, refresh=False): if self._datacontainer_gui is None or refresh: self._datacontainer_gui = DataContainerGUI(self, box=box) - return self._datacontainer_browser + return self._datacontainer_gui -safe_monkey_patch(DataContainer, "_datacontainer_gui", None) -safe_monkey_patch(DataContainer, "gui", _datacontainer_gui) +safe_monkey_patch(DataContainer, "gui", _datacontainer_gui, "_datacontainer_gui", None) def _pyiron_base_project_browser(self): @@ -58,8 +101,7 @@ def _pyiron_base_project_browser(self): return self._project_browser -safe_monkey_patch(Project, "_project_browser", None) -safe_monkey_patch(Project, "browser", property(_pyiron_base_project_browser)) +safe_monkey_patch(Project, "browser", property(_pyiron_base_project_browser), "_project_browser", None) def _has_groups_gui(self, box=None, refresh=False): @@ -68,5 +110,4 @@ def _has_groups_gui(self, box=None, refresh=False): return self._has_groups_browser -safe_monkey_patch(HasGroups, "_has_groups_browser", None) -safe_monkey_patch(HasGroups, "gui", _has_groups_gui) +safe_monkey_patch(HasGroups, "gui", _has_groups_gui, "_has_groups_browser", None) diff --git a/tests/test_monkey_patching.py b/tests/test_monkey_patching.py new file mode 100644 index 0000000..ab549f5 --- /dev/null +++ b/tests/test_monkey_patching.py @@ -0,0 +1,74 @@ +import unittest +import warnings +import sys + +from pyiron_base import DataContainer, HasGroups, Project + + +class HasGroupsImplementation(HasGroups): + def __getitem__(self, key): + pass + + def _list_groups(self): + return [] + + def _list_nodes(self): + return [] + + +class TestMonkeyPatching(unittest.TestCase): + def test_base_classes(self): + + with self.subTest('Before explicit pyiron_gui import'): + if "pyiron_gui" not in sys.modules: + self.assertFalse(hasattr(DataContainer, 'gui')) + self.assertFalse(hasattr(HasGroups, 'gui')) + self.assertFalse(hasattr(Project, 'browser')) + from pyiron_gui import DataContainerGUI, ProjectBrowser, HasGroupsBrowser + import pyiron_gui.monkey_patching + with self.subTest('DataContainer'): + self.assertTrue(hasattr(DataContainer, 'gui')) + dc_gui = DataContainer().gui() + self.assertIsInstance(dc_gui, DataContainerGUI) + with self.subTest('Project'): + self.assertTrue(hasattr(Project, 'browser')) + pr_browser = Project('.').browser + self.assertIsInstance(pr_browser, ProjectBrowser) + with self.subTest('HasGroups'): + self.assertTrue(hasattr(HasGroups, 'gui')) + hg_gui = HasGroupsImplementation().gui() + self.assertIsInstance(hg_gui, HasGroupsBrowser) + + def test_safe_monkey_patch(self): + class ToBePatched: + pass + + def to_be_applied(cls_instance): + return cls_instance.v + + def not_to_be_applied(cls_instance): + return cls_instance.v + + from pyiron_gui.monkey_patching import safe_monkey_patch + + with warnings.catch_warnings(record=True) as w: + safe_monkey_patch(ToBePatched, 'any', to_be_applied, 'v', None) + self.assertTrue(len(w) == 0, f"Unexpected warnings {[wi.message for wi in w]}") + + patched_instance = ToBePatched() + self.assertIsNone(patched_instance.v) + self.assertTrue(hasattr(patched_instance, 'any')) + self.assertTrue(callable(patched_instance.any)) + self.assertIsNone(patched_instance.any()) + + with self.subTest('Rewrite with same name and object'): + with warnings.catch_warnings(record=True) as w: + safe_monkey_patch(ToBePatched, 'any', to_be_applied, 'v', None) + self.assertEqual(len(w), 0, f"Unexpected warnings {[wi.message for wi in w]}") + + with self.subTest('Not rewrite with different names'): + with warnings.catch_warnings(record=True) as w: + safe_monkey_patch(ToBePatched, 'any', not_to_be_applied, 'v', 'Should not be stored') + self.assertEqual(len(w), 1) + self.assertIs(ToBePatched.any, to_be_applied) + self.assertIsNone(ToBePatched.v) From d46571cd82bb0d29ba168a7d4db3c8a6d76df509 Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Thu, 22 Sep 2022 15:55:47 +0000 Subject: [PATCH 8/9] Format black --- pyiron_gui/monkey_patching.py | 43 +++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/pyiron_gui/monkey_patching.py b/pyiron_gui/monkey_patching.py index 7d52f87..e5cf621 100644 --- a/pyiron_gui/monkey_patching.py +++ b/pyiron_gui/monkey_patching.py @@ -30,8 +30,7 @@ def _safe_monkey_patch_method(cls, method_name, func): method = getattr(cls, method_name) if hasattr(cls, method_name) else None if method is not None and ( - method.__module__ != func.__module__ - or method.__name__ != func.__name__ + method.__module__ != func.__module__ or method.__name__ != func.__name__ ): warnings.warn( f"Class {cls.__name__} already has attribute {method_name} - Aborting monkey patch of gui elements." @@ -47,8 +46,8 @@ def _safe_monkey_patch_method(cls, method_name, func): def _safe_monkey_patch_property(cls, property_name, prop): method = getattr(cls, property_name) if hasattr(cls, property_name) else None if method is not None and ( - method.fget.__module__ != prop.fget.__module__ - or method.fget.__name__ != prop.fget.__name__ + method.fget.__module__ != prop.fget.__module__ + or method.fget.__name__ != prop.fget.__name__ ): warnings.warn( f"Class {cls.__name__} already has attribute {property_name} - Aborting monkey patch of gui elements." @@ -61,9 +60,21 @@ def _safe_monkey_patch_property(cls, property_name, prop): return True -def safe_monkey_patch(cls: type, func_or_property_name: str, func_or_property, attr_name: Union[str, None] = None, attr_val=None): - attribute_or_bound_method = getattr(cls, func_or_property_name) if hasattr(cls, func_or_property_name) else None - if hasattr(cls, func_or_property_name) and type(func_or_property) != type(attribute_or_bound_method): +def safe_monkey_patch( + cls: type, + func_or_property_name: str, + func_or_property, + attr_name: Union[str, None] = None, + attr_val=None, +): + attribute_or_bound_method = ( + getattr(cls, func_or_property_name) + if hasattr(cls, func_or_property_name) + else None + ) + if hasattr(cls, func_or_property_name) and type(func_or_property) != type( + attribute_or_bound_method + ): warnings.warn( f"Class {cls.__name__} already has attribute {func_or_property_name} - Aborting monkey patch " f"of gui elements. type(attribute_or_bound_method) = {type(attribute_or_bound_method)} " @@ -72,16 +83,22 @@ def safe_monkey_patch(cls: type, func_or_property_name: str, func_or_property, a return if callable(func_or_property): - success = _safe_monkey_patch_method(cls, func_or_property_name, func_or_property) + success = _safe_monkey_patch_method( + cls, func_or_property_name, func_or_property + ) if success and attr_name is not None: setattr(cls, attr_name, attr_val) elif isinstance(func_or_property, property): - success = _safe_monkey_patch_property(cls, func_or_property_name, func_or_property) + success = _safe_monkey_patch_property( + cls, func_or_property_name, func_or_property + ) if success and attr_name is not None: setattr(cls, attr_name, attr_val) else: - warnings.warn(f'{func_or_property_name} not added since provided func_or_property ({func_or_property} ' - f'is neither callable nor property') + warnings.warn( + f"{func_or_property_name} not added since provided func_or_property ({func_or_property} " + f"is neither callable nor property" + ) def _datacontainer_gui(self, box=None, refresh=False): @@ -101,7 +118,9 @@ def _pyiron_base_project_browser(self): return self._project_browser -safe_monkey_patch(Project, "browser", property(_pyiron_base_project_browser), "_project_browser", None) +safe_monkey_patch( + Project, "browser", property(_pyiron_base_project_browser), "_project_browser", None +) def _has_groups_gui(self, box=None, refresh=False): From 1c00559785331d872c772156472f6235c6e6ee85 Mon Sep 17 00:00:00 2001 From: Niklas Siemer <70580458+niklassiemer@users.noreply.github.com> Date: Wed, 12 Oct 2022 11:39:30 +0200 Subject: [PATCH 9/9] Apply suggestions from code review Co-authored-by: Marvin Poul --- pyiron_gui/monkey_patching.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pyiron_gui/monkey_patching.py b/pyiron_gui/monkey_patching.py index e5cf621..863299b 100644 --- a/pyiron_gui/monkey_patching.py +++ b/pyiron_gui/monkey_patching.py @@ -28,7 +28,7 @@ def _safe_monkey_patch_method(cls, method_name, func): - method = getattr(cls, method_name) if hasattr(cls, method_name) else None + method = getattr(cls, method_name, None) if method is not None and ( method.__module__ != func.__module__ or method.__name__ != func.__name__ ): @@ -44,7 +44,7 @@ def _safe_monkey_patch_method(cls, method_name, func): def _safe_monkey_patch_property(cls, property_name, prop): - method = getattr(cls, property_name) if hasattr(cls, property_name) else None + method = getattr(cls, property_name, None) if method is not None and ( method.fget.__module__ != prop.fget.__module__ or method.fget.__name__ != prop.fget.__name__ @@ -67,11 +67,7 @@ def safe_monkey_patch( attr_name: Union[str, None] = None, attr_val=None, ): - attribute_or_bound_method = ( - getattr(cls, func_or_property_name) - if hasattr(cls, func_or_property_name) - else None - ) + attribute_or_bound_method = getattr(cls, func_or_property_name, None) if hasattr(cls, func_or_property_name) and type(func_or_property) != type( attribute_or_bound_method ):