diff --git a/CHANGELOG.md b/CHANGELOG.md index b3e58f7319342388df2ee6b697aad1683bb5a3c9..687c4c4e355722313bfe6832916c40b8b4963e37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## Version 0.8.0 - Released 2024-08-14 +### Add +* Add experimental new flag `__allow_newattr__` which relaxes the constraint + that you can't add keys on the fly. + ### Removed * Remove 3.6 and 3.7 support diff --git a/scriptconfig/config.py b/scriptconfig/config.py index a71c4d6842decbe905c5d28b775e6b89c1ff6956..be32cdf92ff3fe051b5ecdff5a8782627e9f7fb5 100644 --- a/scriptconfig/config.py +++ b/scriptconfig/config.py @@ -308,6 +308,7 @@ class Config(ub.NiceRepr, DictLike, metaclass=MetaConfig): """ __scfg_class__ = 'Config' __default__ = {} + # __allow_newattr__ = False def __init__(self, data=None, default=None, cmdline=False, _dont_call_post_init=False): @@ -525,13 +526,17 @@ class Config(ub.NiceRepr, DictLike, metaclass=MetaConfig): if key not in self._data: key = self._normalize_alias_key(key) if key not in self._data: - raise Exception('Cannot add keys to ScriptConfig objects') + if not getattr(self, '__allow_newattr__', False): + raise Exception( + 'Cannot add keys to scriptconfig.Config objects unless ' + 'self.__allow_newattr__ is True' + ) if scfg_isinstance(value, Value): # If the new item is a Value object simply overwrite the old one self._data[key] = value else: - template = self.__default__[key] - if scfg_isinstance(template, Value): + template = self.__default__.get(key, None) + if template is not None and scfg_isinstance(template, Value): # If the new value is raw data, and we have a underlying Value # object update it. self._data[key] = template.cast(value) @@ -788,7 +793,7 @@ class Config(ub.NiceRepr, DictLike, metaclass=MetaConfig): """ if getattr(self, '_alias_map', None) is None: self._alias_map = self._build_alias_map() - return self._alias_map[key] + return self._alias_map.get(key, key) def _normalize_alias_dict(self, data): """ diff --git a/scriptconfig/dataconfig.py b/scriptconfig/dataconfig.py index 4e423676e9aa7e7f0059a6fc4e4e5bfd185c3615..3941c5735cd58d1dd4df5919005c33159d934588 100644 --- a/scriptconfig/dataconfig.py +++ b/scriptconfig/dataconfig.py @@ -337,16 +337,23 @@ class DataConfig(Config, metaclass=MetaDataConfig): Forwards setattrs in the configuration to the dictionary interface, otherwise passes it through. """ - if not key.startswith('_') and getattr(self, '_enable_setattr', False) and key in self: - # After object initialization allow the user to use setattr on any - # value in the underlying dictionary. Everything else uses the - # normal mechanism. - try: - self[key] = value - except KeyError: - raise AttributeError(key) - else: + if key.startswith('_'): + # Currently we do not allow leading underscores to be config + # values to give us some flexibility for API changes. self.__dict__[key] = value + else: + can_setattr = (getattr(self, '__allow_newattr__', False)) # case where user can add new keys on the fly + can_setattr |= (getattr(self, '_enable_setattr', False) and key in self) # internal usage for initialization + if can_setattr: + # After object initialization allow the user to use setattr on any + # value in the underlying dictionary. Everything else uses the + # normal mechanism. + try: + self[key] = value + except KeyError: + raise AttributeError(key) + else: + self.__dict__[key] = value @classmethod def legacy(cls, cmdline=False, data=None, default=None, strict=False): diff --git a/tests/test_newattr.py b/tests/test_newattr.py new file mode 100644 index 0000000000000000000000000000000000000000..048a9d0c940f0f3bb6407420d408cf6232d7c15a --- /dev/null +++ b/tests/test_newattr.py @@ -0,0 +1,47 @@ +def test_newattr(): + """ + Create an empty config and test different ways of adding new attributes. + Test allowed and disallowed cases. + """ + + import scriptconfig as scfg + import pytest + class TestNewattrCLI(scfg.DataConfig): + ... + + config = TestNewattrCLI() + + # By default new attributes are not allowed via the dictionary interface + with pytest.raises(Exception): + config['newattr1'] = 123 + + # Quirk: they are allowed via setattr, but they do not become part of the + # config. This is something we could change. + config.newattr2 = 456 + assert 'newattr2' in config.__dict__ + assert 'newattr2' not in config + assert config.newattr2 == 456, ( + 'even though it is not in the config, you can still access it ' + 'to cary info around') + print(f'config={config}') + print(f'config.__dict__={config.__dict__}') + + config = TestNewattrCLI() + # Enable experimental newattr + config.__allow_newattr__ = True + + config['newattr1'] = 123 + config.newattr2 = 456 + + assert 'newattr2' in config + assert 'newattr1' in config + print(f'config={config}') + + +if __name__ == '__main__': + """ + + CommandLine: + python ~/code/scriptconfig/tests/test_newattr.py + """ + test_newattr()