diff options
Diffstat (limited to 'rpmlint/config.py')
-rw-r--r-- | rpmlint/config.py | 183 |
1 files changed, 183 insertions, 0 deletions
diff --git a/rpmlint/config.py b/rpmlint/config.py new file mode 100644 index 0000000..136e5a0 --- /dev/null +++ b/rpmlint/config.py @@ -0,0 +1,183 @@ +import os +from pathlib import Path +import re +import sys + +from rpmlint.helpers import print_warning +try: + import tomllib +except ImportError: + import tomli as tomllib +import tomli_w +from xdg.BaseDirectory import xdg_config_dirs + + +class Config: + """ + Load and parse rpmlint configuration. + + Configuration files are written in toml and should be placed in one of + the XDG_CONFIG_DIRS directory or passed as "config" argument directly. + + By default it loads configdefaults.toml and all default locations and + initializes basic testing layout for the rpmlint binary. Based on the + opening order 'newer' configuration takes precedence over already + existing one. + """ + + re_filter = re.compile(r'^\s*addFilter\s*\(\s*r?[\"\'](.*)[\"\']\s*\)') + re_badness = re.compile(r'\s*setBadness\s*\([\'\"](.*)[\'\"],\s*[\'\"]?(\d+)[\'\"]?\)') + config_defaults = Path(__file__).parent / 'configdefaults.toml' + + def __init__(self, config=None): + """ + Initialize basic options and load rpmlint configuration. + + Args: + config: A list of paths of configuration file(s) passed by user in + command line. + """ + # ordered list of configuration files we loaded + # useful when debugging where from we got all the config options + self.conf_files = [] + # Configuration content parsed from the toml configuration file + self.configuration = None + # List of rpmlintrc filters + self.rpmlintrc_filters = [] + # whether to print more information or not + self.info = False + # whether to treat all messages as errors or not + self.strict = False + # whether to treat individual errors as non-fatal + self.permissive = False + + # find configuration files and load them + self.find_configs(config) + self.load_config() + # if loading of the configuration failed -> fall back only to defaults + if not self.configuration: + # reset the configs only to defaults + self.conf_files = [self.config_defaults] + self.load_config() + + def find_configs(self, config=None): + """ + Find and store paths to all config files. + + It searches for default configuration, files in XDG_CONFIG_DIRS and + user defined configuration (argument "config"). All configuration + file paths found are then stored in self.conf_files variable. + XDG_CONFIG_DIRS contains preference-ordered set of base directories + to search for configuration files. Users can override it by their + own configuration file (config parameter) and then that is + added too. + """ + # first load up the file that contains defaults + self.conf_files.append(self.config_defaults) + + # Skip auto-loading when running under PYTEST + if not os.environ.get('PYTEST_XDIST_TESTRUNUID') and not os.environ.get('CONFIG_DISABLE_AUTOLOADING'): + # Then load up config directories on system + for directory in reversed(xdg_config_dirs): + confdir = Path(directory) / 'rpmlint' + if confdir.is_dir(): + # load all configs in the folders + confopts = sorted(confdir.glob('*toml')) + self.conf_files += confopts + + # As a last item load up the user configuration + if config: + for path in config: + if path.exists(): + # load this only if it really exist + self.conf_files.append(path) + else: + print_warning(f'(none): W: error locating user requested configuration: {path}') + + def _merge_dictionaries(self, dest, source, override): + """ + Merge in place dest dictionary for values in source in recursive way. + If override is set to True, override instead of merging. + """ + for k, v in source.items(): + vdest = dest.get(k) + if isinstance(vdest, dict) and isinstance(v, dict): + self._merge_dictionaries(vdest, v, override) + else: + if isinstance(vdest, list) and not override: + for item in v: + if item not in vdest: + vdest.append(item) + else: + dest[k] = v + + def _is_override_config(self, config_file): + return '.override.' in config_file.name + + def _sort_config_files(self, config_file): + """ + Sort config files in the following order: + configdefaults.toml, normal configs, *.override.* configs + """ + if config_file == self.config_defaults: + return 0 + elif not self._is_override_config(config_file): + return 1 + else: + return 2 + + def load_config(self, config=None): + """ + Load the configuration files and append it to local dictionary. + + It's stored in self.configuration with the content of already loaded + options. + """ + if config: + # just add the new config at the end of the list, someone injected + # config file to us + for path in config: + if path not in self.conf_files and path.exists(): + self.conf_files.append(path) + + cfg = {} + # sort self.conf_files as we print list of loaded configuration files + self.conf_files = sorted(self.conf_files, key=self._sort_config_files) + for cf in self.conf_files: + try: + with open(cf, 'rb') as f: + toml_config = tomllib.load(f) + self._merge_dictionaries(cfg, toml_config, self._is_override_config(cf)) + except tomllib.TOMLDecodeError as terr: + print_warning(f'(none): E: fatal error while parsing configuration file {cf}: {terr}') + sys.exit(4) + self.configuration = cfg + + def load_rpmlintrc(self, rpmlintrc_file): + """ + Load existing rpmlintrc files. + + Only setBadness and addFilter are processed. + """ + + rpmlintrc_lines = rpmlintrc_file.read_text().splitlines() + filters = [] + for line in rpmlintrc_lines: + m = self.re_filter.match(line) + if m: + filters.append(m.group(1)) + m = self.re_badness.match(line) + if m: + self.configuration['Scoring'].update({m.group(1): m.group(2)}) + + self.configuration['Filters'] += filters + self.rpmlintrc_filters = filters + + def print_config(self): + """Print the current state of the configuration.""" + if self.configuration: + print(tomli_w.dumps(self.configuration)) + + def set_badness(self, result, badness): + """Set specific badness for some result.""" + self.configuration['Scoring'][result] = badness |