Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Monkey patch GUI elements #151

Merged
merged 10 commits into from
Oct 12, 2022
11 changes: 10 additions & 1 deletion pyiron_gui/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
__version__ = "0.1"
__all__ = []

# 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


from ._version import get_versions

Expand Down
128 changes: 128 additions & 0 deletions pyiron_gui/monkey_patching.py
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)
2 changes: 0 additions & 2 deletions tests/project/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand Down
74 changes: 74 additions & 0 deletions tests/test_monkey_patching.py
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)