Format: text/x-python
⬇ Download File (12.9 KB)
   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