summaryrefslogtreecommitdiffstats
path: root/gitlint/config.py
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2021-12-04 03:31:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2021-12-04 03:31:41 +0000
commit72b8c35be4293bd21de123854491c658c53af100 (patch)
tree735464cc081879561927a37650b1102beaa1f4f9 /gitlint/config.py
parentAdding upstream version 0.16.0. (diff)
downloadgitlint-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.py528
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)