Welcome, guest | Sign In | My Account | Store | Cart

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.

Python, 191 lines
  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)