An immutable dictionary-like structure, intended for configuration options. Similar to frozendict, and other similar projects. See this rejected PEP for a partial list, or this more recent PEP.
with some differences:
- Additional constraints on modifications through
__setitem__
- Access to values using
__getitem__
like Bunch - Selectively extract values only for known keys
- Drop (remove) keys
- Handles changes to nested data in an immutable way
The use case is where there are many components which have configuration options, and settings need to be passed from e.g. the user down through several layers to the components.
- Default settings are defined by the individual components which depend on them
- Higher level components collect default values from sub-components
- The user settings are filtered and passed down to the sub-components
- Using immutable structures minimises unexpected side-effects when settings are shared between components.
In this example class C
depends on classes A
and B
from frozen_options import Options
class A:
defaults = Options(setting = 3)
def __init__(self, user_settings):
self.settings = self.defaults.takeValues(user_settings)
class B:
defaults = Options(greeting = "hello")
def __init__(self, user_settings):
self.settings = self.defaults.takeValues(user_settings)
class C:
defaults = Options(A.defaults, B.defaults, # Collect defaults from sub-components
answer = 42)
def __init__(self, user_settings, **kwargs):
self.settings = self.defaults.takeValues(user_settings, kwargs)
def doSomething(self):
a = A(self.settings) # Pass settings down
Options
also handles nested structures, so if name clashes need to be avoided
then class C
could be defined as:
class C:
defaults = Options(A = A.defaults, # Nested Options
B = B.defaults,
answer = 42)
def __init__(self, *args, **kwargs):
self.settings = self.defaults.takeValues(*args, kwargs) # Merges nested Options
def doSomething(self):
a = A(self.settings.A)
c = C(A = {'setting':4}) # c.settings => Options(A=Options(setting=4),
# B=Options(greeting='hello'),
# answer=42)
Options can be created by setting key-values
from frozen_options import Options
option = Options(value = 42, greeting = "hello")
print(option) # => {'value': 42, 'greeting': 'hello'}
which can be combined with dictionaries or other dict
like mapping
collections, including other Options
:
mydict = {"pi":3.14, "alpha":0.007297}
option = Options(mydict, another="something")
print(option) # => {'pi': 3.14, 'alpha': 0.007297, 'another': 'something'}
Don’t! Once an Options
object has been created, it can be passed around as a value,
without fear that it will be modified. The following will all raise TypeError
exceptions:
option.changes = 8 # => TypeError
option["something"] = None # => TypeError
del option["alpha"] # => TypeError
Note: Because this is python, there is always a way to modify the data if you really want to.
Options implements collections.Mapping
, so can be used in many places which expect
a dict
. If needed, it can be converted back to a dict
:
mutable_data = dict(option)
This creates a copy, so the result mutable_data
can be modified
without affecting option
.
Options can be changed by creating a new Options
object. Two use
cases are supported: A new Options
object can be created as a union
of other Options
, dicts and keywords; and values can be taken for
known keys using takeValues
.
Options
can be initialised with a combination of Options
, dict
and other mapping objects, and keywords. First the arguments are
processed in order, merging nested mapping structures (trees of
Options or dicts). This can be used to transform values in nested
Options
. Keywords are processed last, and replace keys set earlier.
options = Options(value = 42, greeting = "hello")
more_options = Options(options, value = 3, pi = 3.14)
print(more_options) # => {'value': 3, 'greeting': 'hello', 'pi': 3.14}
Note that the original options
object is not changed. By combining
dicts
, Options
or other mapping objects, Options
initialisation
creates the union of these objects:
mydict = {'pi':3.14, 'alpha':0.007297}
more_options = Options(options, mydict)
print(more_options) # => {'value': 42, 'greeting': 'hello', 'pi': 3.14, 'alpha': 0.007297}
Because Options construction merges nested mapping structures, keys in nested structures can be transformed, to an arbitrary depth:
options = Options(value = 42,
nested = Options(greeting = "hello",
pi = 3.14))
# Transform nested structure
new_options = Options(options, {'nested':{'pi':3, 'alpha': 0.007297}})
print(new_options) # => {'value': 42, 'nested': Options(greeting='hello', pi=3, alpha=0.007297)}
Note that here the nested Options
has been transformed, modifying
the pi
value, and adding the alpha
key. If instead we wanted
to replace the nested options, rather than merging them, we could use
the keyword
new_options = Options(options, nested=Options(pi=3, alpha=0.007297))
print(new_options) # => {'value': 42, 'nested': Options(pi=3, alpha=0.007297)}
The other use-case is where there is a collection of default options, and a user-supplied collection of settings. Not all of the user settings may apply to a particular part of the code, so here we just want to take the keys we know about from the user settings.
default = Options(greeting = "hello", value = 3)
# User supplies some settings, including options not needed here
user_settings = Options(value = 42, other_setting = "Goodbye")
settings = default.takeValues(user_settings)
print(settings) # => {'greeting': 'hello', 'value': 42}
Note that other_setting
was ignored because it was not in the default options.
This also works on arbitrarily nested Options
objects.
A new Options can be created, without copying any keys in a given list:
options = Options(value = 42, greeting = 'hello', pi=3.14)
smaller = options.drop('greeting', 'value')
print(smaller) # => {'pi': 3.14}
or this could be done by filtering, or a dict comprehension:
another = Options({key:value for (key,value) in options.items()
if key != "pi"})
print(another) # => {'value': 42, 'greeting': 'hello'}