Source code for coalib.settings.ConfigurationGathering

import os
import sys
import logging

from coalib.collecting.Collectors import (
    collect_all_bears_from_sections, filter_section_bears_by_languages)
from coalib.bearlib.languages.Language import Language, UnknownLanguageError
from coalib.misc import Constants
from coalib.output.ConfWriter import ConfWriter
from coalib.output.printers.LOG_LEVEL import LOG_LEVEL
from coalib.parsing.CliParsing import parse_cli, check_conflicts
from coalib.parsing.ConfParser import ConfParser
from coalib.parsing.DefaultArgParser import PathArg
from coalib.settings.Section import Section, extract_aspects_from_section
from coalib.settings.SectionFilling import fill_settings
from coalib.settings.Setting import Setting, path
from string import Template

COAFILE_OUTPUT = Template('$type \'$file\' $found!\n'
                          'Here\'s what you can do:\n'
                          '* add `--save` to generate a config file with '
                          'your current options\n'
                          '* add `-I` to suppress any use of config files\n')


[docs]def aspectize_sections(sections): """ Search for aspects related setting in a section, initialize it, and then embed the aspects information as AspectList object into the section itself. :param sections: List of section that potentially contain aspects setting. :return: The new sections. """ for _, section in sections.items(): if validate_aspect_config(section): section.aspects = extract_aspects_from_section(section) else: section.aspects = None return sections
[docs]def validate_aspect_config(section): """ Validate if a section contain required setting to run in aspects mode. :param section: The section that potentially contain aspect setting. :return: The validity of section. """ aspects = section.get('aspects') if not len(aspects): return False if not section.language: logging.warning('Setting `language` is not found in section `{}`. ' 'Usage of aspect-based setting must include ' 'language information.'.format(section.name)) return False if len(section.get('bears')): logging.warning('`aspects` and `bears` setting is detected ' 'in section `{}`. Aspect-based configuration will ' 'takes priority and will overwrite any ' 'explicitly listed bears.'.format(section.name)) return True
def _set_section_language(sections): """ Validate ``language`` setting and inject them to section if valid. :param sections: List of sections that potentially contain ``language``. """ for section_name, section in sections.items(): section_language = section.get('language') if not len(section_language): continue try: section.language = Language[section_language] except UnknownLanguageError as exc: logging.warning('Section `{}` contain invalid language setting: ' '{}'.format(section_name, exc))
[docs]def merge_section_dicts(lower, higher): """ Merges the section dictionaries. The values of higher will take precedence over the ones of lower. Lower will hold the modified dict in the end. :param lower: A section. :param higher: A section which values will take precedence over the ones from the other. :return: The merged dict. """ for name in higher: if name in lower: lower[name].update(higher[name], ignore_defaults=True) else: # no deep copy needed lower[name] = higher[name] return lower
[docs]def load_config_file(filename, log_printer=None, silent=False): """ Loads sections from a config file. Prints an appropriate warning if it doesn't exist and returns a section dict containing an empty default section in that case. It assumes that the cli_sections are available. :param filename: The file to load settings from. :param log_printer: The log printer to log the warning/error to (in case). :param silent: Whether or not to warn the user/exit if the file doesn't exist. :raises SystemExit: Exits when the given filename is invalid and is not the default coafile. Only raised when ``silent`` is ``False``. """ filename = os.path.abspath(filename) try: return ConfParser().parse(filename) except FileNotFoundError: if not silent: if os.path.basename(filename) == Constants.default_coafile: logging.warning(COAFILE_OUTPUT .substitute(type='Default coafile', file=Constants.default_coafile, found='not found')) else: logging.error(COAFILE_OUTPUT .substitute(type='Requested coafile', file=filename, found='does not exist')) sys.exit(2) return {'default': Section('default')}
[docs]def save_sections(sections): """ Saves the given sections if they are to be saved. :param sections: A section dict. """ default_section = sections['cli'] try: if bool(default_section.get('save', 'false')): conf_writer = ConfWriter( str(default_section.get('config', Constants.default_coafile))) else: return except ValueError: conf_writer = ConfWriter(str(default_section.get('save', '.coafile'))) conf_writer.write_sections(sections) conf_writer.close()
[docs]def warn_nonexistent_targets(targets, sections, log_printer=None): """ Prints out a warning on the given log printer for all targets that are not existent within the given sections. :param targets: The targets to check. :param sections: The sections to search. (Dict.) :param log_printer: The log printer to warn to. """ for target in targets: if target not in sections: logging.warning( "The requested section '{section}' is not existent. " 'Thus it cannot be executed.'.format(section=target)) # Can't be summarized as python will evaluate conditions lazily, those # functions have intended side effects though. files_config_absent = warn_config_absent(sections, 'files') bears_config_absent = warn_config_absent(sections, ['bears', 'aspects']) if files_config_absent or bears_config_absent: raise SystemExit(2) # Invalid CLI options provided
[docs]def warn_config_absent(sections, argument, log_printer=None): """ Checks if at least 1 of the given arguments is present somewhere in the sections and emits a warning that code analysis can not be run without it. :param sections: A dictionary of sections. :param argument: An argument OR a list of arguments that at least 1 should present. :param log_printer: A log printer to emit the warning to. :return: Returns a boolean False if the given argument is present in the sections, else returns True. """ if isinstance(argument, str): argument = [argument] for section in sections.values(): if any(arg in section for arg in argument): return False formatted_args = ' or '.join('`--{}`'.format(arg) for arg in argument) logging.warning('coala will not run any analysis. Did you forget ' 'to give the {} argument?'.format(formatted_args)) return True
[docs]def load_configuration(arg_list, log_printer=None, arg_parser=None, args=None, silent=False): """ Parses the CLI args and loads the config file accordingly, taking default_coafile and the users .coarc into account. :param arg_list: The list of CLI arguments. :param log_printer: The LogPrinter object for logging. :param arg_parser: An ``argparse.ArgumentParser`` instance used for parsing the CLI arguments. :param args: Alternative pre-parsed CLI arguments. :param silent: Whether or not to display warnings, ignored if ``save`` is enabled. :return: A tuple holding (log_printer: LogPrinter, sections: dict(str, Section), targets: list(str)). (Types indicated after colon.) """ cli_sections = parse_cli(arg_list=arg_list, arg_parser=arg_parser, args=args) check_conflicts(cli_sections) if ( bool(cli_sections['cli'].get('find_config', 'False')) and str(cli_sections['cli'].get('config')) == ''): cli_sections['cli'].add_or_create_setting( Setting('config', PathArg(find_user_config(os.getcwd())))) # We don't want to store targets argument back to file, thus remove it targets = [item.lower() for item in list( cli_sections['cli'].contents.pop('targets', ''))] if bool(cli_sections['cli'].get('no_config', 'False')): sections = cli_sections else: base_sections = load_config_file(Constants.system_coafile, silent=silent) user_sections = load_config_file( Constants.user_coafile, silent=True) default_config = str(base_sections['default'].get('config', '.coafile')) user_config = str(user_sections['default'].get( 'config', default_config)) config = os.path.abspath( str(cli_sections['cli'].get('config', user_config))) try: save = bool(cli_sections['cli'].get('save', 'False')) except ValueError: # A file is deposited for the save parameter, means we want to save # but to a specific file. save = True coafile_sections = load_config_file(config, silent=save or silent) sections = merge_section_dicts(base_sections, user_sections) sections = merge_section_dicts(sections, coafile_sections) if 'cli' in sections: logging.warning('\'cli\' is an internally reserved section name. ' 'It may have been generated into your coafile ' 'while running coala with `--save`. The settings ' 'in that section will inherit implicitly to all ' 'sections as defaults just like CLI args do. ' 'Please change the name of that section in your ' 'coafile to avoid any unexpected behavior.') sections = merge_section_dicts(sections, cli_sections) for name, section in list(sections.items()): section.set_default_section(sections) if name == 'default': if section.contents: logging.warning('Implicit \'Default\' section inheritance is ' 'deprecated. It will be removed soon. To ' 'silence this warning remove settings in the ' '\'Default\' section from your coafile. You ' 'can use dots to specify inheritance: the ' 'section \'all.python\' will inherit all ' 'settings from \'all\'.') sections['default'].update(sections['cli']) sections['default'].name = 'cli' sections['cli'] = sections['default'] del sections['default'] str_log_level = str(sections['cli'].get('log_level', '')).upper() logging.getLogger().setLevel(LOG_LEVEL.str_dict.get(str_log_level, LOG_LEVEL.INFO)) return sections, targets
[docs]def find_user_config(file_path, max_trials=10): """ Uses the filepath to find the most suitable user config file for the file by going down one directory at a time and finding config files there. :param file_path: The path of the file whose user config needs to be found :param max_trials: The maximum number of directories to go down to. :return: The config file's path, empty string if none was found """ file_path = os.path.normpath(os.path.abspath(os.path.expanduser( file_path))) old_dir = None base_dir = (file_path if os.path.isdir(file_path) else os.path.dirname(file_path)) home_dir = os.path.expanduser('~') while base_dir != old_dir and old_dir != home_dir and max_trials != 0: config_file = os.path.join(base_dir, '.coafile') if os.path.isfile(config_file): return config_file old_dir = base_dir base_dir = os.path.dirname(old_dir) max_trials = max_trials - 1 return ''
[docs]def get_config_directory(section): """ Retrieves the configuration directory for the given section. Given an empty section: >>> section = Section("name") The configuration directory is not defined and will therefore fallback to the current directory: >>> get_config_directory(section) == os.path.abspath(".") True If the ``files`` setting is given with an originating coafile, the directory of the coafile will be assumed the configuration directory: >>> section.append(Setting("files", "**", origin="/tmp/.coafile")) >>> get_config_directory(section) == os.path.abspath('/tmp/') True However if its origin is already a directory this will be preserved: >>> files = section['files'] >>> files.origin = os.path.abspath('/tmp/dir/') >>> section.append(files) >>> os.makedirs(section['files'].origin, exist_ok=True) >>> get_config_directory(section) == section['files'].origin True The user can manually set a project directory with the ``project_dir`` setting: >>> section.append(Setting('project_dir', os.path.abspath('/tmp'), '/')) >>> get_config_directory(section) == os.path.abspath('/tmp') True If no section is given, the current directory is returned: >>> get_config_directory(None) == os.path.abspath(".") True To summarize, the config directory will be chosen by the following priorities if possible in that order: - the ``project_dir`` setting - the origin of the ``files`` setting, if it's a directory - the directory of the origin of the ``files`` setting - the current directory :param section: The section to inspect. :return: The directory where the project is lying. """ if section is None: return os.getcwd() if 'project_dir' in section: return path(section.get('project_dir')) config = os.path.abspath(section.get('files', '').origin) return config if os.path.isdir(config) else os.path.dirname(config)
[docs]def get_all_bears(log_printer=None, arg_parser=None, silent=True, bear_globs=('**',)): """ :param log_printer: The log_printer to handle logging. :param arg_parser: An ``ArgParser`` object. :param silent: Whether or not to display warnings. :param bear_globs: List of glob patterns. :return: Tuple containing dictionaries of local bears and global bears. """ sections, _ = load_configuration(arg_list=None, arg_parser=arg_parser, silent=silent) local_bears, global_bears = collect_all_bears_from_sections( sections, bear_globs=bear_globs) return local_bears, global_bears
[docs]def get_filtered_bears(languages, log_printer=None, arg_parser=None, silent=True): """ :param languages: List of languages. :param log_printer: The log_printer to handle logging. :param arg_parser: An ``ArgParser`` object. :param silent: Whether or not to display warnings. :return: Tuple containing dictionaries of local bears and global bears. """ local_bears, global_bears = get_all_bears(arg_parser=arg_parser, silent=silent) if languages: local_bears = filter_section_bears_by_languages( local_bears, languages) global_bears = filter_section_bears_by_languages( global_bears, languages) return local_bears, global_bears
[docs]def gather_configuration(acquire_settings, log_printer=None, arg_list=None, arg_parser=None, args=None): """ Loads all configuration files, retrieves bears and all needed settings, saves back if needed and warns about non-existent targets. This function: - Reads and merges all settings in sections from - Default config - User config - Configuration file - CLI - Collects all the bears - Fills up all needed settings - Writes back the new sections to the configuration file if needed - Gives all information back to caller :param acquire_settings: The method to use for requesting settings. It will get a parameter which is a dictionary with the settings name as key and a list containing a description in [0] and the names of the bears who need this setting in all following indexes. :param log_printer: The log printer to use for logging. The log level will be adjusted to the one given by the section. :param arg_list: CLI args to use :param arg_parser: Instance of ArgParser that is used to parse none-setting arguments. :param args: Alternative pre-parsed CLI arguments. :return: A tuple with the following contents: - A dictionary with the sections - Dictionary of list of local bears for each section - Dictionary of list of global bears for each section - The targets list """ if args is None: # Note: arg_list can also be []. Hence we cannot use # `arg_list = arg_list or default_list` arg_list = sys.argv[1:] if arg_list is None else arg_list sections, targets = load_configuration(arg_list, arg_parser=arg_parser, args=args) _set_section_language(sections) aspectize_sections(sections) local_bears, global_bears = fill_settings(sections, targets, acquire_settings) save_sections(sections) warn_nonexistent_targets(targets, sections) return (sections, local_bears, global_bears, targets)