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 154 for key in sorted(os.environ):155 if not key.startswith(prefix):156 continue157 158 value = os.environ[key]159 key = key.removeprefix(prefix)160 161 try:162 value = loads(value)163 except Exception:164 # Keep the value as a string if loading failed.165 pass166 167 if "__" not in key:168 # A non-nested key, set directly.169 self[key] = value170 continue171 172 # Traverse nested dictionaries with keys separated by "__".173 current = self174 *parts, tail = key.split("__")175 176 for part in parts:177 # If an intermediate dict does not exist, create it.178 if part not in current:179 current[part] = {}180 181 current = current[part]182 183 current[tail] = value184 185 return True186 187 def from_pyfile(188 self, filename: str | os.PathLike[str], silent: bool = False189 ) -> bool:190 """Updates the values in the config from a Python file. This function191 behaves as if the file was imported as module with the192 :meth:`from_object` function.193 194 :param filename: the filename of the config. This can either be an195 absolute filename or a filename relative to the196 root path.197 :param silent: set to ``True`` if you want silent failure for missing198 files.199 :return: ``True`` if the file was loaded successfully.200 201 .. versionadded:: 0.7202 `silent` parameter.203 """204 filename = os.path.join(self.root_path, filename)205 d = types.ModuleType("config")206 d.__file__ = filename207 try:208 with open(filename, mode="rb") as config_file:209 exec(compile(config_file.read(), filename, "exec"), d.__dict__)210 except OSError as e:211 if silent and e.errno in (errno.ENOENT, errno.EISDIR, errno.ENOTDIR):212 return False213 e.strerror = f"Unable to load configuration file ({e.strerror})"214 raise215 self.from_object(d)216 return True217 218 def from_object(self, obj: object | str) -> None:219 """Updates the values from the given object. An object can be of one220 of the following two types:221 222 - a string: in this case the object with that name will be imported223 - an actual object reference: that object is used directly224 225 Objects are usually either modules or classes. :meth:`from_object`226 loads only the uppercase attributes of the module/class. A ``dict``227 object will not work with :meth:`from_object` because the keys of a228 ``dict`` are not attributes of the ``dict`` class.229 230 Example of module-based configuration::231 232 app.config.from_object('yourapplication.default_config')233 from yourapplication import default_config234 app.config.from_object(default_config)235 236 Nothing is done to the object before loading. If the object is a237 class and has ``@property`` attributes, it needs to be238 instantiated before being passed to this method.239 240 You should not use this function to load the actual configuration but241 rather configuration defaults. The actual config should be loaded242 with :meth:`from_pyfile` and ideally from a location not within the243 package because the package might be installed system wide.244 245 See :ref:`config-dev-prod` for an example of class-based configuration246 using :meth:`from_object`.247 248 :param obj: an import name or object249 """250 if isinstance(obj, str):251 obj = import_string(obj)252 for key in dir(obj):253 if key.isupper():254 self[key] = getattr(obj, key)255 256 def from_file(257 self,258 filename: str | os.PathLike[str],259 load: t.Callable[[t.IO[t.Any]], t.Mapping[str, t.Any]],260 silent: bool = False,261 text: bool = True,262 ) -> bool:263 """Update the values in the config from a file that is loaded264 using the ``load`` parameter. The loaded data is passed to the265 :meth:`from_mapping` method.266 267 .. code-block:: python268 269 import json270 app.config.from_file("config.json", load=json.load)271 272 import tomllib273 app.config.from_file("config.toml", load=tomllib.load, text=False)274 275 :param filename: The path to the data file. This can be an276 absolute path or relative to the config root path.277 :param load: A callable that takes a file handle and returns a278 mapping of loaded data from the file.279 :type load: ``Callable[[Reader], Mapping]`` where ``Reader``280 implements a ``read`` method.281 :param silent: Ignore the file if it doesn't exist.282 :param text: Open the file in text or binary mode.283 :return: ``True`` if the file was loaded successfully.284 285 .. versionchanged:: 2.3286 The ``text`` parameter was added.287 288 .. versionadded:: 2.0289 """290 filename = os.path.join(self.root_path, filename)291 292 try:293 with open(filename, "r" if text else "rb") as f:294 obj = load(f)295 except OSError as e:296 if silent and e.errno in (errno.ENOENT, errno.EISDIR):297 return False298 299 e.strerror = f"Unable to load configuration file ({e.strerror})"300 raise301 302 return self.from_mapping(obj)303 304 def from_mapping(305 self, mapping: t.Mapping[str, t.Any] | None = None, **kwargs: t.Any306 ) -> bool:307 """Updates the config like :meth:`update` ignoring items with308 non-upper keys.309 310 :return: Always returns ``True``.311 312 .. versionadded:: 0.11313 """314 mappings: dict[str, t.Any] = {}315 if mapping is not None:316 mappings.update(mapping)317 mappings.update(kwargs)318 for key, value in mappings.items():319 if key.isupper():320 self[key] = value321 return True322 323 def get_namespace(324 self, namespace: str, lowercase: bool = True, trim_namespace: bool = True325 ) -> dict[str, t.Any]:326 """Returns a dictionary containing a subset of configuration options327 that match the specified namespace/prefix. Example usage::328 329 app.config['IMAGE_STORE_TYPE'] = 'fs'330 app.config['IMAGE_STORE_PATH'] = '/var/app/images'331 app.config['IMAGE_STORE_BASE_URL'] = 'http://img.website.com'332 image_store_config = app.config.get_namespace('IMAGE_STORE_')333 334 The resulting dictionary `image_store_config` would look like::335 336 {337 'type': 'fs',338 'path': '/var/app/images',339 'base_url': 'http://img.website.com'340 }341 342 This is often useful when configuration options map directly to343 keyword arguments in functions or class constructors.344 345 :param namespace: a configuration namespace346 :param lowercase: a flag indicating if the keys of the resulting347 dictionary should be lowercase348 :param trim_namespace: a flag indicating if the keys of the resulting349 dictionary should not include the namespace350 351 .. versionadded:: 0.11352 """353 rv = {}354 for k, v in self.items():355 if not k.startswith(namespace):356 continue357 if trim_namespace:358 key = k[len(namespace) :]359 else:360 key = k361 if lowercase:362 key = key.lower()363 rv[key] = v364 return rv365 366 def __repr__(self) -> str:367 return f"<{type(self).__name__} {dict.__repr__(self)}>"368 