codekingpro/portable-devtools
114k
1from __future__ import annotations2 3import errno4import json5import os6import types7import typing as t8 9from werkzeug.utils import import_string10 11if t.TYPE_CHECKING:12 import typing_extensions as te13 14 from .sansio.app import App15 16 17T = t.TypeVar("T")18 19 20class ConfigAttribute(t.Generic[T]):21 """Makes an attribute forward to the config"""22 23 def __init__(24 self, name: str, get_converter: t.Callable[[t.Any], T] | None = None25 ) -> None:26 self.__name__ = name27 self.get_converter = get_converter28 29 @t.overload30 def __get__(self, obj: None, owner: None) -> te.Self: ...31 32 @t.overload33 def __get__(self, obj: App, owner: type[App]) -> T: ...34 35 def __get__(self, obj: App | None, owner: type[App] | None = None) -> T | te.Self:36 if obj is None:37 return self38 39 rv = obj.config[self.__name__]40 41 if self.get_converter is not None:42 rv = self.get_converter(rv)43 44 return rv # type: ignore[no-any-return]45 46 def __set__(self, obj: App, value: t.Any) -> None:47 obj.config[self.__name__] = value48 49 50class Config(dict): # type: ignore[type-arg]51 """Works exactly like a dict but provides ways to fill it from files52 or special dictionaries. There are two common patterns to populate the53 config.54 55 Either you can fill the config from a config file::56 57 app.config.from_pyfile('yourconfig.cfg')58 59 Or alternatively you can define the configuration options in the60 module that calls :meth:`from_object` or provide an import path to61 a module that should be loaded. It is also possible to tell it to62 use the same module and with that provide the configuration values63 just before the call::64 65 DEBUG = True66 SECRET_KEY = 'development key'67 app.config.from_object(__name__)68 69 In both cases (loading from any Python file or loading from modules),70 only uppercase keys are added to the config. This makes it possible to use71 lowercase values in the config file for temporary values that are not added72 to the config or to define the config keys in the same file that implements73 the application.74 75 Probably the most interesting way to load configurations is from an76 environment variable pointing to a file::77 78 app.config.from_envvar('YOURAPPLICATION_SETTINGS')79 80 In this case before launching the application you have to set this81 environment variable to the file you want to use. On Linux and OS X82 use the export statement::83 84 export YOURAPPLICATION_SETTINGS='/path/to/config/file'85 86 On windows use `set` instead.87 88 :param root_path: path to which files are read relative from. When the89 config object is created by the application, this is90 the application's :attr:`~flask.Flask.root_path`.91 :param defaults: an optional dictionary of default values92 """93 94 def __init__(95 self,96 root_path: str | os.PathLike[str],97 defaults: dict[str, t.Any] | None = None,98 ) -> None:99 super().__init__(defaults or {})100 self.root_path = root_path101 102 def from_envvar(self, variable_name: str, silent: bool = False) -> bool:103 """Loads a configuration from an environment variable pointing to104 a configuration file. This is basically just a shortcut with nicer105 error messages for this line of code::106 107 app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS'])108 109 :param variable_name: name of the environment variable110 :param silent: set to ``True`` if you want silent failure for missing111 files.112 :return: ``True`` if the file was loaded successfully.113 """114 rv = os.environ.get(variable_name)115 if not rv:116 if silent:117 return False118 raise RuntimeError(119 f"The environment variable {variable_name!r} is not set"120 " and as such configuration could not be loaded. Set"121 " this variable and make it point to a configuration"122 " file"123 )124 return self.from_pyfile(rv, silent=silent)125 126 def from_prefixed_env(127 self, prefix: str = "FLASK", *, loads: t.Callable[[str], t.Any] = json.loads128 ) -> bool:129 """Load any environment variables that start with ``FLASK_``,130 dropping the prefix from the env key for the config key. Values131 are passed through a loading function to attempt to convert them132 to more specific types than strings.133 134 Keys are loaded in :func:`sorted` order.135 136 The default loading function attempts to parse values as any137 valid JSON type, including dicts and lists.138 139 Specific items in nested dicts can be set by separating the140 keys with double underscores (``__``). If an intermediate key141 doesn't exist, it will be initialized to an empty dict.142 143 :param prefix: Load env vars that start with this prefix,144 separated with an underscore (``_``).145 :param loads: Pass each string value to this function and use146 the returned value as the config value. If any error is147 raised it is ignored and the value remains a string. The148 default is :func:`json.loads`.149 150 .. versionadded:: 2.1151 """152 prefix = f"{prefix}_"153 len_prefix = len(prefix)154 155 for key in sorted(os.environ):156 if not key.startswith(prefix):157 continue158 159 value = os.environ[key]160 161 try:162 value = loads(value)163 except Exception:164 # Keep the value as a string if loading failed.165 pass166 167 # Change to key.removeprefix(prefix) on Python >= 3.9.168 key = key[len_prefix:]169 170 if "__" not in key:171 # A non-nested key, set directly.172 self[key] = value173 continue174 175 # Traverse nested dictionaries with keys separated by "__".176 current = self177 *parts, tail = key.split("__")178 179 for part in parts:180 # If an intermediate dict does not exist, create it.181 if part not in current:182 current[part] = {}183 184 current = current[part]185 186 current[tail] = value187 188 return True189 190 def from_pyfile(191 self, filename: str | os.PathLike[str], silent: bool = False192 ) -> bool:193 """Updates the values in the config from a Python file. This function194 behaves as if the file was imported as module with the195 :meth:`from_object` function.196 197 :param filename: the filename of the config. This can either be an198 absolute filename or a filename relative to the199 root path.200 :param silent: set to ``True`` if you want silent failure for missing201 files.202 :return: ``True`` if the file was loaded successfully.203 204 .. versionadded:: 0.7205 `silent` parameter.206 """207 filename = os.path.join(self.root_path, filename)208 d = types.ModuleType("config")209 d.__file__ = filename210 try:211 with open(filename, mode="rb") as config_file:212 exec(compile(config_file.read(), filename, "exec"), d.__dict__)213 except OSError as e:214 if silent and e.errno in (errno.ENOENT, errno.EISDIR, errno.ENOTDIR):215 return False216 e.strerror = f"Unable to load configuration file ({e.strerror})"217 raise218 self.from_object(d)219 return True220 221 def from_object(self, obj: object | str) -> None:222 """Updates the values from the given object. An object can be of one223 of the following two types:224 225 - a string: in this case the object with that name will be imported226 - an actual object reference: that object is used directly227 228 Objects are usually either modules or classes. :meth:`from_object`229 loads only the uppercase attributes of the module/class. A ``dict``230 object will not work with :meth:`from_object` because the keys of a231 ``dict`` are not attributes of the ``dict`` class.232 233 Example of module-based configuration::234 235 app.config.from_object('yourapplication.default_config')236 from yourapplication import default_config237 app.config.from_object(default_config)238 239 Nothing is done to the object before loading. If the object is a240 class and has ``@property`` attributes, it needs to be241 instantiated before being passed to this method.242 243 You should not use this function to load the actual configuration but244 rather configuration defaults. The actual config should be loaded245 with :meth:`from_pyfile` and ideally from a location not within the246 package because the package might be installed system wide.247 248 See :ref:`config-dev-prod` for an example of class-based configuration249 using :meth:`from_object`.250 251 :param obj: an import name or object252 """253 if isinstance(obj, str):254 obj = import_string(obj)255 for key in dir(obj):256 if key.isupper():257 self[key] = getattr(obj, key)258 259 def from_file(260 self,261 filename: str | os.PathLike[str],262 load: t.Callable[[t.IO[t.Any]], t.Mapping[str, t.Any]],263 silent: bool = False,264 text: bool = True,265 ) -> bool:266 """Update the values in the config from a file that is loaded267 using the ``load`` parameter. The loaded data is passed to the268 :meth:`from_mapping` method.269 270 .. code-block:: python271 272 import json273 app.config.from_file("config.json", load=json.load)274 275 import tomllib276 app.config.from_file("config.toml", load=tomllib.load, text=False)277 278 :param filename: The path to the data file. This can be an279 absolute path or relative to the config root path.280 :param load: A callable that takes a file handle and returns a281 mapping of loaded data from the file.282 :type load: ``Callable[[Reader], Mapping]`` where ``Reader``283 implements a ``read`` method.284 :param silent: Ignore the file if it doesn't exist.285 :param text: Open the file in text or binary mode.286 :return: ``True`` if the file was loaded successfully.287 288 .. versionchanged:: 2.3289 The ``text`` parameter was added.290 291 .. versionadded:: 2.0292 """293 filename = os.path.join(self.root_path, filename)294 295 try:296 with open(filename, "r" if text else "rb") as f:297 obj = load(f)298 except OSError as e:299 if silent and e.errno in (errno.ENOENT, errno.EISDIR):300 return False301 302 e.strerror = f"Unable to load configuration file ({e.strerror})"303 raise304 305 return self.from_mapping(obj)306 307 def from_mapping(308 self, mapping: t.Mapping[str, t.Any] | None = None, **kwargs: t.Any309 ) -> bool:310 """Updates the config like :meth:`update` ignoring items with311 non-upper keys.312 313 :return: Always returns ``True``.314 315 .. versionadded:: 0.11316 """317 mappings: dict[str, t.Any] = {}318 if mapping is not None:319 mappings.update(mapping)320 mappings.update(kwargs)321 for key, value in mappings.items():322 if key.isupper():323 self[key] = value324 return True325 326 def get_namespace(327 self, namespace: str, lowercase: bool = True, trim_namespace: bool = True328 ) -> dict[str, t.Any]:329 """Returns a dictionary containing a subset of configuration options330 that match the specified namespace/prefix. Example usage::331 332 app.config['IMAGE_STORE_TYPE'] = 'fs'333 app.config['IMAGE_STORE_PATH'] = '/var/app/images'334 app.config['IMAGE_STORE_BASE_URL'] = 'http://img.website.com'335 image_store_config = app.config.get_namespace('IMAGE_STORE_')336 337 The resulting dictionary `image_store_config` would look like::338 339 {340 'type': 'fs',341 'path': '/var/app/images',342 'base_url': 'http://img.website.com'343 }344 345 This is often useful when configuration options map directly to346 keyword arguments in functions or class constructors.347 348 :param namespace: a configuration namespace349 :param lowercase: a flag indicating if the keys of the resulting350 dictionary should be lowercase351 :param trim_namespace: a flag indicating if the keys of the resulting352 dictionary should not include the namespace353 354 .. versionadded:: 0.11355 """356 rv = {}357 for k, v in self.items():358 if not k.startswith(namespace):359 continue360 if trim_namespace:361 key = k[len(namespace) :]362 else:363 key = k364 if lowercase:365 key = key.lower()366 rv[key] = v367 return rv368 369 def __repr__(self) -> str:370 return f"<{type(self).__name__} {dict.__repr__(self)}>"371 