An open API service indexing awesome lists of open source software.

https://github.com/bendudson/frozen-options

Immutable configuration data for python
https://github.com/bendudson/frozen-options

Last synced: about 1 year ago
JSON representation

Immutable configuration data for python

Awesome Lists containing this project

README

          

* Frozen Options

An immutable dictionary-like structure, intended for configuration
options. Similar to [[https://github.com/slezica/python-frozendict][frozendict]], and other similar projects. See [[https://www.python.org/dev/peps/pep-0416/][this
rejected PEP]] for a partial list, or [[https://www.python.org/dev/peps/pep-0603/][this more recent PEP]].

with some differences:
- Additional constraints on modifications through =__setitem__=
- Access to values using =__getitem__= like [[https://pypi.org/project/bunch/][Bunch]]
- Selectively extract values only for known keys
- Drop (remove) keys
- Handles changes to nested data in an immutable way

** Intended use

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=
#+BEGIN_SRC python :session example :results output
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
#+END_SRC

=Options= also handles nested structures, so if name clashes need to be avoided
then class =C= could be defined as:
#+BEGIN_SRC python :session example :results output
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)
#+END_SRC

** Creating Options

Options can be created by setting key-values
#+BEGIN_SRC python :session options-example :results output
from frozen_options import Options

option = Options(value = 42, greeting = "hello")
print(option) # => {'value': 42, 'greeting': 'hello'}
#+END_SRC

#+RESULTS:
: {'value': 42, 'greeting': 'hello'}

which can be combined with dictionaries or other =dict= like mapping
collections, including other =Options=:
#+BEGIN_SRC python :session options-example :results output
mydict = {"pi":3.14, "alpha":0.007297}

option = Options(mydict, another="something")

print(option) # => {'pi': 3.14, 'alpha': 0.007297, 'another': 'something'}
#+END_SRC

#+RESULTS:
: {'pi': 3.14, 'alpha': 0.007297, 'another': 'something'}

** Mutating options

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:
#+BEGIN_SRC python :session options-example :results output
option.changes = 8 # => TypeError
option["something"] = None # => TypeError
del option["alpha"] # => TypeError
#+END_SRC

#+RESULTS:

Note: Because this is python, there is always a way to modify the data
if you really want to.

** Converting back to dictionary

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=:
#+BEGIN_SRC python :session options-example :results output
mutable_data = dict(option)
#+END_SRC
This creates a copy, so the result =mutable_data= can be modified
without affecting =option=.

** Transforming Options

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=.

*** Adding keys and replacing values

=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.
#+BEGIN_SRC python :session options-example :results output
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}
#+END_SRC

#+RESULTS:
: {'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:

#+BEGIN_SRC python :session options-example :results output
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}
#+END_SRC

#+RESULTS:
: {'value': 42, 'greeting': 'hello', 'pi': 3.14, 'alpha': 0.007297}

*** Nested immutable Options

Because Options construction merges nested mapping structures, keys in nested
structures can be transformed, to an arbitrary depth:
#+BEGIN_SRC python :session options-example :results output
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)}
#+END_SRC

#+RESULTS:
: {'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
#+BEGIN_SRC python :session options-example :results output
new_options = Options(options, nested=Options(pi=3, alpha=0.007297))

print(new_options) # => {'value': 42, 'nested': Options(pi=3, alpha=0.007297)}
#+END_SRC

#+RESULTS:
: {'value': 42, 'nested': Options(pi=3, alpha=0.007297)}

*** Replacing values of known keys

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.
#+BEGIN_SRC python :session options-example :results output
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}
#+END_SRC

#+RESULTS:
: {'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.

*** Removing keys

A new Options can be created, without copying any keys in a given list:
#+BEGIN_SRC python :session options-example :results output
options = Options(value = 42, greeting = 'hello', pi=3.14)

smaller = options.drop('greeting', 'value')
print(smaller) # => {'pi': 3.14}
#+END_SRC

#+RESULTS:
: {'pi': 3.14}

or this could be done by filtering, or a dict comprehension:
#+BEGIN_SRC python :session options-example :results output
another = Options({key:value for (key,value) in options.items()
if key != "pi"})
print(another) # => {'value': 42, 'greeting': 'hello'}
#+END_SRC

#+RESULTS:
: {'value': 42, 'greeting': 'hello'}