diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2021-12-04 03:31:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2021-12-04 03:31:41 +0000 |
commit | 72b8c35be4293bd21de123854491c658c53af100 (patch) | |
tree | 735464cc081879561927a37650b1102beaa1f4f9 /gitlint/config.py | |
parent | Adding upstream version 0.16.0. (diff) | |
download | gitlint-72b8c35be4293bd21de123854491c658c53af100.tar.xz gitlint-72b8c35be4293bd21de123854491c658c53af100.zip |
Adding upstream version 0.17.0.upstream/0.17.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'gitlint/config.py')
-rw-r--r-- | gitlint/config.py | 528 |
1 files changed, 0 insertions, 528 deletions
diff --git a/gitlint/config.py b/gitlint/config.py deleted file mode 100644 index 6d2ead2..0000000 --- a/gitlint/config.py +++ /dev/null @@ -1,528 +0,0 @@ -from configparser import ConfigParser, Error as ConfigParserError - -import copy -import io -import re -import os -import shutil - -from collections import OrderedDict -from gitlint.utils import DEFAULT_ENCODING -from gitlint import rules # For some weird reason pylint complains about this, pylint: disable=unused-import -from gitlint import options -from gitlint import rule_finder -from gitlint.contrib import rules as contrib_rules -from gitlint.exception import GitlintError - - -def handle_option_error(func): - """ Decorator that calls given method/function and handles any RuleOptionError gracefully by converting it to a - LintConfigError. """ - - def wrapped(*args): - try: - return func(*args) - except options.RuleOptionError as e: - raise LintConfigError(str(e)) from e - - return wrapped - - -class LintConfigError(GitlintError): - pass - - -class LintConfig: - """ Class representing gitlint configuration. - Contains active config as well as number of methods to easily get/set the config. - """ - - # Default tuple of rule classes (tuple because immutable). - default_rule_classes = (rules.IgnoreByTitle, - rules.IgnoreByBody, - rules.IgnoreBodyLines, - rules.IgnoreByAuthorName, - rules.TitleMaxLength, - rules.TitleTrailingWhitespace, - rules.TitleLeadingWhitespace, - rules.TitleTrailingPunctuation, - rules.TitleHardTab, - rules.TitleMustNotContainWord, - rules.TitleRegexMatches, - rules.TitleMinLength, - rules.BodyMaxLineLength, - rules.BodyMinLength, - rules.BodyMissing, - rules.BodyTrailingWhitespace, - rules.BodyHardTab, - rules.BodyFirstLineEmpty, - rules.BodyChangedFileMention, - rules.BodyRegexMatches, - rules.AuthorValidEmail) - - def __init__(self): - self.rules = RuleCollection(self.default_rule_classes) - self._verbosity = options.IntOption('verbosity', 3, "Verbosity") - self._ignore_merge_commits = options.BoolOption('ignore-merge-commits', True, "Ignore merge commits") - self._ignore_fixup_commits = options.BoolOption('ignore-fixup-commits', True, "Ignore fixup commits") - self._ignore_squash_commits = options.BoolOption('ignore-squash-commits', True, "Ignore squash commits") - self._ignore_revert_commits = options.BoolOption('ignore-revert-commits', True, "Ignore revert commits") - self._debug = options.BoolOption('debug', False, "Enable debug mode") - self._extra_path = None - target_description = "Path of the target git repository (default=current working directory)" - self._target = options.PathOption('target', os.path.realpath(os.getcwd()), target_description) - self._ignore = options.ListOption('ignore', [], 'List of rule-ids to ignore') - self._contrib = options.ListOption('contrib', [], 'List of contrib-rules to enable') - self._config_path = None - ignore_stdin_description = "Ignore any stdin data. Useful for running in CI server." - self._ignore_stdin = options.BoolOption('ignore-stdin', False, ignore_stdin_description) - self._staged = options.BoolOption('staged', False, "Read staged commit meta-info from the local repository.") - self._fail_without_commits = options.BoolOption('fail-without-commits', False, - "Hard fail when the target commit range is empty") - - @property - def target(self): - return self._target.value if self._target else None - - @target.setter - @handle_option_error - def target(self, value): - return self._target.set(value) - - @property - def verbosity(self): - return self._verbosity.value - - @verbosity.setter - @handle_option_error - def verbosity(self, value): - self._verbosity.set(value) - if self.verbosity < 0 or self.verbosity > 3: - raise LintConfigError("Option 'verbosity' must be set between 0 and 3") - - @property - def ignore_merge_commits(self): - return self._ignore_merge_commits.value - - @ignore_merge_commits.setter - @handle_option_error - def ignore_merge_commits(self, value): - return self._ignore_merge_commits.set(value) - - @property - def ignore_fixup_commits(self): - return self._ignore_fixup_commits.value - - @ignore_fixup_commits.setter - @handle_option_error - def ignore_fixup_commits(self, value): - return self._ignore_fixup_commits.set(value) - - @property - def ignore_squash_commits(self): - return self._ignore_squash_commits.value - - @ignore_squash_commits.setter - @handle_option_error - def ignore_squash_commits(self, value): - return self._ignore_squash_commits.set(value) - - @property - def ignore_revert_commits(self): - return self._ignore_revert_commits.value - - @ignore_revert_commits.setter - @handle_option_error - def ignore_revert_commits(self, value): - return self._ignore_revert_commits.set(value) - - @property - def debug(self): - return self._debug.value - - @debug.setter - @handle_option_error - def debug(self, value): - return self._debug.set(value) - - @property - def ignore(self): - return self._ignore.value - - @ignore.setter - def ignore(self, value): - if value == "all": - value = [rule.id for rule in self.rules] - return self._ignore.set(value) - - @property - def ignore_stdin(self): - return self._ignore_stdin.value - - @ignore_stdin.setter - @handle_option_error - def ignore_stdin(self, value): - return self._ignore_stdin.set(value) - - @property - def staged(self): - return self._staged.value - - @staged.setter - @handle_option_error - def staged(self, value): - return self._staged.set(value) - - @property - def fail_without_commits(self): - return self._fail_without_commits.value - - @fail_without_commits.setter - @handle_option_error - def fail_without_commits(self, value): - return self._fail_without_commits.set(value) - - @property - def extra_path(self): - return self._extra_path.value if self._extra_path else None - - @extra_path.setter - def extra_path(self, value): - try: - if self.extra_path: - self._extra_path.set(value) - else: - self._extra_path = options.PathOption( - 'extra-path', value, - "Path to a directory or module with extra user-defined rules", - type='both' - ) - - # Make sure we unload any previously loaded extra-path rules - self.rules.delete_rules_by_attr("is_user_defined", True) - - # Find rules in the new extra-path and add them to the existing rules - rule_classes = rule_finder.find_rule_classes(self.extra_path) - self.rules.add_rules(rule_classes, {'is_user_defined': True}) - - except (options.RuleOptionError, rules.UserRuleError) as e: - raise LintConfigError(str(e)) from e - - @property - def contrib(self): - return self._contrib.value - - @contrib.setter - def contrib(self, value): - try: - self._contrib.set(value) - - # Make sure we unload any previously loaded contrib rules when re-setting the value - self.rules.delete_rules_by_attr("is_contrib", True) - - # Load all classes from the contrib directory - contrib_dir_path = os.path.dirname(os.path.realpath(contrib_rules.__file__)) - rule_classes = rule_finder.find_rule_classes(contrib_dir_path) - - # For each specified contrib rule, check whether it exists among the contrib classes - for rule_id_or_name in self.contrib: - rule_class = next((rc for rc in rule_classes if - rule_id_or_name in (rc.id, rc.name)), False) - - # If contrib rule exists, instantiate it and add it to the rules list - if rule_class: - self.rules.add_rule(rule_class, rule_class.id, {'is_contrib': True}) - else: - raise LintConfigError(f"No contrib rule with id or name '{rule_id_or_name}' found.") - - except (options.RuleOptionError, rules.UserRuleError) as e: - raise LintConfigError(str(e)) from e - - def _get_option(self, rule_name_or_id, option_name): - rule = self.rules.find_rule(rule_name_or_id) - if not rule: - raise LintConfigError(f"No such rule '{rule_name_or_id}'") - - option = rule.options.get(option_name) - if not option: - raise LintConfigError(f"Rule '{rule_name_or_id}' has no option '{option_name}'") - - return option - - def get_rule_option(self, rule_name_or_id, option_name): - """ Returns the value of a given option for a given rule. LintConfigErrors will be raised if the - rule or option don't exist. """ - option = self._get_option(rule_name_or_id, option_name) - return option.value - - def set_rule_option(self, rule_name_or_id, option_name, option_value): - """ Attempts to set a given value for a given option for a given rule. - LintConfigErrors will be raised if the rule or option don't exist or if the value is invalid. """ - option = self._get_option(rule_name_or_id, option_name) - try: - option.set(option_value) - except options.RuleOptionError as e: - msg = f"'{option_value}' is not a valid value for option '{rule_name_or_id}.{option_name}'. {e}." - raise LintConfigError(msg) from e - - def set_general_option(self, option_name, option_value): - attr_name = option_name.replace("-", "_") - # only allow setting general options that exist and don't start with an underscore - if not hasattr(self, attr_name) or attr_name[0] == "_": - raise LintConfigError(f"'{option_name}' is not a valid gitlint option") - - # else: - setattr(self, attr_name, option_value) - - def __eq__(self, other): - return isinstance(other, LintConfig) and \ - self.rules == other.rules and \ - self.verbosity == other.verbosity and \ - self.target == other.target and \ - self.extra_path == other.extra_path and \ - self.contrib == other.contrib and \ - self.ignore_merge_commits == other.ignore_merge_commits and \ - self.ignore_fixup_commits == other.ignore_fixup_commits and \ - self.ignore_squash_commits == other.ignore_squash_commits and \ - self.ignore_revert_commits == other.ignore_revert_commits and \ - self.ignore_stdin == other.ignore_stdin and \ - self.staged == other.staged and \ - self.fail_without_commits == other.fail_without_commits and \ - self.debug == other.debug and \ - self.ignore == other.ignore and \ - self._config_path == other._config_path # noqa - - def __str__(self): - # config-path is not a user exposed variable, so don't print it under the general section - return (f"config-path: {self._config_path}\n" - f"[GENERAL]\n" - f"extra-path: {self.extra_path}\n" - f"contrib: {self.contrib}\n" - f"ignore: {','.join(self.ignore)}\n" - f"ignore-merge-commits: {self.ignore_merge_commits}\n" - f"ignore-fixup-commits: {self.ignore_fixup_commits}\n" - f"ignore-squash-commits: {self.ignore_squash_commits}\n" - f"ignore-revert-commits: {self.ignore_revert_commits}\n" - f"ignore-stdin: {self.ignore_stdin}\n" - f"staged: {self.staged}\n" - f"fail-without-commits: {self.fail_without_commits}\n" - f"verbosity: {self.verbosity}\n" - f"debug: {self.debug}\n" - f"target: {self.target}\n" - f"[RULES]\n{self.rules}") - - -class RuleCollection: - """ Class representing an ordered list of rules. Methods are provided to easily retrieve, add or delete rules. """ - - def __init__(self, rule_classes=None, rule_attrs=None): - # Use an ordered dict so that the order in which rules are applied is always the same - self._rules = OrderedDict() - if rule_classes: - self.add_rules(rule_classes, rule_attrs) - - def find_rule(self, rule_id_or_name): - rule = self._rules.get(rule_id_or_name) - # if not found, try finding rule by name - if not rule: - rule = next((rule for rule in self._rules.values() if rule.name == rule_id_or_name), None) - return rule - - def add_rule(self, rule_class, rule_id, rule_attrs=None): - """ Instantiates and adds a rule to RuleCollection. - Note: There can be multiple instantiations of the same rule_class in the RuleCollection, as long as the - rule_id is unique. - :param rule_class python class representing the rule - :param rule_id unique identifier for the rule. If not unique, it will - overwrite the existing rule with that id - :param rule_attrs dictionary of attributes to set on the instantiated rule obj - """ - rule_obj = rule_class() - rule_obj.id = rule_id - if rule_attrs: - for key, val in rule_attrs.items(): - setattr(rule_obj, key, val) - self._rules[rule_obj.id] = rule_obj - - def add_rules(self, rule_classes, rule_attrs=None): - """ Convenience method to add multiple rules at once based on a list of rule classes. """ - for rule_class in rule_classes: - self.add_rule(rule_class, rule_class.id, rule_attrs) - - def delete_rules_by_attr(self, attr_name, attr_val): - """ Deletes all rules from the collection that match a given attribute name and value """ - # Create a new list based on _rules.values() because in python 3, values() is a ValuesView as opposed to a list - # This means you can't modify the ValueView while iterating over it. - for rule in [r for r in self._rules.values()]: # pylint: disable=unnecessary-comprehension - if hasattr(rule, attr_name) and (getattr(rule, attr_name) == attr_val): - del self._rules[rule.id] - - def __iter__(self): - for rule in self._rules.values(): - yield rule - - def __eq__(self, other): - return isinstance(other, RuleCollection) and self._rules == other._rules - - def __len__(self): - return len(self._rules) - - def __str__(self): - return_str = "" - for rule in self._rules.values(): - return_str += f" {rule.id}: {rule.name}\n" - for option_name, option_value in sorted(rule.options.items()): - if option_value.value is None: - option_val_repr = None - elif isinstance(option_value.value, list): - option_val_repr = ",".join(option_value.value) - elif isinstance(option_value, options.RegexOption): - option_val_repr = option_value.value.pattern - else: - option_val_repr = option_value.value - return_str += f" {option_name}={option_val_repr}\n" - return return_str - - -class LintConfigBuilder: - """ Factory class that can build gitlint config. - This is primarily useful to deal with complex configuration scenarios where configuration can be set and overridden - from various sources (typically according to certain precedence rules) before the actual config should be - normalized, validated and build. Example usage can be found in gitlint.cli. - """ - - RULE_QUALIFIER_SYMBOL = ":" - - def __init__(self): - self._config_blueprint = OrderedDict() - self._config_path = None - - def set_option(self, section, option_name, option_value): - if section not in self._config_blueprint: - self._config_blueprint[section] = OrderedDict() - self._config_blueprint[section][option_name] = option_value - - def set_config_from_commit(self, commit): - """ Given a git commit, applies config specified in the commit message. - Supported: - - gitlint-ignore: all - """ - for line in commit.message.body: - pattern = re.compile(r"^gitlint-ignore:\s*(.*)") - matches = pattern.match(line) - if matches and len(matches.groups()) == 1: - self.set_option('general', 'ignore', matches.group(1)) - - def set_config_from_string_list(self, config_options): - """ Given a list of config options of the form "<rule>.<option>=<value>", parses out the correct rule and option - and sets the value accordingly in this factory object. """ - for config_option in config_options: - try: - config_name, option_value = config_option.split("=", 1) - if not option_value: - raise ValueError() - rule_name, option_name = config_name.split(".", 1) - self.set_option(rule_name, option_name, option_value) - except ValueError as e: # raised if the config string is invalid - raise LintConfigError( - f"'{config_option}' is an invalid configuration option. Use '<rule>.<option>=<value>'") from e - - def set_from_config_file(self, filename): - """ Loads lint config from a ini-style config file """ - if not os.path.exists(filename): - raise LintConfigError(f"Invalid file path: {filename}") - self._config_path = os.path.realpath(filename) - try: - parser = ConfigParser() - - with io.open(filename, encoding=DEFAULT_ENCODING) as config_file: - parser.read_file(config_file, filename) - - for section_name in parser.sections(): - for option_name, option_value in parser.items(section_name): - self.set_option(section_name, option_name, str(option_value)) - - except ConfigParserError as e: - raise LintConfigError(str(e)) from e - - def _add_named_rule(self, config, qualified_rule_name): - """ Adds a Named Rule to a given LintConfig object. - IMPORTANT: This method does *NOT* overwrite existing Named Rules with the same canonical id. - """ - - # Split up named rule in its parts: the name/id that specifies the parent rule, - # And the name of the rule instance itself - rule_name_parts = qualified_rule_name.split(self.RULE_QUALIFIER_SYMBOL, 1) - rule_name = rule_name_parts[1].strip() - parent_rule_specifier = rule_name_parts[0].strip() - - # assert that the rule name is valid: - # - not empty - # - no whitespace or colons - if rule_name == "" or bool(re.search("\\s|:", rule_name, re.UNICODE)): - msg = f"The rule-name part in '{qualified_rule_name}' cannot contain whitespace, colons or be empty" - raise LintConfigError(msg) - - # find parent rule - parent_rule = config.rules.find_rule(parent_rule_specifier) - if not parent_rule: - msg = f"No such rule '{parent_rule_specifier}' (named rule: '{qualified_rule_name}')" - raise LintConfigError(msg) - - # Determine canonical id and name by recombining the parent id/name and instance name parts. - canonical_id = parent_rule.__class__.id + self.RULE_QUALIFIER_SYMBOL + rule_name - canonical_name = parent_rule.__class__.name + self.RULE_QUALIFIER_SYMBOL + rule_name - - # Add the rule to the collection of rules if it's not there already - if not config.rules.find_rule(canonical_id): - config.rules.add_rule(parent_rule.__class__, canonical_id, {'is_named': True, 'name': canonical_name}) - - return canonical_id - - def build(self, config=None): - """ Build a real LintConfig object by normalizing and validating the options that were previously set on this - factory. """ - # If we are passed a config object, then rebuild that object instead of building a new lintconfig object from - # scratch - if not config: - config = LintConfig() - - config._config_path = self._config_path - - # Set general options first as this might change the behavior or validity of the other options - general_section = self._config_blueprint.get('general') - if general_section: - for option_name, option_value in general_section.items(): - config.set_general_option(option_name, option_value) - - for section_name, section_dict in self._config_blueprint.items(): - for option_name, option_value in section_dict.items(): - # Skip over the general section, as we've already done that above - if section_name != "general": - - # If the section name contains a colon (:), then this section is defining a Named Rule - # Which means we need to instantiate that Named Rule in the config. - if self.RULE_QUALIFIER_SYMBOL in section_name: - section_name = self._add_named_rule(config, section_name) - - config.set_rule_option(section_name, option_name, option_value) - - return config - - def clone(self): - """ Creates an exact copy of a LintConfigBuilder. """ - builder = LintConfigBuilder() - builder._config_blueprint = copy.deepcopy(self._config_blueprint) - builder._config_path = self._config_path - return builder - - -GITLINT_CONFIG_TEMPLATE_SRC_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "files/gitlint") - - -class LintConfigGenerator: - @staticmethod - def generate_config(dest): - """ Generates a gitlint config file at the given destination location. - Expects that the given ```dest``` points to a valid destination. """ - shutil.copyfile(GITLINT_CONFIG_TEMPLATE_SRC_PATH, dest) |