Total Complexity | 40 |
Total Lines | 192 |
Duplicated Lines | 0 % |
Complex classes like auxlib.Configuration often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | # -*- coding: utf-8 -*- |
||
58 | class Configuration(object): |
||
59 | """A map implementation to manage configuration and context information. Values |
||
60 | can be accessed (read, not assigned) as either a dict lookup (e.g. `config[key]`) or as an |
||
61 | attribute (e.g. `config.key`). |
||
62 | |||
63 | This class makes the foundational assumption of a yaml configuration file, as values in yaml |
||
64 | are typed. |
||
65 | |||
66 | This class allows overriding configuration keys with environment variables. Given an app name |
||
67 | `foo` and a config parameter `bar: 15`, setting a `FOO_BAR` environment variable to `22` will |
||
68 | override the value of `bar`. The type of `22` remains `int` because the underlying value of |
||
69 | `15` is used to infer the type of the `FOO_BAR` environment variable. When an underlying |
||
70 | parameter does not exist in a config file, the type is intelligently guessed. |
||
71 | |||
72 | Args: |
||
73 | app_name (str) |
||
74 | config_sources (str or list, optional) |
||
75 | required_parameters (iter, optional) |
||
76 | |||
77 | Raises: |
||
78 | InitializationError: on instantiation, when `required_parameters` are not found |
||
79 | warns: on instantiation, when a given `config_file` cannot be read |
||
80 | NotFoundError: when requesting a key that does not exist |
||
81 | |||
82 | Examples: |
||
83 | >>> for (key, value) in [('FOO_BAR', 22), ('FOO_BAZ', 'yes'), ('FOO_BANG', 'monkey')]: |
||
84 | ... os.environ[key] = str(value) |
||
85 | |||
86 | >>> context = Configuration('foo') |
||
87 | >>> context.bar, context.bar.__class__ |
||
88 | (22, <... 'int'>) |
||
89 | >>> context['baz'], type(context['baz']) |
||
90 | (True, <... 'bool'>) |
||
91 | >>> context.bang, type(context.bang) |
||
92 | ('monkey', <... 'str'>) |
||
93 | |||
94 | """ |
||
95 | |||
96 | def __init__(self, appname, config_sources=None, required_parameters=None, package=None): |
||
97 | self.appname = appname |
||
98 | self.package = package or inspect.getmodule(self).__package__ |
||
99 | |||
100 | # Indicates if sources are being loaded for the first time or are being reloaded. |
||
101 | self.__initial_load = True |
||
102 | |||
103 | # The ordered list of sources from which to load key, value pairs. |
||
104 | self.__sources = list() |
||
105 | |||
106 | # The object that holds the actual key, value pairs after they're loaded from sources. |
||
107 | # Keys are stored as all lower-case. Access by key is provided in a case-insensitive way. |
||
108 | self._config_map = dict() |
||
109 | |||
110 | self._registered_env_keys = set() # TODO: add explanation comments |
||
111 | self._required_keys = set(listify(required_parameters)) |
||
112 | |||
113 | if hasattr(signal, 'SIGHUP'): # Reload config on SIGHUP (UNIX only) |
||
114 | self.__set_up_sighup_handler() |
||
115 | |||
116 | self.__load_environment_keys() |
||
117 | self.append_sources(config_sources) |
||
118 | |||
119 | def append_sources(self, config_sources): |
||
120 | force_reload = True |
||
121 | for source in listify(config_sources): |
||
122 | self.__append_source(source, force_reload) |
||
123 | |||
124 | def append_required(self, required_parameters): |
||
125 | self._required_keys.update(listify(required_parameters)) |
||
126 | |||
127 | def __append_source(self, source, force_reload=False, _parent_source=None): |
||
128 | source.parent_config = self |
||
129 | self.__load_source(source, force_reload) |
||
130 | source.parent_source = _parent_source |
||
131 | self.__sources.append(source) |
||
132 | |||
133 | def verify(self): |
||
134 | self.__ensure_required_keys() |
||
135 | return self |
||
136 | |||
137 | def set_env(self, key, value): |
||
138 | """Sets environment variables by prepending the app_name to `key`. Also registers the |
||
139 | environment variable with the instance object preventing an otherwise-required call to |
||
140 | `reload()`. |
||
141 | """ |
||
142 | os.environ[make_env_key(self.appname, key)] = str(value) # must coerce to string |
||
143 | self._registered_env_keys.add(key) |
||
144 | self._clear_memoization() |
||
145 | |||
146 | def unset_env(self, key): |
||
147 | """Removes an environment variable using the prepended app_name convention with `key`.""" |
||
148 | os.environ.pop(make_env_key(self.appname, key), None) |
||
149 | self._registered_env_keys.discard(key) |
||
150 | self._clear_memoization() |
||
151 | |||
152 | def _reload(self, force=False): |
||
153 | """Reloads the configuration from the file and environment variables. Useful if using |
||
154 | `os.environ` instead of this class' `set_env` method, or if the underlying configuration |
||
155 | file is changed externally. |
||
156 | """ |
||
157 | self._config_map = dict() |
||
158 | self._registered_env_keys = set() |
||
159 | self.__reload_sources(force) |
||
160 | self.__load_environment_keys() |
||
161 | self.verify() |
||
162 | self._clear_memoization() |
||
163 | |||
164 | @memoizemethod # memoized for performance; always use self.set_env() instead of os.setenv() |
||
165 | def __getitem__(self, key): |
||
166 | key = key.lower() |
||
167 | if key in self._registered_env_keys: |
||
168 | from_env = os.getenv(make_env_key(self.appname, key)) |
||
169 | from_sources = self._config_map.get(key) |
||
170 | return typify(from_env, type(from_sources) if from_sources is not None else None) |
||
171 | else: |
||
172 | try: |
||
173 | return self._config_map[key] |
||
174 | except KeyError as e: |
||
175 | raise NotFoundError(e) |
||
176 | |||
177 | def __getattr__(self, key): |
||
178 | return self[key] |
||
179 | |||
180 | def get(self, key, default=None): |
||
181 | try: |
||
182 | return self[key] |
||
183 | except KeyError: |
||
184 | return default |
||
185 | |||
186 | def __setitem__(self, *args): |
||
187 | raise AssignmentError() |
||
188 | |||
189 | def __iter__(self): |
||
190 | for key in self._registered_env_keys | set(self._config_map.keys()): |
||
191 | yield key |
||
192 | |||
193 | def items(self): |
||
194 | for key in self: |
||
195 | yield key, self[key] |
||
196 | |||
197 | def __load_source(self, source, force_reload=False): |
||
198 | if force_reload and source.parent_source: |
||
199 | # TODO: double-check case of reload without force reload for chained configs |
||
200 | return |
||
201 | |||
202 | items = source.dump(force_reload) |
||
203 | if source.provides and not set(source.provides).issubset(items): |
||
204 | raise NotImplementedError() # TODO: fix this |
||
205 | |||
206 | additional_requirements = items.pop('additional_requirements', None) |
||
207 | if isinstance(additional_requirements, string_types): |
||
208 | additional_requirements = additional_requirements.split(',') |
||
209 | self._required_keys |= set(listify(additional_requirements)) |
||
210 | |||
211 | additional_sources = items.pop('additional_sources', None) |
||
212 | |||
213 | self._config_map.update(items) |
||
214 | |||
215 | if additional_sources: |
||
216 | for src in additional_sources: |
||
217 | class_name, kwargs = src.popitem() |
||
218 | additional_source = globals()[class_name](**kwargs) |
||
219 | self.__append_source(additional_source, force_reload, source) |
||
220 | |||
221 | def __load_environment_keys(self): |
||
222 | app_prefix = self.appname.upper() + '_' |
||
223 | for env_key in os.environ: |
||
224 | if env_key.startswith(app_prefix): |
||
225 | self._registered_env_keys.add(reverse_env_key(self.appname, env_key)) |
||
226 | # We don't actually add values to _config_map here. Rather, they're pulled |
||
227 | # directly from os.environ at __getitem__ time. This allows for type casting |
||
228 | # environment variables if possible. |
||
229 | |||
230 | def __ensure_required_keys(self): |
||
231 | available_keys = self._registered_env_keys | set(self._config_map.keys()) |
||
232 | missing_keys = self._required_keys - available_keys |
||
233 | if missing_keys: |
||
234 | raise EnvironmentError("Required key(s) not found in environment\n" |
||
235 | " or configuration sources.\n" |
||
236 | " Missing Keys: {0}".format(list(missing_keys))) |
||
237 | |||
238 | def _clear_memoization(self): |
||
239 | self.__dict__.pop('_memoized_results', None) |
||
240 | |||
241 | def __set_up_sighup_handler(self): |
||
242 | def sighup_handler(signum, frame): |
||
243 | if signum != signal.SIGHUP: |
||
244 | return |
||
245 | self._reload(True) |
||
246 | if callable(self.__previous_sighup_handler): |
||
247 | self.__previous_sighup_handler(signum, frame) |
||
248 | self.__previous_sighup_handler = signal.getsignal(signal.SIGHUP) |
||
249 | signal.signal(signal.SIGHUP, sighup_handler) |
||
250 | |||
319 |