1 from __future__ import annotations 2 3 import errno 4 import json 5 import os 6 import types 7 import typing as t 8 9 from werkzeug.utils import import_string 10 11 if t.TYPE_CHECKING: 12 import typing_extensions as te 13 14 from .sansio.app import App 15 16 17 T = t.TypeVar("T") 18 19 20 class 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 = None 25 ) -> None: 26 self.__name__ = name 27 self.get_converter = get_converter 28 29 @t.overload 30 def __get__(self, obj: None, owner: None) -> te.Self: ... 31 32 @t.overload 33 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 self 38 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__] = value 48 49 50 class Config(dict): # type: ignore[type-arg] 51 """Works exactly like a dict but provides ways to fill it from files 52 or special dictionaries. There are two common patterns to populate the 53 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 the 60 module that calls :meth:`from_object` or provide an import path to 61 a module that should be loaded. It is also possible to tell it to 62 use the same module and with that provide the configuration values 63 just before the call:: 64 65 DEBUG = True 66 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 use 71 lowercase values in the config file for temporary values that are not added 72 to the config or to define the config keys in the same file that implements 73 the application. 74 75 Probably the most interesting way to load configurations is from an 76 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 this 81 environment variable to the file you want to use. On Linux and OS X 82 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 the 89 config object is created by the application, this is 90 the application's :attr:`~flask.Flask.root_path`. 91 :param defaults: an optional dictionary of default values 92 """ 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_path 101 102 def from_envvar(self, variable_name: str, silent: bool = False) -> bool: 103 """Loads a configuration from an environment variable pointing to 104 a configuration file. This is basically just a shortcut with nicer 105 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 variable 110 :param silent: set to ``True`` if you want silent failure for missing 111 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 False 118 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.loads 128 ) -> bool: 129 """Load any environment variables that start with ``FLASK_``, 130 dropping the prefix from the env key for the config key. Values 131 are passed through a loading function to attempt to convert them 132 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 any 137 valid JSON type, including dicts and lists. 138 139 Specific items in nested dicts can be set by separating the 140 keys with double underscores (``__``). If an intermediate key 141 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 use 146 the returned value as the config value. If any error is 147 raised it is ignored and the value remains a string. The 148 default is :func:`json.loads`. 149 150 .. versionadded:: 2.1 151 """ 152 prefix = f"{prefix}_" 153 154 for key in sorted(os.environ): 155 if not key.startswith(prefix): 156 continue 157 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 pass 166 167 if "__" not in key: 168 # A non-nested key, set directly. 169 self[key] = value 170 continue 171 172 # Traverse nested dictionaries with keys separated by "__". 173 current = self 174 *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] = value 184 185 return True 186 187 def from_pyfile( 188 self, filename: str | os.PathLike[str], silent: bool = False 189 ) -> bool: 190 """Updates the values in the config from a Python file. This function 191 behaves as if the file was imported as module with the 192 :meth:`from_object` function. 193 194 :param filename: the filename of the config. This can either be an 195 absolute filename or a filename relative to the 196 root path. 197 :param silent: set to ``True`` if you want silent failure for missing 198 files. 199 :return: ``True`` if the file was loaded successfully. 200 201 .. versionadded:: 0.7 202 `silent` parameter. 203 """ 204 filename = os.path.join(self.root_path, filename) 205 d = types.ModuleType("config") 206 d.__file__ = filename 207 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 False 213 e.strerror = f"Unable to load configuration file ({e.strerror})" 214 raise 215 self.from_object(d) 216 return True 217 218 def from_object(self, obj: object | str) -> None: 219 """Updates the values from the given object. An object can be of one 220 of the following two types: 221 222 - a string: in this case the object with that name will be imported 223 - an actual object reference: that object is used directly 224 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 a 228 ``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_config 234 app.config.from_object(default_config) 235 236 Nothing is done to the object before loading. If the object is a 237 class and has ``@property`` attributes, it needs to be 238 instantiated before being passed to this method. 239 240 You should not use this function to load the actual configuration but 241 rather configuration defaults. The actual config should be loaded 242 with :meth:`from_pyfile` and ideally from a location not within the 243 package because the package might be installed system wide. 244 245 See :ref:`config-dev-prod` for an example of class-based configuration 246 using :meth:`from_object`. 247 248 :param obj: an import name or object 249 """ 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 loaded 264 using the ``load`` parameter. The loaded data is passed to the 265 :meth:`from_mapping` method. 266 267 .. code-block:: python 268 269 import json 270 app.config.from_file("config.json", load=json.load) 271 272 import tomllib 273 app.config.from_file("config.toml", load=tomllib.load, text=False) 274 275 :param filename: The path to the data file. This can be an 276 absolute path or relative to the config root path. 277 :param load: A callable that takes a file handle and returns a 278 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.3 286 The ``text`` parameter was added. 287 288 .. versionadded:: 2.0 289 """ 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 False 298 299 e.strerror = f"Unable to load configuration file ({e.strerror})" 300 raise 301 302 return self.from_mapping(obj) 303 304 def from_mapping( 305 self, mapping: t.Mapping[str, t.Any] | None = None, **kwargs: t.Any 306 ) -> bool: 307 """Updates the config like :meth:`update` ignoring items with 308 non-upper keys. 309 310 :return: Always returns ``True``. 311 312 .. versionadded:: 0.11 313 """ 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] = value 321 return True 322 323 def get_namespace( 324 self, namespace: str, lowercase: bool = True, trim_namespace: bool = True 325 ) -> dict[str, t.Any]: 326 """Returns a dictionary containing a subset of configuration options 327 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 to 343 keyword arguments in functions or class constructors. 344 345 :param namespace: a configuration namespace 346 :param lowercase: a flag indicating if the keys of the resulting 347 dictionary should be lowercase 348 :param trim_namespace: a flag indicating if the keys of the resulting 349 dictionary should not include the namespace 350 351 .. versionadded:: 0.11 352 """ 353 rv = {} 354 for k, v in self.items(): 355 if not k.startswith(namespace): 356 continue 357 if trim_namespace: 358 key = k[len(namespace) :] 359 else: 360 key = k 361 if lowercase: 362 key = key.lower() 363 rv[key] = v 364 return rv 365 366 def __repr__(self) -> str: 367 return f"<{type(self).__name__} {dict.__repr__(self)}>" 368