1
0
Fork 0
gitlint/gitlint-core/gitlint/rule_finder.py
Daniel Baumann 435cb3a48d
Merging upstream version 0.17.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-02-13 06:06:00 +01:00

145 lines
7.2 KiB
Python

import fnmatch
import inspect
import os
import sys
import importlib
from gitlint import rules, options
def find_rule_classes(extra_path):
"""
Searches a given directory or python module for rule classes. This is done by
adding the directory path to the python path, importing the modules and then finding
any Rule class in those modules.
:param extra_path: absolute directory or file path to search for rule classes
:return: The list of rule classes that are found in the given directory or module
"""
files = []
modules = []
if os.path.isfile(extra_path):
files = [os.path.basename(extra_path)]
directory = os.path.dirname(extra_path)
elif os.path.isdir(extra_path):
files = os.listdir(extra_path)
directory = extra_path
else:
raise rules.UserRuleError(f"Invalid extra-path: {extra_path}")
# Filter out files that are not python modules
for filename in files:
if fnmatch.fnmatch(filename, '*.py'):
# We have to treat __init__ files a bit special: add the parent dir instead of the filename, and also
# add their parent dir to the sys.path (this fixes import issues with pypy2).
if filename == "__init__.py":
modules.append(os.path.basename(directory))
sys.path.append(os.path.dirname(directory))
else:
modules.append(os.path.splitext(filename)[0])
# No need to continue if there are no modules specified
if not modules:
return []
# Append the extra rules path to python path so that we can import them
sys.path.append(directory)
# Find all the rule classes in the found python files
rule_classes = []
for module in modules:
# Import the module
try:
importlib.import_module(module)
except Exception as e:
raise rules.UserRuleError(f"Error while importing extra-path module '{module}': {e}")
# Find all rule classes in the module. We do this my inspecting all members of the module and checking
# 1) is it a class, if not, skip
# 2) is the parent path the current module. If not, we are dealing with an imported class, skip
# 3) is it a subclass of rule
rule_classes.extend([clazz for _, clazz in inspect.getmembers(sys.modules[module])
if
inspect.isclass(clazz) and # check isclass to ensure clazz.__module__ exists
clazz.__module__ == module and # ignore imported classes
(issubclass(clazz, rules.LineRule) or
issubclass(clazz, rules.CommitRule) or
issubclass(clazz, rules.ConfigurationRule))])
# validate that the rule classes are valid user-defined rules
for rule_class in rule_classes:
assert_valid_rule_class(rule_class)
return rule_classes
def assert_valid_rule_class(clazz, rule_type="User-defined"): # pylint: disable=too-many-branches
"""
Asserts that a given rule clazz is valid by checking a number of its properties:
- Rules must extend from LineRule, CommitRule or ConfigurationRule
- Rule classes must have id and name string attributes.
The options_spec is optional, but if set, it must be a list of gitlint Options.
- Rule classes must have a validate method. In case of a CommitRule, validate must take a single commit parameter.
In case of LineRule, validate must take line and commit as first and second parameters.
- LineRule classes must have a target class attributes that is set to either
- ConfigurationRule classes must have an apply method that take `config` and `commit` as parameters.
CommitMessageTitle or CommitMessageBody.
- Rule id's cannot start with R, T, B, M or I as these rule ids are reserved for gitlint itself.
"""
# Rules must extend from LineRule, CommitRule or ConfigurationRule
if not (issubclass(clazz, rules.LineRule) or issubclass(clazz, rules.CommitRule)
or issubclass(clazz, rules.ConfigurationRule)):
msg = f"{rule_type} rule class '{clazz.__name__}' " + \
f"must extend from {rules.CommitRule.__module__}.{rules.LineRule.__name__}, " + \
f"{rules.CommitRule.__module__}.{rules.CommitRule.__name__} or " + \
f"{rules.CommitRule.__module__}.{rules.ConfigurationRule.__name__}"
raise rules.UserRuleError(msg)
# Rules must have an id attribute
if not hasattr(clazz, 'id') or clazz.id is None or not clazz.id:
raise rules.UserRuleError(f"{rule_type} rule class '{clazz.__name__}' must have an 'id' attribute")
# Rule id's cannot start with gitlint reserved letters
if clazz.id[0].upper() in ['R', 'T', 'B', 'M', 'I']:
msg = f"The id '{clazz.id[0]}' of '{clazz.__name__}' is invalid. Gitlint reserves ids starting with R,T,B,M,I"
raise rules.UserRuleError(msg)
# Rules must have a name attribute
if not hasattr(clazz, 'name') or clazz.name is None or not clazz.name:
raise rules.UserRuleError(f"{rule_type} rule class '{clazz.__name__}' must have a 'name' attribute")
# if set, options_spec must be a list of RuleOption
if not isinstance(clazz.options_spec, list):
msg = f"The options_spec attribute of {rule_type.lower()} rule class '{clazz.__name__}' " + \
f"must be a list of {options.RuleOption.__module__}.{options.RuleOption.__name__}"
raise rules.UserRuleError(msg)
# check that all items in options_spec are actual gitlint options
for option in clazz.options_spec:
if not isinstance(option, options.RuleOption):
msg = f"The options_spec attribute of {rule_type.lower()} rule class '{clazz.__name__}' " + \
f"must be a list of {options.RuleOption.__module__}.{options.RuleOption.__name__}"
raise rules.UserRuleError(msg)
# Line/Commit rules must have a `validate` method
# We use isroutine() as it's both python 2 and 3 compatible. Details: http://stackoverflow.com/a/17019998/381010
if (issubclass(clazz, rules.LineRule) or issubclass(clazz, rules.CommitRule)):
if not hasattr(clazz, 'validate') or not inspect.isroutine(clazz.validate):
raise rules.UserRuleError(f"{rule_type} rule class '{clazz.__name__}' must have a 'validate' method")
# Configuration rules must have an `apply` method
elif issubclass(clazz, rules.ConfigurationRule):
if not hasattr(clazz, 'apply') or not inspect.isroutine(clazz.apply):
msg = f"{rule_type} Configuration rule class '{clazz.__name__}' must have an 'apply' method"
raise rules.UserRuleError(msg)
# LineRules must have a valid target: rules.CommitMessageTitle or rules.CommitMessageBody
if issubclass(clazz, rules.LineRule):
if clazz.target not in [rules.CommitMessageTitle, rules.CommitMessageBody]:
msg = f"The target attribute of the {rule_type.lower()} LineRule class '{clazz.__name__}' " + \
f"must be either {rules.CommitMessageTitle.__module__}.{rules.CommitMessageTitle.__name__} " + \
f"or {rules.CommitMessageTitle.__module__}.{rules.CommitMessageBody.__name__}"
raise rules.UserRuleError(msg)