Skip to content

bendudson/frozen-options

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Frozen Options

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

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

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)

Creating Options

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'}

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:

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.

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:

mutable_data = dict(option)

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.

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}

Nested immutable Options

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)}

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.

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.

Removing keys

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'}

About

Immutable configuration data for python

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages