Source code for pytest_helper.config_file_handler

# -*- coding: utf-8 -*-
"""
Description
-----------

This module contains all the code for searching for, caching, and implementing
config files.

..  Copyright (c) 2015 by Allen Barker.
    License: MIT, see LICENSE for more details.

.. default-role:: code

"""

# TODO: Where possible switch from the saved dict in this namespace to saving
# in the per-module info dict.

# TODO: Add tests of the config file stuff.

# TODO, maybe: Add an option to the pytest-helper.conf file which allows for an
# arbitrary directory to be added to the sys.path.  So, in test dir at root of
# repo you can just put in that file for the distribution directory and not
# have to use any other sys_paths command.  LATER when you want to use pip
# local install (when setup.py is set up and you are ready to package) you can
# just delete that line from the file.  BUT, the difficulty is that any
# relative files should be relative to that file itself, NOT the importing
# file... should be a fairly easy conversion barring the problems with changing
# the local python directory (needs to be done early, if possible).

from __future__ import print_function, division, absolute_import
import inspect
import sys
import os
import ast
try:
    from configparser import ConfigParser
except ImportError: # Must be Python 2; use old names.
    from ConfigParser import SafeConfigParser as ConfigParser

from pytest_helper.global_settings import (
        PytestHelperException,
        CONFIG_FILE_NAMES, # Filenames for config files, searched in order.
        FAIL_ON_MISSING_CONFIG, # Raise exception if config enabled but not found.
        CONFIG_SECTION_STRING, # Label for active section of the config file.
        NAME_OF_PYTEST_HELPER_PER_MODULE_INFO_DICT)

#
# Config file locating and reading functions.
#

[docs]def get_importing_module_filename(level=2): """Run this during the initialization of a module to return the absolute pathname of the module that it is being imported from.""" module_filename = inspect.getframeinfo( inspect.getouterframes(inspect.currentframe())[level][0])[0] return os.path.abspath(module_filename)
[docs]def get_config_file_pathname(calling_mod_dir): """Get the full pathname of the configuration file, returning `None` if nothing was found. Go up the directory tree starting at `calling_mod_dir`, taking the first file found with an allowed name. If no package are encountered by the top-level root directory, return `None`.""" stop_at_package_top = False # Whether to stop search after package top. dirname = calling_mod_dir # Start looking in calling module's dir. # Go up the directory tree. in_package = False while True: if os.path.exists(os.path.join(dirname, "__init__.py")): in_package = True for config_name in CONFIG_FILE_NAMES: config_path = os.path.join(dirname, config_name) if os.path.exists(config_path): return config_path dirname, name = os.path.split(dirname) # Go up one dir. if not name: # If no subdir name then we were at root dir. return None if stop_at_package_top and in_package and not os.path.exists( os.path.join(dirname, "__init__.py")): return None # Past the top of a package, nothing found.
[docs]def read_and_eval_config_file(filename): """Return a dict of dicts containing a dict of parameter arguments for each section of the config file, with the evaluated value.""" config = ConfigParser() try: config.read(filename) except SyntaxError: print("Error in reading the pytest-helper config file named '{0}'" .format(filename), file=sys.stderr) raise # Convert the ConfigParser format into a regular dict of dicts. config_dict = {s:dict(config.items(s)) for s in config.sections()} # Evaluate each string within each config file section. for section, subdict in config_dict.items(): for key, value in subdict.items(): try: config_dict[section][key] = ast.literal_eval(value) except (ValueError, SyntaxError): raise PytestHelperException("Error in evaluating the config" " file '{0}'. Error in section '{1}' on the key '{2}' with" " value '{3}'." .format(filename, section, key, value)) return config_dict
# Note the below cache precludes dynamically changing the config file, which # seems like a bad idea to allow anyway but might have uses. config_dict_cache = {} # Cache config dicts by their full filenames (save space and time).
[docs]def get_config(calling_mod, calling_mod_dir, disable=False): """Return the configuration corresponding to the module `calling_mod`. Return an empty dict if no config is found. Caches its values in the namespace of the modules (in the pytest-helper data dict). Also caches config files based on their pathnames. If nothing is found in the cache, it looks for the file and reads it in if possible. If `disable` is set for a module then `get_config` will always return an empty config dict for the given module and skip searching for the file.""" # TODO, maybe: Could speed up even more by using a cache on each pathname # up to the config file. A bit more space but faster. if not hasattr(calling_mod, NAME_OF_PYTEST_HELPER_PER_MODULE_INFO_DICT): setattr(calling_mod, NAME_OF_PYTEST_HELPER_PER_MODULE_INFO_DICT, {}) module_info_dict = getattr(calling_mod, NAME_OF_PYTEST_HELPER_PER_MODULE_INFO_DICT) if "config_data_dict" not in module_info_dict: # Search for a file if the key is not set. config_file_path = get_config_file_pathname(calling_mod_dir) if config_file_path in config_dict_cache: # Look in the config dict cache. config_data_dict = config_dict_cache[config_file_path] elif config_file_path: # Some path was set. config_data_dict = read_and_eval_config_file(config_file_path) config_dict_cache[config_file_path] = config_data_dict else: # Returned None from config_file_path. if FAIL_ON_MISSING_CONFIG: raise PytestHelperException("Config file specified but" " not found in the directory tree. At least an" " empty file must be present.") else: config_data_dict = {} # Assume it is empty if not found and no fail. module_info_dict["config_data_dict"] = config_data_dict else: config_data_dict = module_info_dict["config_data_dict"] if "config_disabled" not in config_data_dict: config_data_dict["config_disabled"] = False # Disable config files for the module if that flag is set. if disable: config_data_dict["config_disabled"] = True # If module not enabled (by current or prev call with enable=True) return empty. if config_data_dict["config_disabled"]: return {} return config_data_dict
[docs]def get_config_value(config_key, default, calling_mod, calling_mod_dir): """Return the config value from the config file corresponding to the key `config_key`. Return the value `default` if no config value is set. This is called in the main functions to get defaults.""" config_dict = get_config(calling_mod, calling_mod_dir) if CONFIG_SECTION_STRING in config_dict: config_dict = config_dict[CONFIG_SECTION_STRING] else: return default if config_key in config_dict: return config_dict[config_key] else: return default