-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdeclare_config.py
281 lines (202 loc) · 8.69 KB
/
declare_config.py
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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
"""Module for defining configuration file items declaratively."""
import os
import pathlib
import re
import yaml
# ============================================================================
# Configuration File Base Class
# ============================================================================
class Configuration:
"""Base class for Configuration objects.
Subclasses can take any of the decorators in this module defined for
configuration classes and can declare settings using the Setting
descriptor.
"""
def __init__(self, config_data=None):
"""Constructor.
@param config_data: The data with which to populate the configuration.
"""
self._config_data = config_data or {}
provider = None
@classmethod
def load(cls, *args, **kwargs):
"""
"""
if args or kwargs:
config_data = load_configuration(*args, **kwargs)
else:
if cls.provider is None:
raise TypeError(cls.__name__ + " has no provider defined")
config_data = cls.provider()
if config_data is None:
raise ValueError("configuration file could not be loaded")
return cls(config_data)
def _resolve(self, config_path, default=None):
path_parts = config_path.split('.')
current_path = self._config_data
for item in path_parts:
if item not in current_path:
return None
current_path = current_path[item]
else:
return current_path
@classmethod
def setting_definitions(cls):
for attr_name in dir(cls):
attr_value = getattr(cls, attr_name)
if isinstance(attr_value, Setting):
yield attr_value
# ============================================================================
# Configuration Providers
# ============================================================================
def load_configuration_from_file(file_location, must_exist=False):
file_location = pathlib.Path(file_location).expanduser()
if not file_location.exists():
if must_exist:
raise ValueError("could not find configuration file at "
+ str(file_location))
else:
return None
with file_location.open() as stream:
return yaml.load(stream, Loader=yaml.FullLoader)
def load_configuration_from_environment_variable(ev_name, must_exist=True):
ev_value = os.getenv(ev_name)
if ev_value is None:
return None
else:
return load_configuration_from_file(ev_value, must_exist=must_exist)
def load_configuration(location, **kwargs):
"""Attempts to load the configuration file as a dictionary from location.
Returns None if the location cannot be found.
"""
if isinstance(location, dict):
return location
elif location.startswith("$"):
ev_name = location[1:]
return load_configuration_from_environment_variable(ev_name, **kwargs)
else:
return load_configuration_from_file(location, **kwargs)
def chain_providers(func1, func2):
if func2 is None:
return func1
def chained(cls, *args, **kwargs):
result = func1(cls)
if result is not None:
return result
else:
return func2()
return chained
def configuration_provider(location, **kwargs):
def decorator(configuration_class):
configuration_class.provider = classmethod(
chain_providers(
lambda cls: load_configuration(location, **kwargs),
configuration_class.provider))
return configuration_class
return decorator
# ============================================================================
# Configuration value descriptor
# ============================================================================
class Setting:
def __init__(self, config_path=None, default=None, setting_type=str):
"""Constructor.
@param config_path: The path in the configuration file to the item.
@param defult: The default value to use if no setting value is given.
@param setting_type: A function to be applied to the resulting string
configuration setting (usually a constructor for a type, e.g.,
`int` or `pathlib.Path`). Applied after the default has been
applied.
"""
if config_path is None and default is None:
raise ValueError("must provide config_path or default")
self.config_path = config_path
self.default = default
if not callable(setting_type):
raise ValueError("must provide callable for setting_type")
self.setting_type = setting_type
self.preprocessors = []
self.postprocessors = []
def register_preprocessor(self, preprocessor):
"""Add a preprocessor function to be applied before the setting_type.
Preprocessors are callable objects taking the settings object
instance as the first positional argument and the string configured
value for this particular setting (and all previously-applied
preprocessors) as the second. Preprocessors are called in order of
registration.
@param preprocessor: The preprocessor function as described above.
"""
self.preprocessors.append(preprocessor)
def register_postprocessor(self, postprocessor):
"""Add a postprocessor function to be applied after the setting_type.
Postprocessors are callable objects taking the settings object
instance as the first positional argument and the result of calling
setting_type (and all previously-applied postprocessors) on the
configured value for this particular setting as the second.
Postprocessors are called in order of registration.
@param preprocessor: The preprocessor function as described above.
"""
self.postprocessors.append(postprocessor)
def _get_configured_value(self, instance):
"""Get the configured string from the Configuration object."""
configured_value = None
if self.config_path is not None:
configured_value = instance._resolve(self.config_path)
if configured_value is None:
if self.default is None:
raise ValueError("missing required config " + self.config_path)
else:
configured_value = self.default
return configured_value
def _process_configured_value(self, instance, configured_value):
"""Apply all pre-/post-processors to the given configured value."""
result = configured_value
for preprocessor in self.preprocessors:
result = preprocessor(instance, result)
result = self.setting_type(result)
for postprocessor in self.postprocessors:
result = postprocessor(instance, result)
return result
def __get__(self, instance, owner):
"""
@param instance: The instance this object belongs to, or None if this
object is being referenced from a class context.
@param owner: The class to which this endpoint belongs.
"""
if instance is None:
return self
configured_value = self._get_configured_value(instance)
return self._process_configured_value(instance, configured_value)
# ============================================================================
# Special Decorators
# ============================================================================
def register_preprocessor(preprocessor):
def decorator(cls):
for configured_value in cls.setting_definitions():
configured_value.register_preprocessor(preprocessor)
return cls
return decorator
def register_postprocessor(postprocessor):
def decorator(cls):
for configured_value in cls.setting_definitions():
configured_value.register_postprocessor(postprocessor)
return cls
return decorator
def enable_expanduser(decorated):
"""Decorator enabling HOME expansion (~) for Path settings."""
def postprocessor(settings, setting_value):
if isinstance(setting_value, pathlib.Path):
return setting_value.expanduser()
else:
return setting_value
return register_postprocessor(postprocessor)(decorated)
def enable_nested_settings(decorated):
"""Decorator that enables nested settings.
@param config_class: The decorated class
"""
def preprocessor(settings, setting_value):
return re.sub(
r'\$\{(\w+)\}',
lambda m: str(getattr(settings, m.group(1))),
str(setting_value)
)
return register_preprocessor(preprocessor)(decorated)