Source code for hammurabi.law

"""
This module contains the definition of Law which is responsible for
the execution of its registered Rules. Every Law can have multiple rules to execute.

In case a rule raises an exception the execution may abort and none of
the remaining rules will be executed neither pipes or children. An abort
can cause an inconsistent state or a dirty git branch. If ``rule_can_abort``
config is set to True, the whole execution of the :class:``hammurabi.pillar.Pillar``
will be aborted and the original exception will be re-raised.
"""

import logging
from typing import Iterable, List, Tuple, Union

from hammurabi.config import config
from hammurabi.exceptions import AbortLawError, PreconditionFailedError
from hammurabi.helpers import full_strip
from hammurabi.mixins import GitMixin
from hammurabi.rules.base import Precondition, Rule


[docs]class Law(GitMixin): """ A Law is a collection of Rules which is responsible for the rule execution and git committing. Example usage: .. code-block:: python >>> from pathlib import Path >>> from hammurabi import Law, Pillar, FileExists >>> >>> example_law = Law( >>> name="Name of the law", >>> description="Well detailed description what this law does.", >>> rules=( >>> FileExists( >>> name="Create pyproject.toml", >>> path=Path("./pyproject.toml") >>> ), >>> ) >>> ) >>> >>> pillar = Pillar() >>> pillar.register(example_law) """ def __init__( self, name: str, description: str, rules: Iterable[Rule], preconditions: Iterable[Precondition] = (), ) -> None: """ :param name: Name of the law :type name: str :param description: Detailed description what kind of rules are included :type description: str :param rules: List of those rules which should be included in the law :type rules: Iterable[Rule] """ super().__init__() self.name = name.strip() self.description = full_strip(description) self.rules: Iterable[Rule] = tuple() self.preconditions = preconditions self._failed_rules: Tuple[Rule, ...] = tuple() for rule in rules: self.rules += (rule,) def __repr__(self) -> str: return ( "Law(" f'name="{self.name}", ' f'description="{self.description}", ' f"rules={self.rules}, " f"preconditions={self.preconditions})" ) def __str__(self) -> str: return f"{self.name} law" @property def passed_rules(self) -> Tuple[Rule, ...]: """ Return the rules which did modifications and not failed. :return: Return the passed rules :rtype: Tuple[Rule, ...] """ return tuple(r for r in self.rules if r.made_changes) @property def failed_rules(self) -> Tuple[Rule, ...]: """ Return the rules which did modifications and failed. :return: Return the failed rules :rtype: Union[Tuple[()], Tuple[Rule]] """ return self._failed_rules @property def skipped_rules(self) -> Tuple[Rule, ...]: """ Return the rules which neither modified the code nor failed. :return: Return the skipped rules :rtype: Tuple[Rule, ...] """ def is_skipped(rule) -> bool: """ Return the evaluation if the rule is skipped or not. :return: Evaluation if the rule is skipped :rtype: bool """ not_passed = rule not in self.passed_rules not_failed = rule not in self.failed_rules return not_passed and not_failed return tuple(r for r in self.rules if is_skipped(r)) @property def documentation(self) -> str: """ Get the name and description of the Law object. :return: Return the name and description of the law as its documentation :rtype: str """ return f"{self.name}\n{self.description}" @property def can_proceed(self) -> bool: """ Evaluate if the execution can be continued. If preconditions are set, those will be evaluated by this method. :return: Return with the result of evaluation :rtype: bool .. warning:: :func:`hammurabi.rules.base.Rule.can_proceed` checks the result of ``self.preconditions``, which means the preconditions are executed. Make sure that you are not doing any modifications within rules used as preconditions, otherwise take extra attention for those rules. """ logging.debug('Checking if "%s" can proceed with execution', self.name) return all([condition.execute() for condition in self.preconditions])
[docs] def get_execution_order(self) -> List[Union[Rule, Precondition]]: """ Get the execution order of the registered rules. The order will contain the pipes and children as well. This helper function is useful in debugging and information gathering. :return: Return the execution order of the rules :rtype: List[Rule] """ order: List[Union[Rule, Precondition]] = list() for rule in self.rules: order.extend(rule.get_execution_order()) return order
[docs] def commit(self) -> None: """ Commit the changes made by registered rules and add a meaningful commit message. Example commit message: .. code-block:: text Migrate to next generation project template * Create pyproject.toml * Add meta info from setup.py to pyproject.toml * Add existing dependencies * Remove requirements.txt * Remove setup.py """ if not self.passed_rules: return order = self.get_execution_order() rules = [f"* {rule.name}" for rule in order if rule.made_changes] rules_commit_message = "\n".join(rules) self.git_commit(f"{self.documentation}\n\n{rules_commit_message}")
@staticmethod def __execute_rule_chain(rule: Rule) -> None: """ Execute the given rule. In case of an exception, the execution of rules will continue except the failing one. The failed rule's pipe and children will not be executed. :param rule: A registered rule :type rule: Rule :raises: ``AbortLawError`` """ try: rule.execute() except PreconditionFailedError: logging.warning( 'Cancelling execution of "%s", the preconditions are not fulfilled', rule.name, ) except Exception as exc: # pylint: disable=broad-except logging.error('Execution of "%s" is aborted: %s', rule.name, str(exc)) chained_rules = filter( lambda r: isinstance(r, Rule), rule.get_rule_chain(rule) ) for chain in chained_rules: logging.error('Due to errors "%s" is aborted', chain.name) raise AbortLawError(str(exc)) from exc def __execute_rule(self, rule: Rule) -> None: """ Execute a rule registered for the law. In case of an exception the exception will be re-raised. """ try: self.__execute_rule_chain(rule) except AbortLawError as exc: logging.error(str(exc)) self._failed_rules += (rule,) if config.settings.rule_can_abort: raise exc
[docs] def enforce(self) -> None: """ Execute all registered rule. If ``rule_can_abort`` config option is set to ``True``, all the rules will be aborted and an exception will be raised. When the whole execution chain is finished, the changes will be committed except the failed ones. .. note:: Failed rules and their chain (excluding prerequisites) will be added to the pull request description. :raises: ``AbortLawError`` """ if not self.can_proceed: logging.warning( 'Cancelling execution of "%s", the preconditions are not fulfilled', self.name, ) return logging.info('Executing law "%s"', self.name) for rule in self.rules: self.__execute_rule(rule) logging.info('Committing changes made by "%s"', self.name) self.commit()