From 283b0c3aec827187a3e92523c60a17aba6c13fd6 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 30 Mar 2020 15:41:00 -0600 Subject: [PATCH 1/5] adding an ImmutableConfigValue class --- pyutilib/misc/config.py | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/pyutilib/misc/config.py b/pyutilib/misc/config.py index b011ca51..9284d7f0 100644 --- a/pyutilib/misc/config.py +++ b/pyutilib/misc/config.py @@ -1068,6 +1068,53 @@ def _data_collector(self, level, prefix, visibility=None, docMode=False): yield (level, prefix, self, self) +class ImmutableConfigValue(ConfigValue): + """ + A config value that supports temporary immutability. + + + Parameters + ---------- + default: optional + The default value that this ConfigValue will take if no value is + provided. + + domain: callable, optional + The domain can be any callable that accepts a candidate value + and returns the value converted to the desired type, optionally + performing any data validation. The result will be stored into + the ConfigValue. Examples include type constructors like `int` + or `float`. More complex domain examples include callable + objects; for example, the :py:class:`In` class that ensures that + the value falls into an acceptable set or even a complete + :py:class:`ConfigDict` instance. + + description: str, optional + The short description of this value + + doc: str, optional + The long documentation string for this value + + visibility: int, optional + The visibility of this ConfigValue when generating templates and + documentation. Visibility supports specification of "advanced" + or "developer" options. ConfigValues with visibility=0 (the + default) will always be printed / included. ConfigValues + with higher visibility values will only be included when the + generation method specifies a visibility greater than or equal + to the visibility of this object. + """ + def __init__(self, *args, **kwds): + self._mutable = True + self._immutable_error_message = str(self) + ' is currently immutable.' + super(ImmutableConfigValue, self).__init__(*args, **kwds) + + def set_value(self, value): + if not self._mutable: + raise RuntimeError(self._immutable_error_message) + super(ImmutableConfigValue, self).set_value(value) + + class ConfigList(ConfigBase): """Store and manipulate a list of configuration values. From 99ca7e128a4c0aa3f360b8a0b5457fafdb971a1a Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 31 Mar 2020 02:59:11 -0600 Subject: [PATCH 2/5] adding an ImmutableConfigValue --- pyutilib/misc/config.py | 59 +++++++++--------------------- pyutilib/misc/tests/test_config.py | 29 ++++++++++++++- 2 files changed, 46 insertions(+), 42 deletions(-) diff --git a/pyutilib/misc/config.py b/pyutilib/misc/config.py index 9284d7f0..26455309 100644 --- a/pyutilib/misc/config.py +++ b/pyutilib/misc/config.py @@ -1069,50 +1069,27 @@ def _data_collector(self, level, prefix, visibility=None, docMode=False): class ImmutableConfigValue(ConfigValue): - """ - A config value that supports temporary immutability. - - - Parameters - ---------- - default: optional - The default value that this ConfigValue will take if no value is - provided. - - domain: callable, optional - The domain can be any callable that accepts a candidate value - and returns the value converted to the desired type, optionally - performing any data validation. The result will be stored into - the ConfigValue. Examples include type constructors like `int` - or `float`. More complex domain examples include callable - objects; for example, the :py:class:`In` class that ensures that - the value falls into an acceptable set or even a complete - :py:class:`ConfigDict` instance. - - description: str, optional - The short description of this value + def set_value(self, value): + raise RuntimeError(str(self) + ' is currently immutable') - doc: str, optional - The long documentation string for this value - visibility: int, optional - The visibility of this ConfigValue when generating templates and - documentation. Visibility supports specification of "advanced" - or "developer" options. ConfigValues with visibility=0 (the - default) will always be printed / included. ConfigValues - with higher visibility values will only be included when the - generation method specifies a visibility greater than or equal - to the visibility of this object. - """ - def __init__(self, *args, **kwds): - self._mutable = True - self._immutable_error_message = str(self) + ' is currently immutable.' - super(ImmutableConfigValue, self).__init__(*args, **kwds) +class MarkImmutable(object): + def __init__(self, *args): + self._locked = list() + try: + for arg in args: + if type(arg) is not ConfigValue: + raise ValueError('Only ConfigValue instances can be marked immutable.') + arg.__class__ = ImmutableConfigValue + self._locked.append(arg) + except: + self.release_lock() + raise - def set_value(self, value): - if not self._mutable: - raise RuntimeError(self._immutable_error_message) - super(ImmutableConfigValue, self).set_value(value) + def release_lock(self): + for arg in self._locked: + arg.__class__ = ConfigValue + self._locked = list() class ConfigList(ConfigBase): diff --git a/pyutilib/misc/tests/test_config.py b/pyutilib/misc/tests/test_config.py index 54ba062b..7190227c 100644 --- a/pyutilib/misc/tests/test_config.py +++ b/pyutilib/misc/tests/test_config.py @@ -17,7 +17,7 @@ import pyutilib.th as unittest import pyutilib.misc.comparison -from pyutilib.misc.config import ConfigValue, ConfigBlock, ConfigList +from pyutilib.misc.config import ConfigValue, ConfigBlock, ConfigList, MarkImmutable from six import PY3, StringIO @@ -39,6 +39,33 @@ def _display(obj, *args): obj.display(ostream=test, *args) return test.getvalue() + +class TestImmutableConfigValue(unittest.TestCase): + def test_immutable_config_value(self): + config = ConfigBlock() + config.declare('a', ConfigValue(default=1, domain=int)) + config.declare('b', ConfigValue(default=1, domain=int)) + config.a = 2 + config.b = 3 + self.assertEqual(config.a, 2) + self.assertEqual(config.b, 3) + locker = MarkImmutable(config.get('a'), config.get('b')) + with self.assertRaises(Exception): + config.a = 4 + with self.assertRaises(Exception): + config.b = 5 + locker.release_lock() + config.a = 4 + config.b = 5 + self.assertEqual(config.a, 4) + self.assertEqual(config.b, 5) + with self.assertRaises(ValueError): + locker = MarkImmutable(config.get('a'), config.b) + self.assertEqual(type(config.get('a')), ConfigValue) + config.a = 6 + self.assertEqual(config.a, 6) + + class Test(unittest.TestCase): def setUp(self): From b5c7dd9b40385abc85edf85efef17575dcaa699d Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 31 Mar 2020 03:06:46 -0600 Subject: [PATCH 3/5] adding some docs for MarkImmutable --- pyutilib/misc/config.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pyutilib/misc/config.py b/pyutilib/misc/config.py index 26455309..dd2a1a49 100644 --- a/pyutilib/misc/config.py +++ b/pyutilib/misc/config.py @@ -1074,6 +1074,26 @@ def set_value(self, value): class MarkImmutable(object): + """ + Mark instances of ConfigValue as immutable. + + Parameters + ---------- + config_value: ConfigValue + The ConfigValue instances that should be marked immutable. + Note that multiple instances of ConfigValue can be passed. + + Examples + -------- + >>> config = ConfigBlock() + >>> config.declare('a', ConfigValue(default=1, domain=int)) + >>> config.declare('b', ConfigValue(default=1, domain=int)) + >>> locker = MarkImmutable(config.get('a'), config.get('b')) + + Now, config.a and config.b cannot be changed. To make them mutable again, + + >>> locker.release_lock() + """ def __init__(self, *args): self._locked = list() try: From 00a211b2449c76c898ac5d1b3e647ab76ad7c74a Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 31 Mar 2020 04:01:00 -0600 Subject: [PATCH 4/5] debugging ImmutableConfigValue --- pyutilib/misc/config.py | 11 +++++++++++ pyutilib/misc/tests/test_config.py | 12 ++++++++++++ 2 files changed, 23 insertions(+) diff --git a/pyutilib/misc/config.py b/pyutilib/misc/config.py index dd2a1a49..80babeb3 100644 --- a/pyutilib/misc/config.py +++ b/pyutilib/misc/config.py @@ -1072,6 +1072,17 @@ class ImmutableConfigValue(ConfigValue): def set_value(self, value): raise RuntimeError(str(self) + ' is currently immutable') + def reset(self): + try: + super(ImmutableConfigValue, self).set_value(self._default) + except: + if hasattr(self._default, '__call__'): + super(ImmutableConfigValue, self).set_value(self._default()) + else: + raise + self._userAccessed = False + self._userSet = False + class MarkImmutable(object): """ diff --git a/pyutilib/misc/tests/test_config.py b/pyutilib/misc/tests/test_config.py index 7190227c..053d0925 100644 --- a/pyutilib/misc/tests/test_config.py +++ b/pyutilib/misc/tests/test_config.py @@ -65,6 +65,18 @@ def test_immutable_config_value(self): config.a = 6 self.assertEqual(config.a, 6) + config.declare('c', ConfigValue(default=-1, domain=int)) + locker = MarkImmutable(config.get('a'), config.get('b')) + config2 = config({'c': -2}) + self.assertEqual(config2.a, 6) + self.assertEqual(config2.b, 5) + self.assertEqual(config2.c, -2) + with self.assertRaises(Exception): + config3 = config({'a': 1}) + locker.release_lock() + config3 = config({'a': 1}) + self.assertEqual(config3.a, 1) + class Test(unittest.TestCase): From 9cc57f0d42443f6961e6b480393b3e3063dccd97 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 31 Mar 2020 04:19:15 -0600 Subject: [PATCH 5/5] updates to ImmutableConfigValue --- pyutilib/misc/config.py | 4 +++- pyutilib/misc/tests/test_config.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pyutilib/misc/config.py b/pyutilib/misc/config.py index 80babeb3..e7744ad2 100644 --- a/pyutilib/misc/config.py +++ b/pyutilib/misc/config.py @@ -1070,7 +1070,9 @@ def _data_collector(self, level, prefix, visibility=None, docMode=False): class ImmutableConfigValue(ConfigValue): def set_value(self, value): - raise RuntimeError(str(self) + ' is currently immutable') + if self._cast(value) != self._data: + raise RuntimeError(str(self) + ' is currently immutable') + super(ImmutableConfigValue, self).set_value(value) def reset(self): try: diff --git a/pyutilib/misc/tests/test_config.py b/pyutilib/misc/tests/test_config.py index 053d0925..5a9e8b34 100644 --- a/pyutilib/misc/tests/test_config.py +++ b/pyutilib/misc/tests/test_config.py @@ -54,6 +54,10 @@ def test_immutable_config_value(self): config.a = 4 with self.assertRaises(Exception): config.b = 5 + config.a = 2 + config.b = 3 + self.assertEqual(config.a, 2) + self.assertEqual(config.b, 3) locker.release_lock() config.a = 4 config.b = 5