Provide an easy method to manage program options among multiple versions.
This module contains two classes used to store application settings in such a way that multiple file versions can possibly coexist with each other. Loading and saving settings is designed to preserve as much data between versions. An error is generated on loading if saving would lead to any data being lost.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | """Provide an easy method to manage program options among multiple versions.
This module contains two classes used to store application settings in such a
way that multiple file versions can possibly coexist with each other. Loading
and saving settings is designed to preserve as much data between versions. An
error is generated on loading if saving would lead to any data being lost."""
__author__ = 'Stephen "Zero" Chappell <Noctis.Skytower@gmail.com>'
__date__ = '4 July 2012'
__version__ = 1, 0, 2
################################################################################
import pickle
import pickletools
import zlib
################################################################################
class Namespace:
"Namespace(**defaults) -> Namespace instance"
def __init__(self, **defaults):
"Initializes instance varaibles and checks defaults while loading."
self.__defaults, self.__changes, self.__master = {}, {}, None
for key, value in defaults.items():
if isinstance(value, Namespace):
if value.__master is not None:
raise ValueError(repr(key) + ' may not have a master!')
value.__master = self
self.__defaults[key] = value
elif isinstance(value, Parameter):
self.__defaults[key] = value
else:
raise TypeError(repr(key) + ' has unacceptable type!')
def __setattr__(self, name, value):
"Sets attributes after validating that they are allowed."
if name in {'_Namespace__defaults',
'_Namespace__changes',
'_Namespace__master'}:
super().__setattr__(name, value)
elif '.' in name:
head, tail = name.split('.', 1)
self[head][tail] = value
else:
if name not in self.__defaults:
raise AttributeError(name)
attr = self.__defaults[name]
if not isinstance(attr, Parameter):
raise TypeError(name)
attr.validate(value)
self.__update_change(name, value, attr)
def __getattr__(self, name):
"Gets current value of attributes and unpacks if necessary."
if '.' in name:
head, tail = name.split('.', 1)
return self[head][tail]
if name not in self.__defaults:
raise AttributeError(name)
if name in self.__changes:
return self.__changes[name].value
attr = self.__defaults[name]
if isinstance(attr, Parameter):
return attr.value
return attr
__setitem__ = __setattr__
__getitem__ = __getattr__
def save(self, path):
"Saves complete namespace tree to file given by path."
if self.__master is None:
state = self.__get_state()
data = zlib.compress(pickletools.optimize(pickle.dumps(state)), 9)
with open(path, 'wb') as file:
file.write(data)
else:
self.__master.save(path)
def load(self, path):
"Loads complete namespace tree from file given by path."
if self.__master is None:
with open(path, 'rb') as file:
data = file.read()
klass, state = pickle.loads(zlib.decompress(data))
if klass is Namespace:
self.__set_state(state)
else:
self.__master.load(path)
def __get_state(self):
"Gets state of instance while takings changes into account."
state, changes = {}, self.__changes.copy()
for database in self.__defaults, changes:
for key, value in database.items():
if database is not changes:
value = changes.pop(key, value)
if isinstance(value, Namespace):
state[key] = value.__get_state()
else:
state[key] = Parameter, value.value
return Namespace, state
def __set_state(self, state):
"Sets state of instance while validating incoming state."
for key, (klass, value) in state.items():
if klass is Namespace:
self.__set_namespace(key, value)
elif klass is Parameter:
self.__set_parameter(key, value)
def __set_namespace(self, key, value):
"Takes namespace and attempts to update internal state."
if key in self.__defaults:
attr = self.__defaults[key]
if isinstance(attr, Namespace):
attr.__set_state(value)
else:
raise ResourceWarning(repr(key) + ' is not a Namespace!')
else:
attr = self.__changes[key] = Namespace()
attr.__master = self
attt.__set_state(value)
def __set_parameter(self, key, value):
"Takes parameter and attempts to update internal state."
if key in self.__defaults:
attr = self.__defaults[key]
if isinstance(attr, Parameter):
try:
attr.validate(value)
except (TypeError, ValueError):
raise ResourceWarning(repr(key) + ' value failed tests!')
else:
self.__update_change(key, value, attr)
else:
raise ResourceWarning(repr(key) + ' is not a Parameter!')
else:
self.__changes[key] = Parameter(value)
def __update_change(self, key, value, attr):
"Takes key/value pair and updates change database as needed."
if value != attr.value:
self.__changes[key] = Parameter(value)
elif key in self.__changes:
del self.__changes[key]
################################################################################
class Parameter:
"Parameter(value, validator=lambda value: True) -> Parameter instance"
def __init__(self, value, validator=lambda value: True):
"Initializes instance variables and validates the value."
self.__value, self.__validator = value, validator
self.validate(value)
def validate(self, value):
"Verifies that the value has the same type and is considered valid."
if not isinstance(value, type(self.value)):
raise TypeError('Value has a different type!')
if not self.__validator(value):
raise ValueError('Validator failed the value!')
@property
def value(self):
"Returns the value that is associated with this Parameter instance."
return self.__value
|
The module up above was written to allow multiple versions of the same application to store their settings in a shared file. Namespaces organize values in different categories, and Parameters validate and store those values. To retain backward's compatibility among different "schemas" of data, name types should never change (Namespaces becoming Parameters or Parameters becoming Namespaces), Parameter types should never change, and Parameter values should always validate. A future version will probably remove these limitations.
The following code provides a simple test and example of how the code could possibly be used in two different programs. It demonstrates how default values are used, schemas are created, and data persistence is accomplished. The tests at the bottom ensure that the code is working as expected.
import sys
from options import *
SETTINGS_1 = Namespace(
primer_length=Parameter(16, lambda value: value in range(16, 257)),
hint_a=Parameter(''),
hint_b=Parameter(''),
kap=Namespace(
creation=Parameter(
'preset', lambda value: value in {'preset', 'single', 'double'}),
application=Parameter(
'key', lambda value: value in {'key', 'primer', 'keyprimer'}),
priority=Parameter(
'key', lambda value: value in {'key', 'primer', 'keyprimer'})),
source=Parameter(open(sys.argv[0]).read()))
########################################################################
LANGUAGES = {'english', 'spanish'}
CREATION = {'preset', 'single', 'double'}
KEYPRIMER = {'key', 'primer', 'keyprimer'}
language_v = lambda value: value in LANGUAGES
primer_length_v = lambda value: value in range(16, 257)
creation_v = lambda value: value in CREATION
application_v = priority_v = lambda value: value in KEYPRIMER
SETTINGS_2 = Namespace(
language=Parameter('english', language_v),
primer_length=Parameter(16, primer_length_v),
hint_a=Parameter(''),
hint_b=Parameter(''),
kap=Namespace(
creation=Parameter('preset', creation_v),
application=Parameter('key', application_v),
priority=Parameter('key', priority_v)))
########################################################################
assert SETTINGS_2.language == 'english'
SETTINGS_2.language = 'spanish'
assert SETTINGS_2.hint_a == ''
SETTINGS_2.hint_a = 'alpha'
assert SETTINGS_2.hint_b == ''
SETTINGS_2.save('test')
assert SETTINGS_1.hint_a == ''
SETTINGS_1.load('test')
assert SETTINGS_1.hint_a == 'alpha'
assert SETTINGS_1.hint_b == ''
SETTINGS_1.hint_b = 'omega'
SETTINGS_1.save('test')
assert SETTINGS_2.language == 'spanish'
SETTINGS_2.language = 'english'
assert SETTINGS_2.language == 'english'
SETTINGS_2.load('test')
assert SETTINGS_2.language == 'spanish'
assert SETTINGS_2.hint_b == 'omega'