summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoris Roovers <joris.roovers@gmail.com>2023-04-04 11:53:07 +0200
committerJoris Roovers <joris.roovers@gmail.com>2023-04-04 10:09:38 +0000
commit10f86a0f364c849189fc11bf581ab72f379bc646 (patch)
treec929f2d907c823d738a19e434115247e4ba5fa31
parent6cbe7986f68a3e5cb336a3eb31f6bd428d469ea8 (diff)
Dataclasses (#479)
Converts existing classes to dataclasses where applicable. Relates to #321
-rw-r--r--gitlint-core/gitlint/cache.py7
-rw-r--r--gitlint-core/gitlint/cli.py23
-rw-r--r--gitlint-core/gitlint/config.py11
-rw-r--r--gitlint-core/gitlint/deprecation.py5
-rw-r--r--gitlint-core/gitlint/display.py7
-rw-r--r--gitlint-core/gitlint/git.py270
-rw-r--r--gitlint-core/gitlint/lint.py12
-rw-r--r--gitlint-core/gitlint/options.py31
-rw-r--r--gitlint-core/gitlint/rules.py50
-rw-r--r--gitlint-core/gitlint/shell.py11
-rw-r--r--gitlint-core/gitlint/tests/rules/test_rules.py17
11 files changed, 218 insertions, 226 deletions
diff --git a/gitlint-core/gitlint/cache.py b/gitlint-core/gitlint/cache.py
index a3dd0c8..5c20896 100644
--- a/gitlint-core/gitlint/cache.py
+++ b/gitlint-core/gitlint/cache.py
@@ -1,8 +1,11 @@
+from dataclasses import dataclass, field
+
+
+@dataclass
class PropertyCache:
"""Mixin class providing a simple cache."""
- def __init__(self):
- self._cache = {}
+ _cache: dict = field(init=False, default_factory=dict)
def _try_cache(self, cache_key, cache_populate_func):
"""Tries to get a value from the cache identified by `cache_key`.
diff --git a/gitlint-core/gitlint/cli.py b/gitlint-core/gitlint/cli.py
index 240b005..82f35ce 100644
--- a/gitlint-core/gitlint/cli.py
+++ b/gitlint-core/gitlint/cli.py
@@ -4,12 +4,19 @@ import os
import platform
import stat
import sys
+from dataclasses import dataclass
+from typing import Optional
import click
import gitlint
from gitlint import hooks
-from gitlint.config import LintConfigBuilder, LintConfigError, LintConfigGenerator
+from gitlint.config import (
+ LintConfig,
+ LintConfigBuilder,
+ LintConfigError,
+ LintConfigGenerator,
+)
from gitlint.deprecation import DEPRECATED_LOG_FORMAT
from gitlint.deprecation import LOG as DEPRECATED_LOG
from gitlint.exception import GitlintError
@@ -230,16 +237,16 @@ def handle_gitlint_error(ctx, exc):
ctx.exit(CONFIG_ERROR_CODE)
+@dataclass
class ContextObj:
"""Simple class to hold data that is passed between Click commands via the Click context."""
- def __init__(self, config, config_builder, commit_hash, refspec, msg_filename, gitcontext=None):
- self.config = config
- self.config_builder = config_builder
- self.commit_hash = commit_hash
- self.refspec = refspec
- self.msg_filename = msg_filename
- self.gitcontext = gitcontext
+ config: LintConfig
+ config_builder: LintConfigBuilder
+ commit_hash: str
+ refspec: str
+ msg_filename: str
+ gitcontext: Optional[GitContext] = None
# fmt: off
diff --git a/gitlint-core/gitlint/config.py b/gitlint-core/gitlint/config.py
index 4b38d90..4637af2 100644
--- a/gitlint-core/gitlint/config.py
+++ b/gitlint-core/gitlint/config.py
@@ -5,6 +5,8 @@ import shutil
from collections import OrderedDict
from configparser import ConfigParser
from configparser import Error as ConfigParserError
+from dataclasses import dataclass, field
+from typing import ClassVar, Optional
from gitlint import (
options,
@@ -416,6 +418,7 @@ class RuleCollection:
return return_str
+@dataclass
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
@@ -423,11 +426,9 @@ class LintConfigBuilder:
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
+ RULE_QUALIFIER_SYMBOL: ClassVar[str] = ":"
+ _config_blueprint: OrderedDict = field(init=False, default_factory=OrderedDict)
+ _config_path: Optional[str] = field(init=False, default=None)
def set_option(self, section, option_name, option_value):
if section not in self._config_blueprint:
diff --git a/gitlint-core/gitlint/deprecation.py b/gitlint-core/gitlint/deprecation.py
index b7c2f42..67c7cf9 100644
--- a/gitlint-core/gitlint/deprecation.py
+++ b/gitlint-core/gitlint/deprecation.py
@@ -1,4 +1,5 @@
import logging
+from typing import ClassVar, Optional, Set
LOG = logging.getLogger("gitlint.deprecated")
DEPRECATED_LOG_FORMAT = "%(levelname)s: %(message)s"
@@ -8,10 +9,10 @@ class Deprecation:
"""Singleton class that handles deprecation warnings and behavior."""
# LintConfig class that is used to determine deprecation behavior
- config = None
+ config: ClassVar[Optional[object]] = None
# Set of warning messages that have already been logged, to prevent duplicate warnings
- warning_msgs = set()
+ warning_msgs: ClassVar[Set[str]] = set()
@classmethod
def get_regex_method(cls, rule, regex_option):
diff --git a/gitlint-core/gitlint/display.py b/gitlint-core/gitlint/display.py
index 1de8d08..834bca0 100644
--- a/gitlint-core/gitlint/display.py
+++ b/gitlint-core/gitlint/display.py
@@ -1,11 +1,14 @@
+from dataclasses import dataclass
from sys import stderr, stdout
+from gitlint.config import LintConfig
+
+@dataclass
class Display:
"""Utility class to print stuff to an output stream (stdout by default) based on the config's verbosity"""
- def __init__(self, lint_config):
- self.config = lint_config
+ config: LintConfig
def _output(self, message, verbosity, exact, stream):
"""Output a message if the config's verbosity is >= to the given verbosity. If exact == True, the message
diff --git a/gitlint-core/gitlint/git.py b/gitlint-core/gitlint/git.py
index 6612a7d..9c08d2d 100644
--- a/gitlint-core/gitlint/git.py
+++ b/gitlint-core/gitlint/git.py
@@ -1,6 +1,9 @@
import logging
import os
+from dataclasses import dataclass, field
+from datetime import datetime
from pathlib import Path
+from typing import Dict, List, Optional
import arrow
@@ -108,6 +111,95 @@ def _parse_git_changed_file_stats(changed_files_stats_raw):
return changed_files_stats
+@dataclass
+class GitContext(PropertyCache):
+ """Class representing the git context in which gitlint is operating: a data object storing information about
+ the git repository that gitlint is linting.
+ """
+
+ commits: List["GitCommit"] = field(init=False, default_factory=list)
+ repository_path: Optional[str] = None
+
+ @property
+ @cache
+ def commentchar(self):
+ return git_commentchar(self.repository_path)
+
+ @property
+ @cache
+ def current_branch(self):
+ try:
+ current_branch = _git("rev-parse", "--abbrev-ref", "HEAD", _cwd=self.repository_path).strip()
+ except GitContextError:
+ # Maybe there is no commit. Try another way to get current branch (need Git 2.22+)
+ current_branch = _git("branch", "--show-current", _cwd=self.repository_path).strip()
+ return current_branch
+
+ @staticmethod
+ def from_commit_msg(commit_msg_str):
+ """Determines git context based on a commit message.
+ :param commit_msg_str: Full git commit message.
+ """
+ context = GitContext()
+ commit_msg_obj = GitCommitMessage.from_full_message(context, commit_msg_str)
+ commit = GitCommit(context, commit_msg_obj)
+ context.commits.append(commit)
+ return context
+
+ @staticmethod
+ def from_staged_commit(commit_msg_str, repository_path):
+ """Determines git context based on a commit message that is a staged commit for a local git repository.
+ :param commit_msg_str: Full git commit message.
+ :param repository_path: Path to the git repository to retrieve the context from
+ """
+ context = GitContext(repository_path=repository_path)
+ commit_msg_obj = GitCommitMessage.from_full_message(context, commit_msg_str)
+ commit = StagedLocalGitCommit(context, commit_msg_obj)
+ context.commits.append(commit)
+ return context
+
+ @staticmethod
+ def from_local_repository(repository_path, refspec=None, commit_hashes=None):
+ """Retrieves the git context from a local git repository.
+ :param repository_path: Path to the git repository to retrieve the context from
+ :param refspec: The commit(s) to retrieve (mutually exclusive with `commit_hash`)
+ :param commit_hash: Hash of the commit to retrieve (mutually exclusive with `refspec`)
+ """
+
+ context = GitContext(repository_path=repository_path)
+
+ if refspec:
+ sha_list = _git("rev-list", refspec, _cwd=repository_path).split()
+ elif commit_hashes: # One or more commit hashes, just pass it to `git log -1`
+ # Even though we have already been passed the commit hash, we ask git to retrieve this hash and
+ # return it to us. This way we verify that the passed hash is a valid hash for the target repo and we
+ # also convert it to the full hash format (we might have been passed a short hash).
+ sha_list = []
+ for commit_hash in commit_hashes:
+ sha_list.append(_git("log", "-1", commit_hash, "--pretty=%H", _cwd=repository_path).replace("\n", ""))
+ else: # If no refspec is defined, fallback to the last commit on the current branch
+ # We tried many things here e.g.: defaulting to e.g. HEAD or HEAD^... (incl. dealing with
+ # repos that only have a single commit - HEAD^... doesn't work there), but then we still get into
+ # problems with e.g. merge commits. Easiest solution is just taking the SHA from `git log -1`.
+ sha_list = [_git("log", "-1", "--pretty=%H", _cwd=repository_path).replace("\n", "")]
+
+ for sha in sha_list:
+ commit = LocalGitCommit(context, sha)
+ context.commits.append(commit)
+
+ return context
+
+ def __eq__(self, other):
+ return (
+ isinstance(other, GitContext)
+ and self.commits == other.commits
+ and self.repository_path == other.repository_path
+ and self.commentchar == other.commentchar
+ and self.current_branch == other.current_branch
+ )
+
+
+@dataclass
class GitCommitMessage:
"""Class representing a git commit message. A commit message consists of the following:
- context: The `GitContext` this commit message is part of
@@ -117,12 +209,11 @@ class GitCommitMessage:
- body: all lines following the title
"""
- def __init__(self, context, original=None, full=None, title=None, body=None):
- self.context = context
- self.original = original
- self.full = full
- self.title = title
- self.body = body
+ context: GitContext = field(compare=False)
+ original: str
+ full: str
+ title: str
+ body: List[str]
@staticmethod
def from_full_message(context, commit_msg_str):
@@ -142,36 +233,20 @@ class GitCommitMessage:
def __str__(self):
return self.full
- def __eq__(self, other):
- return (
- isinstance(other, GitCommitMessage)
- and self.original == other.original
- and self.full == other.full
- and self.title == other.title
- and self.body == other.body
- )
-
+@dataclass
class GitChangedFileStats:
"""Class representing the stats for a changed file in git"""
- def __init__(self, filepath, additions, deletions):
- self.filepath = Path(filepath)
- self.additions = additions
- self.deletions = deletions
-
- def __eq__(self, other):
- return (
- isinstance(other, GitChangedFileStats)
- and self.filepath == other.filepath
- and self.additions == other.additions
- and self.deletions == other.deletions
- )
+ filepath: Path
+ additions: int
+ deletions: int
def __str__(self) -> str:
return f"{self.filepath}: {self.additions} additions, {self.deletions} deletions"
+@dataclass
class GitCommit:
"""Class representing a git commit.
A commit consists of: context, message, author name, author email, date, list of parent commit shas,
@@ -179,27 +254,15 @@ class GitCommit:
In the context of gitlint, only the git context and commit message are required.
"""
- def __init__(
- self,
- context,
- message,
- sha=None,
- date=None,
- author_name=None,
- author_email=None,
- parents=None,
- changed_files_stats=None,
- branches=None,
- ):
- self.context = context
- self.message = message
- self.sha = sha
- self.date = date
- self.author_name = author_name
- self.author_email = author_email
- self.parents = parents or [] # parent commit hashes
- self.changed_files_stats = changed_files_stats or {}
- self.branches = branches or []
+ context: GitContext = field(compare=False)
+ message: GitCommitMessage
+ sha: Optional[str] = None
+ date: Optional[datetime] = None
+ author_name: Optional[str] = None
+ author_email: Optional[str] = None
+ parents: List[str] = field(default_factory=list)
+ changed_files_stats: Dict[str, GitChangedFileStats] = field(default_factory=dict)
+ branches: List[str] = field(default_factory=list)
@property
def is_merge_commit(self):
@@ -250,27 +313,8 @@ class GitCommit:
"-----------------------"
)
- def __eq__(self, other):
- # skip checking the context as context refers back to this obj, this will trigger a cyclic dependency
- return (
- isinstance(other, GitCommit)
- and self.message == other.message
- and self.sha == other.sha
- and self.author_name == other.author_name
- and self.author_email == other.author_email
- and self.date == other.date
- and self.parents == other.parents
- and self.is_merge_commit == other.is_merge_commit
- and self.is_fixup_commit == other.is_fixup_commit
- and self.is_fixup_amend_commit == other.is_fixup_amend_commit
- and self.is_squash_commit == other.is_squash_commit
- and self.is_revert_commit == other.is_revert_commit
- and self.changed_files == other.changed_files
- and self.changed_files_stats == other.changed_files_stats
- and self.branches == other.branches
- )
-
+@dataclass
class LocalGitCommit(GitCommit, PropertyCache):
"""Class representing a git commit that exists in the local git repository.
This class uses lazy loading: it defers reading information from the local git repository until the associated
@@ -366,6 +410,7 @@ class LocalGitCommit(GitCommit, PropertyCache):
return self._try_cache("changed_files_stats", cache_changed_files_stats)
+@dataclass
class StagedLocalGitCommit(GitCommit, PropertyCache):
"""Class representing a git commit that has been staged, but not committed.
@@ -419,92 +464,3 @@ class StagedLocalGitCommit(GitCommit, PropertyCache):
self._cache["changed_files_stats"] = _parse_git_changed_file_stats(changed_files_stats_raw)
return self._try_cache("changed_files_stats", cache_changed_files_stats)
-
-
-class GitContext(PropertyCache):
- """Class representing the git context in which gitlint is operating: a data object storing information about
- the git repository that gitlint is linting.
- """
-
- def __init__(self, repository_path=None):
- PropertyCache.__init__(self)
- self.commits = []
- self.repository_path = repository_path
-
- @property
- @cache
- def commentchar(self):
- return git_commentchar(self.repository_path)
-
- @property
- @cache
- def current_branch(self):
- try:
- current_branch = _git("rev-parse", "--abbrev-ref", "HEAD", _cwd=self.repository_path).strip()
- except GitContextError:
- # Maybe there is no commit. Try another way to get current branch (need Git 2.22+)
- current_branch = _git("branch", "--show-current", _cwd=self.repository_path).strip()
- return current_branch
-
- @staticmethod
- def from_commit_msg(commit_msg_str):
- """Determines git context based on a commit message.
- :param commit_msg_str: Full git commit message.
- """
- context = GitContext()
- commit_msg_obj = GitCommitMessage.from_full_message(context, commit_msg_str)
- commit = GitCommit(context, commit_msg_obj)
- context.commits.append(commit)
- return context
-
- @staticmethod
- def from_staged_commit(commit_msg_str, repository_path):
- """Determines git context based on a commit message that is a staged commit for a local git repository.
- :param commit_msg_str: Full git commit message.
- :param repository_path: Path to the git repository to retrieve the context from
- """
- context = GitContext(repository_path=repository_path)
- commit_msg_obj = GitCommitMessage.from_full_message(context, commit_msg_str)
- commit = StagedLocalGitCommit(context, commit_msg_obj)
- context.commits.append(commit)
- return context
-
- @staticmethod
- def from_local_repository(repository_path, refspec=None, commit_hashes=None):
- """Retrieves the git context from a local git repository.
- :param repository_path: Path to the git repository to retrieve the context from
- :param refspec: The commit(s) to retrieve (mutually exclusive with `commit_hash`)
- :param commit_hash: Hash of the commit to retrieve (mutually exclusive with `refspec`)
- """
-
- context = GitContext(repository_path=repository_path)
-
- if refspec:
- sha_list = _git("rev-list", refspec, _cwd=repository_path).split()
- elif commit_hashes: # One or more commit hashes, just pass it to `git log -1`
- # Even though we have already been passed the commit hash, we ask git to retrieve this hash and
- # return it to us. This way we verify that the passed hash is a valid hash for the target repo and we
- # also convert it to the full hash format (we might have been passed a short hash).
- sha_list = []
- for commit_hash in commit_hashes:
- sha_list.append(_git("log", "-1", commit_hash, "--pretty=%H", _cwd=repository_path).replace("\n", ""))
- else: # If no refspec is defined, fallback to the last commit on the current branch
- # We tried many things here e.g.: defaulting to e.g. HEAD or HEAD^... (incl. dealing with
- # repos that only have a single commit - HEAD^... doesn't work there), but then we still get into
- # problems with e.g. merge commits. Easiest solution is just taking the SHA from `git log -1`.
- sha_list = [_git("log", "-1", "--pretty=%H", _cwd=repository_path).replace("\n", "")]
-
- for sha in sha_list:
- commit = LocalGitCommit(context, sha)
- context.commits.append(commit)
-
- return context
-
- def __eq__(self, other):
- return (
- isinstance(other, GitContext)
- and self.commits == other.commits
- and self.repository_path == other.repository_path
- and self.commentchar == other.commentchar
- and self.current_branch == other.current_branch
- )
diff --git a/gitlint-core/gitlint/lint.py b/gitlint-core/gitlint/lint.py
index 420d3ad..8878669 100644
--- a/gitlint-core/gitlint/lint.py
+++ b/gitlint-core/gitlint/lint.py
@@ -1,20 +1,24 @@
import logging
+from dataclasses import dataclass, field
-from gitlint import display
from gitlint import rules as gitlint_rules
+from gitlint.config import LintConfig
from gitlint.deprecation import Deprecation
+from gitlint.display import Display
LOG = logging.getLogger(__name__)
logging.basicConfig()
+@dataclass
class GitLinter:
"""Main linter class. This is where rules actually get applied. See the lint() method."""
- def __init__(self, config):
- self.config = config
+ config: LintConfig
+ display: Display = field(init=False)
- self.display = display.Display(config)
+ def __post_init__(self):
+ self.display = Display(self.config)
def should_ignore_rule(self, rule):
"""Determines whether a rule should be ignored based on the general list of commits to ignore"""
diff --git a/gitlint-core/gitlint/options.py b/gitlint-core/gitlint/options.py
index ff7d9f1..b3be90e 100644
--- a/gitlint-core/gitlint/options.py
+++ b/gitlint-core/gitlint/options.py
@@ -1,6 +1,8 @@
import os
import re
from abc import abstractmethod
+from dataclasses import dataclass
+from typing import Any
from gitlint.exception import GitlintError
@@ -21,6 +23,7 @@ class RuleOptionError(GitlintError):
pass
+@dataclass
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).
@@ -28,11 +31,12 @@ class RuleOption:
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)
+ name: str
+ value: Any
+ description: str
+
+ def __post_init__(self):
+ self.set(self.value)
@abstractmethod
def set(self, value):
@@ -41,20 +45,17 @@ class RuleOption:
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
-
+@dataclass
class StrOption(RuleOption):
@allow_none
def set(self, value):
self.value = str(value)
+@dataclass
class IntOption(RuleOption):
- def __init__(self, name, value, description, allow_negative=False):
- self.allow_negative = allow_negative
- super().__init__(name, value, description)
+ allow_negative: bool = False
def _raise_exception(self, value):
if self.allow_negative:
@@ -74,6 +75,7 @@ class IntOption(RuleOption):
self._raise_exception(value)
+@dataclass
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):
@@ -83,6 +85,7 @@ class BoolOption(RuleOption):
self.value = value == "true"
+@dataclass
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."""
@@ -96,12 +99,11 @@ class ListOption(RuleOption):
self.value = [str(item.strip()) for item in the_list if item.strip() != ""]
+@dataclass
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)
+ type: str = "dir"
@allow_none
def set(self, value):
@@ -129,6 +131,7 @@ class PathOption(RuleOption):
self.value = os.path.realpath(value)
+@dataclass
class RegexOption(RuleOption):
@allow_none
def set(self, value):
diff --git a/gitlint-core/gitlint/rules.py b/gitlint-core/gitlint/rules.py
index ca4a05b..e316b97 100644
--- a/gitlint-core/gitlint/rules.py
+++ b/gitlint-core/gitlint/rules.py
@@ -1,29 +1,42 @@
import copy
import logging
import re
+from dataclasses import dataclass, field
+from typing import Any, ClassVar, Dict, List, Optional
from gitlint.deprecation import Deprecation
from gitlint.exception import GitlintError
-from gitlint.options import BoolOption, IntOption, ListOption, RegexOption, StrOption
+from gitlint.options import (
+ BoolOption,
+ IntOption,
+ ListOption,
+ RegexOption,
+ RuleOption,
+ StrOption,
+)
+@dataclass
class Rule:
"""Class representing gitlint rules."""
- options_spec = []
- id = None
- name = None
- target = None
- _log = None
- _log_deprecated_regex_style_search = None
+ # Class attributes
+ options_spec: ClassVar[List] = []
+ id: ClassVar[str]
+ name: ClassVar[str]
+ target: ClassVar[Optional["LineRuleTarget"]] = None
+ _log: ClassVar[Optional[logging.Logger]] = None
+ _log_deprecated_regex_style_search: ClassVar[Any]
- def __init__(self, opts=None):
- if not opts:
- opts = {}
+ # Instance attributes
+ _raw_options: Dict[str, str] = field(default_factory=dict, compare=False)
+ options: Dict[str, RuleOption] = field(init=False)
+
+ def __post_init__(self):
self.options = {}
for op_spec in self.options_spec:
self.options[op_spec.name] = copy.deepcopy(op_spec)
- actual_option = opts.get(op_spec.name)
+ actual_option = self._raw_options.get(op_spec.name)
if actual_option is not None:
self.options[op_spec.name].set(actual_option)
@@ -72,20 +85,15 @@ class CommitMessageBody(LineRuleTarget):
"""Target class used for rules that apply to a commit message body"""
+@dataclass
class RuleViolation:
"""Class representing a violation of a rule. I.e.: When a rule is broken, the rule will instantiate this class
to indicate how and where the rule was broken."""
- def __init__(self, rule_id, message, content=None, line_nr=None):
- self.rule_id = rule_id
- self.line_nr = line_nr
- self.message = message
- self.content = content
-
- def __eq__(self, other):
- equal = self.rule_id == other.rule_id and self.message == other.message
- equal = equal and self.content == other.content and self.line_nr == other.line_nr
- return equal
+ rule_id: str
+ message: str
+ content: Optional[str] = None
+ line_nr: Optional[int] = None
def __str__(self):
return f'{self.line_nr}: {self.rule_id} {self.message}: "{self.content}"'
diff --git a/gitlint-core/gitlint/shell.py b/gitlint-core/gitlint/shell.py
index 36160a9..95a91df 100644
--- a/gitlint-core/gitlint/shell.py
+++ b/gitlint-core/gitlint/shell.py
@@ -4,6 +4,7 @@ We still keep the `sh` API and semantics so the rest of the gitlint codebase doe
"""
import subprocess
+from dataclasses import dataclass
from gitlint.utils import TERMINAL_ENCODING
@@ -18,14 +19,14 @@ class CommandNotFound(Exception):
"""Exception indicating a command was not found during execution"""
+@dataclass
class ShResult:
"""Result wrapper class"""
- def __init__(self, full_cmd, stdout, stderr="", exitcode=0):
- self.full_cmd = full_cmd
- self.stdout = stdout
- self.stderr = stderr
- self.exit_code = exitcode
+ full_cmd: str
+ stdout: str
+ stderr: str = ""
+ exit_code: int = 0
def __str__(self):
return self.stdout
diff --git a/gitlint-core/gitlint/tests/rules/test_rules.py b/gitlint-core/gitlint/tests/rules/test_rules.py
index b401372..0e438b6 100644
--- a/gitlint-core/gitlint/tests/rules/test_rules.py
+++ b/gitlint-core/gitlint/tests/rules/test_rules.py
@@ -8,12 +8,17 @@ class RuleTests(BaseTestCase):
self.assertEqual(str(RuleViolation("rule-ïd", "Tēst message", "Tēst content", 57)), expected)
def test_rule_equality(self):
- self.assertEqual(Rule(), Rule())
- # Ensure rules are not equal if they differ on their attributes
- for attr in ["id", "name", "target", "options"]:
- rule = Rule()
- setattr(rule, attr, "åbc")
- self.assertNotEqual(Rule(), rule)
+ # Ensure rules are not equal if they differ on one of their attributes
+ rule_attrs = ["id", "name", "target", "options"]
+ for attr in rule_attrs:
+ rule1 = Rule()
+ rule2 = Rule()
+ for attr2 in rule_attrs:
+ setattr(rule1, attr2, "föo")
+ setattr(rule2, attr2, "föo")
+ self.assertEqual(rule1, rule2)
+ setattr(rule1, attr, "åbc")
+ self.assertNotEqual(rule1, rule2)
def test_rule_log(self):
rule = Rule()