summaryrefslogtreecommitdiffstats
path: root/gitlint-core/gitlint/options.py
diff options
context:
space:
mode:
Diffstat (limited to 'gitlint-core/gitlint/options.py')
-rw-r--r--gitlint-core/gitlint/options.py146
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)