diff options
Diffstat (limited to 'gitlint-core/gitlint/options.py')
-rw-r--r-- | gitlint-core/gitlint/options.py | 146 |
1 files changed, 146 insertions, 0 deletions
diff --git a/gitlint-core/gitlint/options.py b/gitlint-core/gitlint/options.py new file mode 100644 index 0000000..ff7d9f1 --- /dev/null +++ b/gitlint-core/gitlint/options.py @@ -0,0 +1,146 @@ +import os +import re +from abc import abstractmethod + +from gitlint.exception import GitlintError + + +def allow_none(func): + """Decorator that sets option value to None if the passed value is None, otherwise calls the regular set method""" + + def wrapped(obj, value): + if value is None: + obj.value = None + else: + func(obj, value) + + return wrapped + + +class RuleOptionError(GitlintError): + pass + + +class RuleOption: + """Base class representing a configurable part (i.e. option) of a rule (e.g. the max-length of the title-max-line + rule). + This class should not be used directly. Instead, use on the derived classes like StrOption, IntOption to set + options of a particular type like int, str, etc. + """ + + def __init__(self, name, value, description): + self.name = name + self.description = description + self.value = None + self.set(value) + + @abstractmethod + def set(self, value): + """Validates and sets the option's value""" + + def __str__(self): + return f"({self.name}: {self.value} ({self.description}))" + + def __eq__(self, other): + return self.name == other.name and self.description == other.description and self.value == other.value + + +class StrOption(RuleOption): + @allow_none + def set(self, value): + self.value = str(value) + + +class IntOption(RuleOption): + def __init__(self, name, value, description, allow_negative=False): + self.allow_negative = allow_negative + super().__init__(name, value, description) + + def _raise_exception(self, value): + if self.allow_negative: + error_msg = f"Option '{self.name}' must be an integer (current value: '{value}')" + else: + error_msg = f"Option '{self.name}' must be a positive integer (current value: '{value}')" + raise RuleOptionError(error_msg) + + @allow_none + def set(self, value): + try: + self.value = int(value) + except ValueError: + self._raise_exception(value) + + if not self.allow_negative and self.value < 0: + self._raise_exception(value) + + +class BoolOption(RuleOption): + # explicit choice to not annotate with @allow_none: Booleans must be False or True, they cannot be unset. + def set(self, value): + value = str(value).strip().lower() + if value not in ["true", "false"]: + raise RuleOptionError(f"Option '{self.name}' must be either 'true' or 'false'") + self.value = value == "true" + + +class ListOption(RuleOption): + """Option that is either a given list or a comma-separated string that can be split into a list when being set.""" + + @allow_none + def set(self, value): + if isinstance(value, list): + the_list = value + else: + the_list = str(value).split(",") + + self.value = [str(item.strip()) for item in the_list if item.strip() != ""] + + +class PathOption(RuleOption): + """Option that accepts either a directory or both a directory and a file.""" + + def __init__(self, name, value, description, type="dir"): + self.type = type + super().__init__(name, value, description) + + @allow_none + def set(self, value): + value = str(value) + + error_msg = "" + + if self.type == "dir": + if not os.path.isdir(value): + error_msg = f"Option {self.name} must be an existing directory (current value: '{value}')" + elif self.type == "file": + if not os.path.isfile(value): + error_msg = f"Option {self.name} must be an existing file (current value: '{value}')" + elif self.type == "both": + if not os.path.isdir(value) and not os.path.isfile(value): + error_msg = ( + f"Option {self.name} must be either an existing directory or file (current value: '{value}')" + ) + else: + error_msg = f"Option {self.name} type must be one of: 'file', 'dir', 'both' (current: '{self.type}')" + + if error_msg: + raise RuleOptionError(error_msg) + + self.value = os.path.realpath(value) + + +class RegexOption(RuleOption): + @allow_none + def set(self, value): + try: + self.value = re.compile(value, re.UNICODE) + except (re.error, TypeError) as exc: + raise RuleOptionError(f"Invalid regular expression: '{exc}'") from exc + + def __deepcopy__(self, _): + # copy.deepcopy() - used in rules.py - doesn't support copying regex objects prior to Python 3.7 + # To work around this, we have to implement this __deepcopy__ magic method + # Relevant SO thread: + # https://stackoverflow.com/questions/6279305/typeerror-cannot-deepcopy-this-pattern-object + value = None if self.value is None else self.value.pattern + return RegexOption(self.name, value, self.description) |