Source code for mlx.warnings.warnings_checker

# SPDX-License-Identifier: Apache-2.0

import abc
import logging
import os
import re
from math import inf
from string import Template

from .exceptions import WarningsConfigError


def substitute_envvar(checker_config, keys):
    """Modifies configuration for checker inplace, resolving any environment variables for ``keys``

    Args:
        checker_config (dict): Configuration for a specific WarningsChecker
        keys (set): Set of keys to process the value of

    Raises:
        WarningsConfigError: Failed to find an environment variable
    """
    for key in keys:
        if key in checker_config and isinstance(checker_config[key], str):
            template_obj = Template(checker_config[key])
            try:
                checker_config[key] = template_obj.substitute(os.environ)
            except KeyError as err:
                raise WarningsConfigError(f"Failed to find environment variable {err} for configuration value {key!r}")\
                    from None


class DebugOnlyFilter(logging.Filter):
    def filter(self, record: logging.LogRecord) -> bool:
        if record.levelno <= logging.DEBUG:
            return True
        return False


[docs] class WarningsChecker: name = "checker" logging_fmt = "{checker.name_repr}: {message}" def __init__(self, verbose, output): """Constructor The logging is configured. A handler is added only if a parent checker hasn't done this already. A parent-checker uses the same logger as its sub-checkers, but each with their own LoggerAdapter. Args: verbose (bool): Enable/disable verbose logging output (Path/None): The path to the output file """ self.count = 0 self._minimum = 0 self._maximum = 0 self._cq_findings = [] self.cq_enabled = False self.cq_default_path = ".gitlab-ci.yml" self._cq_description_template = Template("$description") self.exclude_patterns = [] self.include_patterns = [] self.logging_args = (verbose, output) self.logger = logging.getLogger(self.name) self.logger.setLevel(logging.WARNING) if output: self.logger.setLevel(logging.DEBUG) elif verbose: self.logger.setLevel(logging.INFO) formatter = logging.Formatter(fmt=self.logging_fmt, style="{") if not self.logger.handlers: self.logger.propagate = True # Propagate to parent loggers handler = logging.StreamHandler() handler.setFormatter(formatter) if verbose: handler.setLevel(logging.INFO) else: handler.setLevel(logging.WARNING) self.logger.addHandler(handler) if output is not None: handler = logging.FileHandler(output, "a") handler.setFormatter(formatter) handler.setLevel(logging.DEBUG) handler.addFilter(DebugOnlyFilter()) self.logger.addHandler(handler) logging_vars = {"checker": self} self.logger = logging.LoggerAdapter(self.logger, extra=logging_vars) @property def name_repr(self): return self.name.replace("_sub", "").capitalize() @property def is_sub_checker(self): return self.name.endswith("_sub") @property def cq_findings(self): """List[dict]: list of code quality findings""" return self._cq_findings @property def cq_description_template(self): """Template: string.Template instance based on the configured template string""" return self._cq_description_template @cq_description_template.setter def cq_description_template(self, template_obj): try: template_obj.template = template_obj.substitute(os.environ, description="$description") except KeyError as err: raise WarningsConfigError(f"Failed to find environment variable from configuration value " f"'cq_description_template': {err}") from err self._cq_description_template = template_obj @property def maximum(self): """Getter function for the maximum amount of warnings Returns: int: Maximum amount of warnings """ return self._maximum @maximum.setter def maximum(self, maximum): if maximum == -1: maximum = inf if self._minimum > maximum: raise ValueError("Invalid argument: maximum limit must be higher than minimum limit ({min}); cannot " "set {max}.".format(max=maximum, min=self._minimum)) self._maximum = maximum @property def minimum(self): """Getter function for the minimum amount of warnings Returns: int: Minimum amount of warnings """ return self._minimum @minimum.setter def minimum(self, minimum): if minimum > self._maximum: raise ValueError("Invalid argument: minimum limit must be lower than maximum limit ({max}); cannot " "set {min}.".format(min=minimum, max=self._maximum)) self._minimum = minimum
[docs] @abc.abstractmethod def check(self, content): """Function for counting the number of warnings in a specific text Args: content (str): The content to parse """ return
[docs] def add_patterns(self, regexes, pattern_container): """Adds regexes as patterns to the specified container Args: regexes (list[str]|None): List of regexes to add pattern_container (list[re.Pattern]): Target storage container for patterns """ if regexes: if not isinstance(regexes, list): raise TypeError("Expected a list value for exclude key in configuration file; got {}" .format(regexes.__class__.__name__)) for regex in regexes: pattern_container.append(re.compile(regex))
[docs] def return_count(self): """Getter function for the amount of warnings found Returns: int: Number of warnings found """ return self.count
[docs] def return_check_limits(self): """Function for checking whether the warning count is within the configured limits A checker instance with sub-checkers is responsible for printing 'Returning error code X.' when the exit code is not 0. Returns: int: 0 if the amount of warnings is within limits, the count of (the sum of sub-checker) warnings otherwise (or 1 in case of a count of 0 warnings) """ if self.count > self._maximum or self.count < self._minimum: return self._return_error_code() elif self._minimum == self._maximum and self.count == self._maximum: msg = f"number of warnings ({self.count}) is exactly as expected. Well done." else: msg = f"number of warnings ({self.count}) is between limits {self._minimum} and {self._maximum}. Well done." self.logger.warning(msg) return 0
def _return_error_code(self): """Function for determining the return code and message on failure Returns: int: The count of warnings (or 1 in case of a count of 0 warnings) """ if self.count > self._maximum: error_reason = f"higher than the maximum limit ({self._maximum})" else: error_reason = f"lower than the minimum limit ({self._minimum})" error_code = self.count if error_code == 0: error_code = 1 string_to_print = f"number of warnings ({self.count}) is {error_reason}." if not self.is_sub_checker: string_to_print += f" Returning error code {error_code}." self.logger.warning(string_to_print) return error_code
[docs] def parse_config(self, config): substitute_envvar(config, {"min", "max"}) self.maximum = int(config["max"]) self.minimum = int(config["min"]) self.add_patterns(config.get("exclude"), self.exclude_patterns) if "cq_default_path" in config: self.cq_default_path = config["cq_default_path"] if "cq_description_template" in config: self.cq_description_template = Template(config["cq_description_template"])
def _is_excluded(self, content): """Checks if the specific text must be excluded based on the configured regexes for exclusion and inclusion. Inclusion has priority over exclusion. Args: content (str): The content to parse Returns: bool: True for exclusion, False for inclusion """ matching_exclude_pattern = self._search_patterns(content, self.exclude_patterns) if not self._search_patterns(content, self.include_patterns) and matching_exclude_pattern: self.logger.info(f"Excluded {content!r} because of configured regex {matching_exclude_pattern!r}") return True return False @staticmethod def _search_patterns(content, patterns): """Returns the regex of the first pattern that matches specified content, None if nothing matches""" for pattern in patterns: if pattern.search(content): return pattern.pattern return None