Skip to content

Commit

Permalink
Make linter configuration a dataclass
Browse files Browse the repository at this point in the history
  • Loading branch information
ssbarnea committed Apr 24, 2023
1 parent 174f95c commit e57816f
Show file tree
Hide file tree
Showing 14 changed files with 109 additions and 89 deletions.
6 changes: 2 additions & 4 deletions src/ansiblelint/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,14 @@
reconfigure,
render_yaml,
)
from ansiblelint.config import get_version_warning, log_entries, options
from ansiblelint.config import Options, get_version_warning, log_entries, options
from ansiblelint.constants import GIT_CMD, RC
from ansiblelint.file_utils import abspath, cwd, normpath
from ansiblelint.loaders import load_ignore_txt
from ansiblelint.skip_utils import normalize_tag
from ansiblelint.version import __version__

if TYPE_CHECKING:
from argparse import Namespace

# RulesCollection must be imported lazily or ansible gets imported too early.
from ansiblelint.rules import RulesCollection
from ansiblelint.runner import LintResult
Expand Down Expand Up @@ -170,7 +168,7 @@ def _do_list(rules: RulesCollection) -> int:


# noinspection PyShadowingNames
def _do_transform(result: LintResult, opts: Namespace) -> None:
def _do_transform(result: LintResult, opts: Options) -> None:
"""Create and run Transformer."""
if "yaml" in opts.skip_list:
# The transformer rewrites yaml files, but the user requested to skip
Expand Down
8 changes: 3 additions & 5 deletions src/ansiblelint/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,14 @@
from ansiblelint import formatters
from ansiblelint._mockings import _perform_mockings
from ansiblelint.color import console, console_stderr, render_yaml
from ansiblelint.config import PROFILES, get_version_warning
from ansiblelint.config import PROFILES, Options, get_version_warning
from ansiblelint.config import options as default_options
from ansiblelint.constants import RC, RULE_DOC_URL
from ansiblelint.errors import MatchError
from ansiblelint.loaders import IGNORE_FILE
from ansiblelint.stats import SummarizedResults, TagStats

if TYPE_CHECKING:
from argparse import Namespace

from ansiblelint._internal.rules import BaseRule
from ansiblelint.file_utils import Lintable
from ansiblelint.runner import LintResult
Expand All @@ -35,7 +33,7 @@
class App:
"""App class represents an execution of the linter."""

def __init__(self, options: Namespace):
def __init__(self, options: Options):
"""Construct app run based on already loaded configuration."""
options.skip_list = _sanitize_list_options(options.skip_list)
options.warn_list = _sanitize_list_options(options.warn_list)
Expand Down Expand Up @@ -332,7 +330,7 @@ def report_summary( # pylint: disable=too-many-branches,too-many-locals


def choose_formatter_factory(
options_list: Namespace,
options_list: Options,
) -> type[formatters.BaseFormatter[Any]]:
"""Select an output formatter based on the incoming command line arguments."""
r: type[formatters.BaseFormatter[Any]] = formatters.Formatter
Expand Down
31 changes: 19 additions & 12 deletions src/ansiblelint/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
from pathlib import Path
from typing import Any, Callable

from ansiblelint.config import DEFAULT_KINDS, DEFAULT_WARN_LIST, PROFILES, log_entries
from ansiblelint.config import (
DEFAULT_KINDS,
DEFAULT_WARN_LIST,
PROFILES,
Options,
log_entries,
)
from ansiblelint.constants import (
CUSTOM_RULESDIR_ENVVAR,
DEFAULT_RULESDIR,
Expand Down Expand Up @@ -56,13 +62,13 @@ def expand_to_normalized_paths(
config[paths_var] = normalized_paths


def load_config(config_file: str) -> dict[Any, Any]:
def load_config(config_file: str | None) -> tuple[dict[Any, Any], str | None]:
"""Load configuration from disk."""
config_path = None

if config_file == "/dev/null":
_logger.debug("Skipping config file as it was set to /dev/null")
return {}
return {}, config_file

if config_file:
config_path = os.path.abspath(config_file)
Expand All @@ -72,7 +78,7 @@ def load_config(config_file: str) -> dict[Any, Any]:
config_path = config_path or get_config_path()
if not config_path or not os.path.exists(config_path):
# a missing default config file should not trigger an error
return {}
return {}, None

config_lintable = Lintable(
config_path,
Expand All @@ -91,7 +97,7 @@ def load_config(config_file: str) -> dict[Any, Any]:
config_dir = os.path.dirname(config_path)
expand_to_normalized_paths(config, config_dir)

return config
return config, config_path


def get_config_path(config_file: str | None = None) -> str | None:
Expand Down Expand Up @@ -459,7 +465,7 @@ def get_cli_parser() -> argparse.ArgumentParser:
return parser


def merge_config(file_config: dict[Any, Any], cli_config: Namespace) -> Namespace:
def merge_config(file_config: dict[Any, Any], cli_config: Options) -> Options:
"""Combine the file config with the CLI args."""
bools = (
"display_relative_path",
Expand Down Expand Up @@ -546,10 +552,10 @@ def merge_config(file_config: dict[Any, Any], cli_config: Namespace) -> Namespac
return cli_config


def get_config(arguments: list[str]) -> Namespace:
def get_config(arguments: list[str]) -> Options:
"""Extract the config based on given args."""
parser = get_cli_parser()
options = parser.parse_args(arguments)
options = Options(**vars(parser.parse_args(arguments)))

# docs is not document, being used for internal documentation building
if options.list_rules and options.format not in [
Expand All @@ -565,12 +571,13 @@ def get_config(arguments: list[str]) -> Namespace:
)

# save info about custom config file, as options.config_file may be modified by merge_config

file_config = load_config(options.config_file)

file_config, options.config_file = load_config(options.config_file)
config = merge_config(file_config, options)

options.rulesdirs = get_rules_dirs(options.rulesdir, options.use_default_rules)
options.rulesdirs = get_rules_dirs(
[str(r) for r in options.rulesdir],
options.use_default_rules,
)

if not options.project_dir:
project_dir, method = find_project_root(
Expand Down
99 changes: 60 additions & 39 deletions src/ansiblelint/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
import time
import urllib.request
import warnings
from argparse import Namespace
from dataclasses import dataclass, field
from functools import lru_cache
from pathlib import Path
from typing import Any
from urllib.error import HTTPError, URLError

from filelock import FileLock
from packaging.version import Version

from ansiblelint import __version__
Expand Down Expand Up @@ -94,44 +95,64 @@

LOOP_VAR_PREFIX = "^(__|{role}_)"

options = Namespace(
cache_dir=None,
colored=True,
configured=False,
cwd=".",
display_relative_path=True,
exclude_paths=[],
format="brief",
lintables=[],
list_rules=False,
list_tags=False,
write_list=[],
parseable=False,
quiet=False,
rulesdirs=[],
skip_list=[],
tags=[],
verbosity=False,
warn_list=[],
kinds=DEFAULT_KINDS,
mock_filters=[],
mock_modules=[],
mock_roles=[],
loop_var_prefix=None,
only_builtins_allow_collections=[],
only_builtins_allow_modules=[],
var_naming_pattern=None,
offline=False,
project_dir=".", # default should be valid folder (do not use None here)
extra_vars=None,
enable_list=[],
skip_action_validation=True,
strict=False,
rules={}, # Placeholder to set and keep configurations for each rule.
profile=None,
task_name_prefix="{stem} | ",
sarif_file=None,
)

@dataclass
class Options: # pylint: disable=too-many-instance-attributes,too-few-public-methods
"""Store ansible-lint effective configuration options."""

cache_dir: str | None = None
colored: bool = True
configured: bool = False
cwd: Path = Path(".")
display_relative_path: bool = True
exclude_paths: list[str] = field(default_factory=list)
format: str = "brief"
lintables: list[str] = field(default_factory=list)
list_rules: bool = False
list_tags: bool = False
write_list: list[str] = field(default_factory=list)
parseable: bool = False
quiet: bool = False
rulesdirs: list[str] = field(default_factory=list)
skip_list: list[str] = field(default_factory=list)
tags: list[str] = field(default_factory=list)
verbosity: int = 0
warn_list: list[str] = field(default_factory=list)
kinds = DEFAULT_KINDS
mock_filters: list[str] = field(default_factory=list)
mock_modules: list[str] = field(default_factory=list)
mock_roles: list[str] = field(default_factory=list)
loop_var_prefix: str | None = None
only_builtins_allow_collections: list[str] = field(default_factory=list)
only_builtins_allow_modules: list[str] = field(default_factory=list)
var_naming_pattern: str | None = None
offline: bool = False
project_dir: str = "." # default should be valid folder (do not use None here)
extra_vars: dict | None = None
enable_list: list[str] = field(default_factory=list)
skip_action_validation: bool = True
strict: bool = False
rules: dict[
str,
Any,
] = field(
default_factory=dict
) # Placeholder to set and keep configurations for each rule.
profile: str | None = None
task_name_prefix: str = "{stem} | "
sarif_file: Path = None
config_file: str | None = None
generate_ignore: bool = False
rulesdir: list[Path] = field(default_factory=list)
progressive: bool = False
cache_dir_lock: FileLock | None = None
use_default_rules: bool = False
version: bool = False # display version command
list_profiles: bool = False # display profiles command
ignore_file: Path | None = None


options = Options()

# Used to store detected tag deprecations
used_old_tags: dict[str, str] = {}
Expand Down
5 changes: 2 additions & 3 deletions src/ansiblelint/file_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import os
import subprocess
import sys
from argparse import Namespace
from collections import OrderedDict, defaultdict
from collections.abc import Iterator, Sequence
from contextlib import contextmanager
Expand All @@ -18,7 +17,7 @@
from wcmatch.wcmatch import RECURSIVE, WcMatch
from yaml.error import YAMLError

from ansiblelint.config import BASE_KINDS, options
from ansiblelint.config import BASE_KINDS, Options, options
from ansiblelint.constants import CONFIG_FILENAMES, GIT_CMD, FileType, States
from ansiblelint.logger import warn_or_fail

Expand Down Expand Up @@ -407,7 +406,7 @@ def data(self) -> Any:


# pylint: disable=redefined-outer-name
def discover_lintables(options: Namespace) -> dict[str, Any]:
def discover_lintables(options: Options) -> dict[str, Any]:
"""Find all files that we know how to lint.
Return format is normalized, relative for stuff below cwd, ~/ for content
Expand Down
5 changes: 2 additions & 3 deletions src/ansiblelint/rules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import logging
import re
import sys
from argparse import Namespace
from collections import defaultdict
from collections.abc import Iterable, Iterator, MutableMapping, MutableSequence
from functools import lru_cache
Expand All @@ -26,7 +25,7 @@
RuntimeErrorRule,
WarningRule,
)
from ansiblelint.config import PROFILES, get_rule_config
from ansiblelint.config import PROFILES, Options, get_rule_config
from ansiblelint.config import options as default_options
from ansiblelint.constants import LINE_NUMBER_KEY, RULE_DOC_URL, SKIPPED_RULES_KEY
from ansiblelint.errors import MatchError
Expand Down Expand Up @@ -373,7 +372,7 @@ class RulesCollection:
def __init__(
self,
rulesdirs: list[str] | None = None,
options: Namespace = default_options,
options: Options = default_options,
profile_name: str | None = None,
conditional: bool = True,
) -> None:
Expand Down
7 changes: 3 additions & 4 deletions src/ansiblelint/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,13 @@
import ansiblelint.skip_utils
import ansiblelint.utils
from ansiblelint._internal.rules import LoadingFailureRule
from ansiblelint.config import Options
from ansiblelint.constants import States
from ansiblelint.errors import MatchError
from ansiblelint.file_utils import Lintable, expand_dirs_in_lintables
from ansiblelint.rules.syntax_check import AnsibleSyntaxCheckRule

if TYPE_CHECKING:
from argparse import Namespace

from ansiblelint.rules import RulesCollection

_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -212,7 +211,7 @@ def _emit_matches(self, files: list[Lintable]) -> Generator[MatchError, None, No
visited.add(lintable)


def _get_matches(rules: RulesCollection, options: Namespace) -> LintResult:
def _get_matches(rules: RulesCollection, options: Options) -> LintResult:
lintables = ansiblelint.utils.get_lintables(opts=options, args=options.lintables)

for rule in rules:
Expand All @@ -227,7 +226,7 @@ def _get_matches(rules: RulesCollection, options: Namespace) -> LintResult:
runner = Runner(
*lintables,
rules=rules,
tags=options.tags,
tags=frozenset(options.tags),
skip_list=options.skip_list,
exclude_paths=options.exclude_paths,
verbosity=options.verbosity,
Expand Down
7 changes: 3 additions & 4 deletions src/ansiblelint/testing/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@

import copy
import os
from argparse import Namespace
from collections.abc import Iterator

import pytest
from _pytest.fixtures import SubRequest

from ansiblelint.config import options # noqa: F401
from ansiblelint.config import Options, options # noqa: F401
from ansiblelint.constants import DEFAULT_RULESDIR
from ansiblelint.rules import RulesCollection
from ansiblelint.testing import RunFromText
Expand All @@ -37,7 +36,7 @@ def default_text_runner(default_rules_collection: RulesCollection) -> RunFromTex


@pytest.fixture()
def rule_runner(request: SubRequest, config_options: Namespace) -> RunFromText:
def rule_runner(request: SubRequest, config_options: Options) -> RunFromText:
"""Return runner for a specific rule class."""
rule_class = request.param
config_options.enable_list.append(rule_class().id)
Expand All @@ -47,7 +46,7 @@ def rule_runner(request: SubRequest, config_options: Namespace) -> RunFromText:


@pytest.fixture(name="config_options")
def fixture_config_options() -> Iterator[Namespace]:
def fixture_config_options() -> Iterator[Options]:
"""Return configuration options that will be restored after testrun."""
global options # pylint: disable=global-statement,invalid-name
original_options = copy.deepcopy(options)
Expand Down
Loading

0 comments on commit e57816f

Please sign in to comment.