Skip to content

Commit

Permalink
Merge pull request #151 from pyiron/monkey
Browse files Browse the repository at this point in the history
Monkey patch GUI elements
  • Loading branch information
niklassiemer authored Oct 12, 2022
2 parents a098efa + 1c00559 commit e51cb86
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 3 deletions.
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)

0 comments on commit e51cb86

Please sign in to comment.