| 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): |
||
| 97 | self.appname = appname |
||
| 98 | self.package = 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, None) |
||
| 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, key, value): |
||
| 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: {}".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 |