Source code for glob_linters.utils.settings

"""Configuration"""
# Configuration vairables and defaults
# pylint: disable=subprocess-run-check
import argparse
import configparser
import logging
import os
import re
import subprocess
import sys
from dataclasses import dataclass
from typing import Callable, ClassVar

from glob_linters import linters

# Default config file
DEFAULT_CONFIG_FILE_PATH: str = ".github/glob-linters.ini"
MYPY_PACKAGE_REQUIREMENTS_FILE_PATH: str = os.path.abspath(
    ".github/mypy_requirements.txt"
)

# User-defined linter config directory for GitHub action
USER_DEFIEND_LINTER_CONFIG_ROOT: str = os.path.abspath(".github/linter-configs")

# Supported file extension/suffix
SUPPORTED_FILE_SUFFIX: list[str] = [".cpp", ".py"]

logger = logging.getLogger(__name__)


[docs]@dataclass class Configs: """Class to store configs/settings""" # Exit code record return_code: ClassVar[int] = 0 # Indicator of reading file has_read_config_file: ClassVar[bool] = False # Lint targets settings target_dir: ClassVar[str] = "." target_suffix: ClassVar[list[str]] = SUPPORTED_FILE_SUFFIX # Running mode debug: ClassVar[bool] = False # Linter dict for running # Clang Linters cpplint: ClassVar[linters.Cpplint] = linters.Cpplint("cpplint") clang_format: ClassVar[linters.ClangFormat] = linters.ClangFormat("clang-format") # Python pylint: ClassVar[linters.Pylint] = linters.Pylint("pylint") flake8: ClassVar[linters.Flake8] = linters.Flake8("flake8") black: ClassVar[linters.Black] = linters.Black("black") isort: ClassVar[linters.Isort] = linters.Isort("isort") mypy: ClassVar[linters.Mypy] = linters.Mypy("mypy") # Linters needed for each file extension linters_enabled: ClassVar[dict[str, list[str]]] = { ".cpp": ["cpplint", "clang_format"], ".py": ["pylint", "flake8", "black", "isort", "mypy"], } # Available configs set_configs: ClassVar[dict[str, dict[str, Callable]]] = { "target": { "target_dir": lambda x: setattr(Configs, "target_dir", x), "target_suffix": lambda x: setattr( Configs, "target_suffix", re.split(r"[,\s]", x) ), }, "executable": { # Clang "cpplint": lambda x: setattr(Configs.cpplint, "executable", x), "cpplint.options": lambda x: setattr( Configs.cpplint, "options", Configs.cpplint.options + re.split(r"[,\s]", x), ), "clang_format": lambda x: setattr(Configs.clang_format, "executable", x), "clang_format.options": lambda x: setattr( Configs.clang_format, "options", Configs.clang_format.options + re.split(r"[,\s]", x), ), # Python "pylint": lambda x: setattr(Configs.pylint, "executable", x), "pylint.options": lambda x: setattr( Configs.pylint, "options", Configs.pylint.options + re.split(r"[,\s]", x), ), "flake8": lambda x: setattr(Configs.flake8, "executable", x), "flake8.options": lambda x: setattr( Configs.flake8, "options", Configs.flake8.options + re.split(r"[,\s]", x), ), "black": lambda x: setattr(Configs.black, "executable", x), "black.options": lambda x: setattr( Configs.black, "options", Configs.black.options + re.split(r"[,\s]", x) ), "isort": lambda x: setattr(Configs.isort, "executable", x), "isort.options": lambda x: setattr( Configs.isort, "options", Configs.isort.options + re.split(r"[,\s]", x) ), "mypy": lambda x: setattr(Configs.mypy, "executable", x), "mypy.options": lambda x: setattr( Configs.mypy, "options", Configs.mypy.options + re.split(r"[,\s]", x) ), }, "env": { "debug": lambda x: setattr(Configs, "debug", x), ".cpp.linters": lambda x: getattr(Configs, "linters_enabled").update( {".cpp": re.split(r",\s", x)} ), ".cpp.disable_linters": lambda x: getattr( Configs, "linters_enabled" ).update( { ".cpp": [ value for value in Configs.linters_enabled[".cpp"] if value not in re.split(r"[,\s]", x) ] } ), ".py.linters": lambda x: getattr(Configs, "linters_enabled").update( {".py": re.split(r"[,\s]", x)} ), ".py.disable_linters": lambda x: getattr(Configs, "linters_enabled").update( { ".py": [ value for value in Configs.linters_enabled[".py"] if value not in re.split(r"[,\s]", x) ] } ), }, "_set-config-file": { "cpplint": lambda: os.symlink( os.path.join(USER_DEFIEND_LINTER_CONFIG_ROOT, "CPPLINT.cfg"), "CPPLINT.cfg", ) if os.path.exists( os.path.join(USER_DEFIEND_LINTER_CONFIG_ROOT, "CPPLINT.cfg") ) else -1, "clang_format": lambda: os.symlink( os.path.join(USER_DEFIEND_LINTER_CONFIG_ROOT, ".clang-format"), ".clang-format", ) if os.path.exists( os.path.join(USER_DEFIEND_LINTER_CONFIG_ROOT, ".clang-format") ) else -1, "pylint": lambda: Configs.pylint.options.extend( ["--rcfile", os.path.join(USER_DEFIEND_LINTER_CONFIG_ROOT, ".pylintrc")] ) if os.path.exists( os.path.join(USER_DEFIEND_LINTER_CONFIG_ROOT, ".pylintrc") ) else -1, "flake8": lambda: Configs.flake8.options.extend( ["--config", os.path.join(USER_DEFIEND_LINTER_CONFIG_ROOT, ".flake8")] ) if os.path.exists(os.path.join(USER_DEFIEND_LINTER_CONFIG_ROOT, ".flake8")) else -1, "black": lambda: Configs.black.options.extend( ["--config", os.path.join(USER_DEFIEND_LINTER_CONFIG_ROOT, ".black")] ) if os.path.exists(os.path.join(USER_DEFIEND_LINTER_CONFIG_ROOT, ".black")) else -1, "isort": lambda: Configs.isort.options.extend( [ "--settings-file", os.path.join(USER_DEFIEND_LINTER_CONFIG_ROOT, ".isort.cfg"), ] ) if os.path.exists( os.path.join(USER_DEFIEND_LINTER_CONFIG_ROOT, ".isort.cfg") ) else -1, "mypy": lambda: Configs.mypy.options.extend( [ "--config-file", os.path.join(USER_DEFIEND_LINTER_CONFIG_ROOT, ".mypy.ini"), ] ) if os.path.exists( os.path.join(USER_DEFIEND_LINTER_CONFIG_ROOT, ".mypy.ini") ) else -1, }, }
[docs]def parse_config_file(config_file: str) -> None: """Parse configuration from configparser-like file Parameters ---------- config_file : str Configuration file path Raises ------ ValueError Invalid section/option found in config file """ config_parser = configparser.ConfigParser() config_parser.read(config_file) for section in config_parser: if section == "DEFAULT": continue if section not in Configs.set_configs: raise ValueError(f"No such section supported: {section}") for option in config_parser[section]: if option not in config_parser[section]: raise ValueError(f"No such option supported: {option}") if option == "target_suffix": diff = set(re.split(r"[,\s]", config_parser[section][option])) - set( SUPPORTED_FILE_SUFFIX ) if len(diff) > 0: raise ValueError(f"No supported file suffix: {diff}") Configs.set_configs[section][option](config_parser[section][option])
[docs]def parse_args(args: argparse.Namespace) -> None: """Parse command line arguments Parameters ---------- args : argparse.Namespace Arguments returned by `argparse.parse_args` Raises ------ ValueError Invalid linter name found """ if os.path.exists(args.config_file): parse_config_file(args.config_file) else: Configs.debug = args.debug Configs.target_dir = args.target_dir Configs.target_suffix = re.split(r"\s", args.target_suffix) if args.configs is not None: for key, value in map(lambda x: re.split("=", x), args.configs): if key in Configs.set_configs["executable"]: if "options" in key: Configs.set_configs["executable"][key](value) else: Configs.set_configs["executable"][key](value) elif key in Configs.set_configs["env"]: Configs.set_configs["env"][key](value) else: raise ValueError(f"No such linter supported: {key}")
[docs]def install_mypy_package_requirements() -> None: """Install additional packages for mypy linting""" if not os.path.exists(MYPY_PACKAGE_REQUIREMENTS_FILE_PATH): logger.info("Not found mypy_requirements.txt, skip package installation") return logger.info("Install packages for mypy checking...") cmd = ["pip", "install", "-r", MYPY_PACKAGE_REQUIREMENTS_FILE_PATH] logger.debug("Install command: %s", " ".join(cmd)) cmd_result = subprocess.run(cmd, capture_output=True) logger.debug("Installation output:") for line in cmd_result.stdout.decode().strip().split("\n"): logger.debug("\t%s", line) if cmd_result.returncode != 0: logger.error("Package installaltion failed:") for line in cmd_result.stderr.decode().strip().split("\n"): logger.error("\t%s", line) sys.exit(1)
[docs]def load_linter_configs() -> None: """Load config file for each enabled linter""" for ext in Configs.target_suffix: for linter_name in Configs.linters_enabled[ext]: logger.debug("Setting linter config for [%s]", linter_name) # Check whether set config file # If set, return None, -1 otherwise return_value = Configs.set_configs["_set-config-file"][linter_name]() if return_value is None: getattr(Configs, linter_name).use_config_file = True