-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #151 from pyiron/monkey
Monkey patch GUI elements
- Loading branch information
Showing
4 changed files
with
212 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
# 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 typing import Union | ||
|
||
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__ = "[email protected]" | ||
__status__ = "development" | ||
__date__ = "July 06, 2022" | ||
|
||
|
||
def _safe_monkey_patch_method(cls, method_name, func): | ||
method = getattr(cls, method_name, 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 | ||
|
||
|
||
def _safe_monkey_patch_property(cls, property_name, prop): | ||
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__ | ||
): | ||
warnings.warn( | ||
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, 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: | ||
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_gui | ||
|
||
|
||
safe_monkey_patch(DataContainer, "gui", _datacontainer_gui, "_datacontainer_gui", None) | ||
|
||
|
||
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, "browser", property(_pyiron_base_project_browser), "_project_browser", None | ||
) | ||
|
||
|
||
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, "gui", _has_groups_gui, "_has_groups_browser", None) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |