Source code for bob.extension.config

#!/usr/bin/env python
# vim: set fileencoding=utf-8 :

'''Functionality to implement python-based config file parsing and loading.
'''

import imp
import pkg_resources
import pkgutil
from os.path import isfile
import logging

logger = logging.getLogger(__name__)

LOADED_CONFIGS = []


def _load_context(path, mod):
  '''Loads the Python file as module, returns a resolved context

  This function is implemented in a way that is both Python 2 and Python 3
  compatible. It does not directly load the python file, but reads its contents
  in memory before Python-compiling it. It leaves no traces on the file system.

  Parameters
  ----------
  path : str
      The full path of the Python file to load the module contents
      from
  mod : module
      A preloaded module to use as context for the next module
      loading. You can create a new module using :py:mod:`imp` as in ``m =
      imp.new_module('name'); m.__dict__.update(ctxt)`` where ``ctxt`` is a
      python dictionary with string -> object values representing the contents
      of the module to be created.

  Returns
  -------
  mod : :any:`module`
      A python module with the fully resolved context
  '''

  # executes the module code on the context of previously imported modules
  exec(compile(open(path, "rb").read(), path, 'exec'), mod.__dict__)

  return mod


def _get_module_filename(module_name):
  """Resolves a module name to an actual Python file.

  Parameters
  ----------
  module_name : str
      The name of the module

  Returns
  -------
  str
      The Python files that corresponds to the module name.
  """
  loader = pkgutil.get_loader(module_name)
  if loader is None:
    return ''
  try:
    return loader.path
  except AttributeError:
    return loader.filename


def _resolve_entry_point_or_modules(paths, entry_point_group):
  """Resolves a mixture of paths, entry point names, and module names to just
  paths. For example paths can be:
  ``paths = ['/tmp/config.py', 'config1', 'bob.extension.config2']``.

  Parameters
  ----------
  paths : [str]
      An iterable strings that either point to actual files, are entry point
      names, or are module names.
  entry_point_group : str
      The entry point group name to search in entry points.

  Raises
  ------
  ValueError
      If one of the paths cannot be resolved to an actual path to a file.

  Returns
  -------
  paths : [str]
      The resolved paths pointing to existing files.
  names : [str]
      The valid python module names to bind each of the files to

  """

  entries = {e.name: e for e in
             pkg_resources.iter_entry_points(entry_point_group)}
  files = []
  names = []

  for i, path in enumerate(paths):

    old_path = path
    module_name = 'user_config'  # fixed module name for files with full paths

    # if it already points to a file
    if isfile(path):
      pass

    # If it is an entry point name, collect path and module name
    elif path in entries:
      module_name = entries[path].module_name
      path = _get_module_filename(module_name)
      if not isfile(path):
        raise ValueError(
            "The specified entry point: `{}' pointing to module: `{}' and "
            "resolved to: `{}' does not point to an existing "
            "file.".format(old_path, module_name, path))

    # If it is not a path nor an entry point name, it is a module name then?
    else:
      # if we have gotten here so far then path is the module_name.
      module_name = path
      path = _get_module_filename(path)
      if not isfile(path):
        raise ValueError(
            "The specified path: `{}' resolved to: `{}' is not a file, not a "
            "entry point name of `{}', nor a module name".format(
                old_path, path, entry_point_group or ''))

    files.append(path)
    names.append(module_name)

  return files, names


[docs]def load(paths, context=None, entry_point_group=None): '''Loads a set of configuration files, in sequence This method will load one or more configuration files. Every time a configuration file is loaded, the context (variables) loaded from the previous file is made available, so the new configuration file can override or modify this context. Parameters ---------- paths : [str] A list or iterable containing paths (relative or absolute) of configuration files that need to be loaded in sequence. Each configuration file is loaded by creating/modifying the context generated after each file readout. context : :py:class:`dict`, optional If provided, start the readout of the first configuration file with the given context. Otherwise, create a new internal context. entry_point_group : :py:class:`str`, optional If provided, it will treat non-existing file paths as entry point names under the ``entry_point_group`` name. Returns ------- mod : :any:`module` A module representing the resolved context, after loading the provided modules and resolving all variables. ''' # resolve entry points to paths if entry_point_group is not None: paths, names = _resolve_entry_point_or_modules(paths, entry_point_group) else: names = len(paths) * ['user_config'] ctxt = imp.new_module('initial_context') if context is not None: ctxt.__dict__.update(context) # Small gambiarra (https://www.urbandictionary.com/define.php?term=Gambiarra) # to avoid the garbage collector to collect some already imported modules. LOADED_CONFIGS.append(ctxt) # if no paths are provided, return context if not paths: return ctxt for k, n in zip(paths, names): logger.debug("Loading configuration file `%s'...", k) mod = imp.new_module(n) # remove the keys that might break the loading of the next config file. ctxt.__dict__.pop('__name__', None) ctxt.__dict__.pop('__package__', None) mod.__dict__.update(ctxt.__dict__) LOADED_CONFIGS.append(mod) ctxt = _load_context(k, mod) return mod
[docs]def mod_to_context(mod): """Converts the loaded module of :any:`load` to a dictionary context. This function removes all the variables that start and end with ``__``. Parameters ---------- mod : object What is returned by :any:`load` Returns ------- dict The context that was in ``mod``. """ return {k: v for k, v in mod.__dict__.items() if not (k.startswith('__') and k.endswith('__'))}