Skip to content
Snippets Groups Projects
Commit 91f2927e authored by Rob Moss's avatar Rob Moss
Browse files

Allow override_dict to delete keys

This is achieved by defining a special class, DELETE_KEY, that cannot be
instantiated and whose only purpose is to identify keys that should be
deleted from the original dictionary.

Note that scenarios defined in TOML files cannot use this feature unless
we implement a value-substitution step when loading scenario definitions
that replaces an arbitrary "magic string" (e.g., "DELETE_KEY") with the
DELETE_KEY class --- and I do not intend to implement such a feature. It
would mean, among other things, that scenario files would no longer be
pure TOML.
parent 43290afc
Branches feature/cache-docs feature/override-delete
Tags
No related merge requests found
Pipeline #32665 passed
......@@ -45,4 +45,6 @@ Internal functions
.. autofunction:: override_dict
.. autoclass:: DELETE_KEY
.. autofunction:: as_list
......@@ -427,6 +427,27 @@ def scenario_observation_model_parameters(scenario):
return obs_models
class _SentinelMeta(type):
"""
A custom metaclass that uses the class name for canonical string
representation, as returned by the built-in ``repr()`` function.
"""
def __repr__(cls):
return f'{cls.__name__}'
class DELETE_KEY(metaclass=_SentinelMeta):
"""
A special value used to indicate that a dictionary key should be deleted,
rather than replaced, when using :func:`override_dict`.
"""
def __init__(self):
msg = f'Class f{type(self).__name__} should not be instantiated'
raise ValueError(msg)
def override_dict(defaults, overrides):
"""
Override a dictionary with values in another dictionary. This will
......@@ -436,8 +457,8 @@ def override_dict(defaults, overrides):
must be a dictionary in order for nested defaults to be propagated.
Otherwise, the default value is simply replaced by the override value.
.. note:: Values in ``defaults`` can only be **replaced**, they cannot be
**removed**.
.. note:: To **remove** keys from ``defaults``, use the special value
:class:`DELETE_KEY`, as shown below.
:param dict defaults: The original values; note that this dictionary
**will be modified**.
......@@ -451,6 +472,14 @@ def override_dict(defaults, overrides):
>>> x = {'a': 1, 'b': 2, 'c': {'x': 3, 'y': 4}}
>>> override_dict(x, {'c': {'x': 10}})
{'a': 1, 'b': 2, 'c': {'x': 10, 'y': 4}}
Use the special value :class:`DELETE_KEY` to delete keys rather than
replacing their values:
>>> from pypfilt.scenario import override_dict, DELETE_KEY
>>> x = {'a': 1, 'b': 2, 'c': {'x': 3, 'y': 4}}
>>> override_dict(x, {'c': {'x': DELETE_KEY}})
{'a': 1, 'b': 2, 'c': {'y': 4}}
"""
for (key, value) in overrides.items():
if isinstance(value, dict):
......@@ -461,6 +490,9 @@ def override_dict(defaults, overrides):
else:
# Replace the default value with this dictionary.
defaults[key] = value
elif value is DELETE_KEY:
# Delete the key, if it exists.
defaults.pop(key, None)
else:
defaults[key] = value
return defaults
"""Test that `pypfilt.scenario.override_dict()` behaves as expected."""
from pypfilt.scenario import override_dict
from pypfilt.scenario import override_dict, DELETE_KEY
def test_override_dict_flat():
......@@ -132,3 +132,50 @@ def test_override_dict_nested_with_nested():
}
override_dict(start, overrides)
assert start == expect
def test_override_dict_nested_with_deletes():
"""
Ensure that deleting keys in a nested dictionary works as expected.
Deleting keys that don't exist should have no effect.
"""
start = {
'a': 1,
'b': {
'x': 10,
'y': 100,
'z': {
'alpha': -1,
'beta': -2,
},
},
'c': 3,
}
overrides = {
'b': {
'x': {
'omega': -10,
},
'y': 50,
'z': DELETE_KEY,
# NOTE: this key does not exist.
'zzz': DELETE_KEY,
},
'c': DELETE_KEY,
'd': 5,
# NOTE: this key does not exist.
'e': DELETE_KEY,
}
expect = {
'a': 1,
'b': {
'x': {
'omega': -10,
},
'y': 50,
},
'd': 5,
}
override_dict(start, overrides)
assert start == expect
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment