Provides a simple way to deal with program variable versioning.
This module defines two classes to store application settings so that multiple file versions can coexist with each other. Loading and saving is designed to preserve all data among the different versions. Errors are generated to protect the data when type or value violations occur.
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 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | """Provides a simple way to deal with program variable versioning.
This module defines two classes to store application settings so that
multiple file versions can coexist with each other. Loading and saving
is designed to preserve all data among the different versions. Errors
are generated to protect the data when type or value violations occur."""
__author__ = 'Stephen "Zero" Chappell <Noctis.Skytower@gmail.com>'
__date__ = '4 July 2012'
__version__ = 1, 0, 0
################################################################################
import pickle
import pickletools
import zlib
################################################################################
class _Settings:
"_Settings(*args, **kwargs) -> NotImplementedError exception"
def __init__(self, *args, **kwargs):
"Notify the instantiator that this class is abstract."
raise NotImplementedError('This is an abstract class!')
@staticmethod
def _save(path, obj):
"Save an object to the specified path."
data = zlib.compress(pickletools.optimize(pickle.dumps(obj)), 9)
with open(path, 'wb') as file:
file.write(data)
@staticmethod
def _load(path):
"Load an object from the specified path."
with open(path, 'rb') as file:
data = file.read()
return pickle.loads(zlib.decompress(data))
################################################################################
class Namespace(_Settings):
"Namespace(**schema) -> Namespace instance"
def __init__(self, **schema):
"Initialize the Namespace instance with a schema definition."
self.__original, self.__dynamic, self.__static, self.__owner = \
{}, {}, {}, None
for name, value in schema.items():
if isinstance(value, _Settings):
if isinstance(value, Namespace):
if value.__owner is not None:
raise ValueError(repr(name) + 'has an owner!')
value.__owner = self
self.__original[name] = value
else:
raise TypeError(repr(name) + ' has bad type!')
def __setattr__(self, name, value):
"Set a named Parameter with a given value to be validated."
if name in {'_Namespace__original',
'_Namespace__dynamic',
'_Namespace__static',
'_Namespace__owner',
'state'}:
super().__setattr__(name, value)
elif '.' in name:
head, tail = name.split('.', 1)
self[head][tail] = value
else:
attr = self.__original.get(name)
if not isinstance(attr, Parameter):
raise AttributeError(name)
attr.validate(value)
if value == attr.value:
self.__dynamic.pop(name, None)
else:
self.__dynamic[name] = value
def __getattr__(self, name):
"Get a Namespace or Parameter value by its original name."
if '.' in name:
head, tail = name.split('.', 1)
return self[head][tail]
if name in self.__dynamic:
return self.__dynamic[name]
attr = self.__original.get(name)
if isinstance(attr, Namespace):
return attr
if isinstance(attr, Parameter):
return attr.value
raise AttributeError(name)
__setitem__ = __setattr__
__getitem__ = __getattr__
def save(self, path):
"Save the state of the entire Namespace tree structure."
if isinstance(self.__owner, Namespace):
self.__owner.save(path)
else:
self._save(path, {Namespace: self.state})
def load(self, path):
"Load the state of the entire Namespace tree structure."
if isinstance(self.__owner, Namespace):
self.__owner.load(path)
else:
self.state = self._load(path)[Namespace]
def __get_state(self):
"Get the state of this Namespace and any child Namespaces."
state = {}
for name, types in self.__static.items():
box = state.setdefault(name, {})
for type_, value in types.items():
box[type_] = value.state if type_ is Namespace else value
for name, value in self.__original.items():
box = state.setdefault(name, {})
if name in self.__dynamic:
value = self.__dynamic[name]
elif isinstance(value, Parameter):
value = value.value
else:
box[Namespace] = value.state
continue
box.setdefault(Parameter, {})[type(value)] = value
return state
def __set_state(self, state):
"Set the state of this Namespace and any child Namespaces."
dispatch = {Namespace: self.__set_namespace,
Parameter: self.__set_parameter}
for name, box in state.items():
for type_, value in box.items():
dispatch[type_](name, value)
def __set_namespace(self, name, state):
"Set the state of a child Namespace."
attr = self.__original.get(name)
if not isinstance(attr, Namespace):
attr = self.__static.setdefault(name, {})[Namespace] = Namespace()
attr.state = state
def __set_parameter(self, name, state):
"Set the state of a child Parameter."
attr = self.__original.get(name)
for type_, value in state.items():
if isinstance(attr, Parameter):
try:
attr.validate(value)
except TypeError:
pass
else:
if value == attr.value:
self.__dynamic.pop(name, None)
else:
self.__dynamic[name] = value
continue
if not isinstance(value, type_):
raise TypeError(repr(name) + ' has bad type!')
self.__static.setdefault(name, {}).setdefault(Parameter, {}) \
[type_] = value
state = property(__get_state, __set_state, doc='Namespace state property.')
################################################################################
class Parameter(_Settings):
"Parameter(value, validator=lambda value: True) -> Parameter instance"
def __init__(self, value, validator=lambda value: True):
"Initialize the Parameter instance with a value to validate."
self.__value, self.__validator = value, validator
self.validate(value)
def validate(self, value):
"Check that value has same type and passes validator."
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):
"Parameter value property."
return self.__value
|
This is a completely revised version of recipe 578173 (Settings Organizer). Setting Namespaces corrects the problem of its predecessor shown when trying to load and save spaces populated with conflicting types. They now store away static data to be incorporated into the save file used by other applications. The following code provides a check and demonstration of the module.
def safe_repr(obj):
return _safe_repr(obj, False, 0)
def _safe_repr(obj, tail, indent):
if isinstance(obj, type):
return obj.__name__
if isinstance(obj, dict) and obj:
return '\n' * tail + _dict_repr(obj, indent + 4 * tail)
return repr(obj)
def _dict_repr(obj, indent):
array, head, end = [], True, len(obj)
for index, (key, value) in enumerate(sorted(obj.items(), key=_key), 1):
array.append(('{}{{{}: {}{}' if head else '\n{} {}: {}{}').format(
' ' * indent, _safe_repr(key, True, indent),
_safe_repr(value, True, indent), ','[index==end:]))
head = False
return ''.join(array) + '}'
_key = lambda p: tuple(i.__name__ if isinstance(i, type) else i for i in p)
################################################################################
LOCAL = \
{Namespace:
{'name_a':
{Namespace:
{'data':
{Parameter:
{dict:
{'a': 'z'}}}},
Parameter:
{bytes: b'xyz',
int: 123,
list: [4, 5, 6],
str: 'abc'}},
'name_b':
{Namespace:
{'info':
{Parameter:
{bytearray: bytearray(b'\x00\x01'),
frozenset: frozenset({1, 9}),
set: {9, 1},
type: Parameter}},
'nice':
{Parameter:
{slice: slice(1, 2, 3)}}}},
'name_c':
{Parameter:
{bool: True,
complex: (1+2j),
float: 3.14,
tuple: (None, 'total')}}}}
################################################################################
N1 = Namespace(
name_a=Namespace(data=Parameter({'a': 'z'})),
name_c=Parameter(3.14))
N2 = Namespace(
name_a=Parameter(123),
name_b=Namespace(info=Parameter(frozenset({9, 1}))),
name_c=Parameter((None, 'total')))
N3 = Namespace(
name_a=Parameter('abc'),
name_b=Namespace(info=Parameter(bytearray(b'\x00\x01'))),
name_c=Parameter(True))
N4 = Namespace(
name_a=Parameter(b'xyz'),
name_b=Namespace(info=Parameter(Parameter)),
name_c=Parameter(1 + 2j))
N5 = Namespace(
name_a=Parameter([4, 5, 6]),
name_b=Namespace(info=Parameter({1, 9}),
nice=Parameter(slice(1, 2, 3))))
################################################################################
import os; path = 'test.sav'
N1.save(path)
print(safe_repr({Namespace: N1.state}) + '\n' + '#' * 40)
N2.load(path)
N2.save(path)
print(safe_repr({Namespace: N2.state}) + '\n' + '#' * 40)
N3.load(path)
N3.save(path)
print(safe_repr({Namespace: N3.state}) + '\n' + '#' * 40)
N4.load(path)
N4.save(path)
print(safe_repr({Namespace: N4.state}) + '\n' + '#' * 40)
N5.load(path)
code = safe_repr({Namespace: N5.state})
print(code + '\n' + '#' * 40)
print(eval(code) == LOCAL)
os.remove(path)