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

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.

Python, 172 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
"""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'