diff --git a/README.md b/README.md index 72c5526..efe8e41 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,17 @@ If the configuration file content is: the result will be `{'section1': {'key1': 'value1', 'key2': 'value2', 'number': 42, 'number2': 3.14, 'number3': -1, 'boolean': True, 'boolean2': False, 'null': None}, 'section2': {'hello': 'world'}, 'section3': {'key': 'value'}}` + + +## Full documentation + +For the full documentation, see the Python docstrings. + ## Changelog + +### Version 1.3.0 +- support for mutable and immutable configurations + ### Version 1.2.1 - Bug fix: `jeb_utils.exceptions.ConfigurationSyntaxError` has inherited from `SyntaxError`, which is semantically wrong. Now it is a normal exception and it inherits from `Exception`. diff --git a/pyproject.toml b/pyproject.toml index d6710a6..db320a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,6 @@ build-backend = "setuptools.build_meta" [project] name = "config-parser" -version = "1.2.1" +version = "1.3.0" description = "A configuration file parser." license = "Apache-2.0" \ No newline at end of file diff --git a/src/config_parser/__init__.py b/src/config_parser/__init__.py index 92f2b3a..da5917c 100644 --- a/src/config_parser/__init__.py +++ b/src/config_parser/__init__.py @@ -24,7 +24,8 @@ Modules: - serialize: A package including the serializers. ''' -from ._configuration import Configuration +from ._configuration import * +from ._configuration import __all__ as _configuration__all__ from . import exceptions from . import json from . import ini @@ -32,10 +33,10 @@ from . import parse from . import serialize __all__ = [ - 'Configuration', 'exceptions', 'json', 'ini', 'parse', - 'serialize' + 'serialize', + *_configuration__all__ ] \ No newline at end of file diff --git a/src/config_parser/_configuration.py b/src/config_parser/_configuration.py index 18f1fae..d62982b 100644 --- a/src/config_parser/_configuration.py +++ b/src/config_parser/_configuration.py @@ -13,49 +13,117 @@ # limitations under the License. import copy +from types import FunctionType +from typing import Any __all__ = [ - 'Configuration' + 'Configuration', + 'set_mutability', + 'is_mutable', ] class Configuration: - def __init__(self, _config: dict = {}): + def __init__(self, _config: dict = {}, mutable: bool = True) -> None: '''A base class for configurations.''' self._config = copy.deepcopy(_config) + self._mutable = mutable - def __getattribute__(self, name): - _config = super().__getattribute__('_config') + def _check_mutability(self) -> None: + return self._mutable + + def _mutate(self, func: FunctionType) -> FunctionType: + def wrapper(*args, **kwargs) -> Any: + if self._check_mutability(): + return func(*args, **kwargs) + else: + raise TypeError('configuration is immutable') + return wrapper + + + def _set(self, name: str, value: str) -> None: + @self._mutate + def _set(name: str, value: str) -> None: + self._config[name] = value + + _set(name, value) + + def _delete(self, name: str) -> None: + @self._mutate + def _delete(name: str) -> None: + del self._config[name] + + _delete(name) + + def _get(self, name: str) -> str: + return self._config[name] + + def __getattribute__(self, name: str): if name.startswith('_') or name in self.__dir__(): return super().__getattribute__(name) else: - if name not in _config: + if name not in self._config: raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") - return _config[name] + return self._get(name) def __setattr__(self, name, value): if name.startswith('_') or name in self.__dir__(): super().__setattr__(name, value) else: - self._config[name] = value + self._set(name, value) def __delattr__(self, name): if name.startswith('_') or name in self.__dir__(): super().__delattr__(name) else: - del self._config[name] + self._delete(name) def __setitem__(self, key, value): - self._config[key] = value + self._set(key, value) def __getitem__(self, key): return self._config[key] def __delitem__(self, key): - del self._config[key] + self._delete(key) def __iter__(self): return iter(self._config.items()) def __contains__(self, item): - return item in self._config \ No newline at end of file + return item in self._config + +def set_mutability(configuration: Configuration, mutable: bool, *, operate_on_original_object: bool = True) -> Configuration: + ''' + Sets the mutability of a configuration. + + :param configuration: The configuration object. + :type configuration: Configuration + :param mutable: The new mutability. + :type mutable: bool + :param operate_on_original_object: If ``True``, the mutability of the + original object will be set. If ``False``, the original object will be + unchanged. The new configuration will always be returned. + :type operate_on_original_object: bool + + :return: The configuration object with the new mutability + :rtype: Configuration + ''' + + if not operate_on_original_object: + configuration = copy.deepcopy(configuration) + + configuration._mutable = mutable + return configuration + +def is_mutable(configuration: Configuration) -> Configuration: + ''' + Checks whether a configuration is mutable. + + :param configuration: The configuration. + :type configuration: Configuration + + :return: The mutability + :rtype: bool + ''' + return configuration._mutable \ No newline at end of file diff --git a/tests/ini/test_ini.py b/tests/ini/test_ini.py index 62f818c..f2d0438 100644 --- a/tests/ini/test_ini.py +++ b/tests/ini/test_ini.py @@ -14,6 +14,7 @@ from src.config_parser.ini import INIConfiguration, INIConfigurationSection from src.config_parser.exceptions import INISyntaxError +from src.config_parser import set_mutability def test_generating(): config = INIConfiguration() @@ -71,4 +72,26 @@ l2=w2''') assert dict(INIConfiguration.from_string('''[section1] k1=v1 k2=v2 -k4=v4''', default = default_configuration)) == {'section1': {'k1': 'v1', 'k2': 'v2', 'k3': 'v3', 'k4': 'v4'}, 'section2': {'l1': 'w1', 'l2': 'w2'}} \ No newline at end of file +k4=v4''', default = default_configuration)) == {'section1': {'k1': 'v1', 'k2': 'v2', 'k3': 'v3', 'k4': 'v4'}, 'section2': {'l1': 'w1', 'l2': 'w2'}} + +def test_mutability(): + config = INIConfiguration(mutable=False) + try: + config.key = 'value' + assert False, 'Expected TypeError' + except TypeError: + pass + + set_mutability(config, True) + assert config._mutable == True + + config.key = 'value' + + set_mutability(config, False) + assert config._mutable == False + + try: + del config.key + assert False, 'Expected TypeError' + except TypeError: + pass \ No newline at end of file diff --git a/tests/json/test_json.py b/tests/json/test_json.py index a27c1de..1988a86 100644 --- a/tests/json/test_json.py +++ b/tests/json/test_json.py @@ -14,6 +14,7 @@ from src.config_parser.json import JSONConfiguration from src.config_parser.exceptions import JSONObjectSyntaxError, JSONValueSyntaxError, JSONStringSyntaxError, JSONArraySyntaxError, EscapeSequenceSyntaxError +from src.config_parser import set_mutability def test_json_configuration(): # Test valid JSON configuration parsing @@ -220,4 +221,26 @@ def test_json_configuration(): JSONConfiguration.from_string('') assert False, 'Excepted JSONValueSyntaxError' except JSONValueSyntaxError: + pass + +def test_mutability(): + config = JSONConfiguration(mutable=False) + try: + config.key = 'value' + assert False, 'Expected TypeError' + except TypeError: + pass + + set_mutability(config, True) + assert config._mutable == True + + config.key = 'value' + + set_mutability(config, False) + assert config._mutable == False + + try: + del config.key + assert False, 'Expected TypeError' + except TypeError: pass \ No newline at end of file diff --git a/tests/test_configuration_class.py b/tests/test_configuration_class.py index b96286b..4a86e74 100644 --- a/tests/test_configuration_class.py +++ b/tests/test_configuration_class.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from src.config_parser import Configuration +from src.config_parser import Configuration, set_mutability, is_mutable def test_crud_configuration_attrs(): config = Configuration() @@ -76,4 +76,37 @@ def test_crud_configuration_config_items(): config.key1 assert False, "AttributeError was not raised" except AttributeError: - pass \ No newline at end of file + pass + +def test_mutability(): + config = Configuration(mutable=False) + try: + config.key = 'value' + assert False, 'Expected TypeError' + except TypeError: + pass + + set_mutability(config, True) + assert config._mutable == True + + config.key = 'value' + + set_mutability(config, False) + assert config._mutable == False + assert is_mutable(config) == False + + try: + del config.key + assert False, 'Expected TypeError' + except TypeError: + pass + + + # Test operate_on_original_object parameter of set_mutability + config = Configuration(mutable = True) + set_mutability(config, False, operate_on_original_object = True) + assert is_mutable(config) == False + + new_config = set_mutability(config, True, operate_on_original_object = False) + assert is_mutable(new_config) == True + assert is_mutable(config) == False \ No newline at end of file