146 lines
4.8 KiB
Python
146 lines
4.8 KiB
Python
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)
|