text: """
  Environment management
  """
  from ploomber.env.envdict import EnvDict
  
  # TODO: add defaults functionality if defined in {module}/env.defaults.yaml
  
  
  class Env:
      """Return the current environment
  
      NOTE: this API is experimental and subject to change, it is recommmended
      to use @with_env and @load_env decorators instead
  
      Env provides a clean and consistent way of managing environment and
      configuration settings. Its simplest usage provides access to settings
      specified via an `env.yaml`.
  
      Settings managed by Env are intended to be runtime constant (they are NOT
      intended to be used as global variables). For example you might want
      to store database URIs. Storing sensitive information is discouraged as
      yaml files are plain text. Use `keyring` for that instead.
  
      All sections are optional, but if there is a path section, all values
      inside that section will be casted to pathlib.Path objects, expanduser()
      is applied so "~" can be used. Strings with a trailing "/" will be
      interpreted as directories and they will be created if they do not exist
  
      There are a few placeholders available:
      * {{user}} expands to the current user (by calling getpass.getuser())
      * {{version}} expands to module.__version__ if _module is defined
      * {{git}} expands to branch name if at the tip, otherwise to
      the current commit hash (_module has to be defined)
  
      Examples
      --------
      >>> from ploomber import Env
      >>> env = Env({'db': {'uri': 'my_uri'}, 'path': {'raw': '/path/to/raw'}})
      >>> env.db.uri
      'my_uri'
      >>> env.path.raw
      PosixPath('/path/to/raw')
  
      Notes
      -----
      Envs are intended to be short-lived, the recommended usage is to start and
      end them only during the execution of a function that builds a DAG by
      using the @with_env and @load_env decorators
      """
      __instance = None
  
      # just a variable to display in error messages so users know where
      # the env was initialized if they try to create a new one
  
      def __new__(cls, source=None):
          if cls.__instance is None:
              cls.__instance = super().__new__(cls)
              return cls.__instance
          else:
              raise RuntimeError('Cannot start environment, one has already '
                                 'started: {}'.format(repr(cls.__instance)))
  
      def __init__(self, source='env.yaml'):
          """Start the environment
  
          Parameters
          ----------
          source: dict, pathlib.Path, str, optional
              If dict, loads it directly, if pathlib.Path or path, reads the file
              (assumes yaml format).
  
          Raises
          ------
          FileNotFoundError
              If source is None and an environment file cannot be found
              automatically
          RuntimeError
              If one environment has already started
  
          Returns
          -------
          ploomber.Env
              An environment object
          """
          if not isinstance(source, EnvDict):
              # try to initialize an EnvDict to perform validation, if any
              # errors occur, discard object
              try:
                  source = EnvDict(source)
              except Exception:
                  Env.__instance = None
                  raise
  
          self._data = source
          self._fn_name = None
  
      @classmethod
      def _init_from_decorator(cls, source, fn_name):
          env = Env(source=source)
          env._fn_name = fn_name
          return env
  
      @classmethod
      def load(cls):
          if cls.__instance is None:
              raise RuntimeError('Env has not been set, run Env() before '
                                 'running Env.load()')
          return cls.__instance
  
      @classmethod
      def end(cls):
          """
          End environment. Usage is discouraged, a single environment is expected
          to exist during the entire Python process lifespan to avoid
          inconsistencies, use it only if you have a very strong reason to
          """
          cls.__instance = None
  
      def __str__(self):
          return str(self._data)
  
      def __repr__(self):
          s = 'Env({})'.format(str(self._data))
  
          if self._fn_name:
              s += ' (initialized in function: %s)' % self._fn_name
  
          if self._data.path_to_env:
              s += ' (from file: %s)' % str(self._data.path_to_env)
  
          return s
  
      def __dir__(self):
          return dir(self._data)
  
      @property
      def name(self):
          return self._data.name
  
      def __getattr__(self, key):
          return getattr(self._data, key)
  
      def __getitem__(self, key):
          return self._data[key]
  
      def __setattr__(self, name, value):
          if name.startswith('_'):
              super().__setattr__(name, value)
          else:
              raise RuntimeError('env is a read-only object')
  
      def __enter__(self):
          return self
  
      def __exit__(self, exc_type, exc_value, traceback):
          Env.end()
  