From ba233a0cbad76b4783a03893e7bf4716fbc0f0ec Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 26 Jun 2024 08:24:58 +0200 Subject: Merging upstream version 24.6.1. Signed-off-by: Daniel Baumann --- src/ansiblelint/__main__.py | 172 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 138 insertions(+), 34 deletions(-) (limited to 'src/ansiblelint/__main__.py') diff --git a/src/ansiblelint/__main__.py b/src/ansiblelint/__main__.py index af434d0..ca4a33b 100755 --- a/src/ansiblelint/__main__.py +++ b/src/ansiblelint/__main__.py @@ -30,12 +30,23 @@ import shutil import site import sys from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, TextIO +from typing import TYPE_CHECKING, Any, TextIO from ansible_compat.prerun import get_cache_dir from filelock import FileLock, Timeout from rich.markup import escape +from ansiblelint.constants import RC, SKIP_SCHEMA_UPDATE + +# safety check for broken ansible core, needs to happen first +try: + # pylint: disable=unused-import + from ansible.parsing.dataloader import DataLoader # noqa: F401 + +except Exception as _exc: # pylint: disable=broad-exception-caught # noqa: BLE001 + logging.fatal(_exc) + sys.exit(RC.INVALID_CONFIG) +# pylint: disable=ungrouped-imports from ansiblelint import cli from ansiblelint._mockings import _perform_mockings_cleanup from ansiblelint.app import get_app @@ -53,20 +64,21 @@ from ansiblelint.config import ( log_entries, options, ) -from ansiblelint.constants import RC from ansiblelint.loaders import load_ignore_txt +from ansiblelint.runner import get_matches from ansiblelint.skip_utils import normalize_tag from ansiblelint.version import __version__ if TYPE_CHECKING: # RulesCollection must be imported lazily or ansible gets imported too early. + from collections.abc import Callable + from ansiblelint.rules import RulesCollection from ansiblelint.runner import LintResult _logger = logging.getLogger(__name__) -cache_dir_lock: None | FileLock = None class LintLogHandler(logging.Handler): @@ -107,8 +119,9 @@ def initialize_logger(level: int = 0) -> None: _logger.debug("Logging initialized to level %s", logging_level) -def initialize_options(arguments: list[str] | None = None) -> None: +def initialize_options(arguments: list[str] | None = None) -> None | FileLock: """Load config options and store them inside options module.""" + cache_dir_lock = None new_options = cli.get_config(arguments or []) new_options.cwd = pathlib.Path.cwd() @@ -132,13 +145,13 @@ def initialize_options(arguments: list[str] | None = None) -> None: options.cache_dir.mkdir(parents=True, exist_ok=True) if not options.offline: # pragma: no cover - cache_dir_lock = FileLock( # pylint: disable=redefined-outer-name + cache_dir_lock = FileLock( f"{options.cache_dir}/.lock", ) try: cache_dir_lock.acquire(timeout=180) except Timeout: # pragma: no cover - _logger.error( + _logger.error( # noqa: TRY400 "Timeout waiting for another instance of ansible-lint to release the lock.", ) sys.exit(RC.LOCK_TIMEOUT) @@ -147,6 +160,8 @@ def initialize_options(arguments: list[str] | None = None) -> None: if "ANSIBLE_DEVEL_WARNING" not in os.environ: # pragma: no branch os.environ["ANSIBLE_DEVEL_WARNING"] = "false" + return cache_dir_lock + def _do_list(rules: RulesCollection) -> int: # On purpose lazy-imports to avoid pre-loading Ansible @@ -194,23 +209,85 @@ def _do_transform(result: LintResult, opts: Options) -> None: def support_banner() -> None: """Display support banner when running on unsupported platform.""" - if sys.version_info < (3, 9, 0): # pragma: no cover - prefix = "::warning::" if "GITHUB_ACTION" in os.environ else "WARNING: " - console_stderr.print( - f"{prefix}ansible-lint is no longer tested under Python {sys.version_info.major}.{sys.version_info.minor} and will soon require 3.9. Do not report bugs for this version.", - style="bold red", - ) -# pylint: disable=too-many-statements,too-many-locals +def fix(runtime_options: Options, result: LintResult, rules: RulesCollection) -> None: + """Fix the linting errors. + + :param options: Options object + :param result: LintResult object + """ + match_count = len(result.matches) + _logger.debug("Begin fixing: %s matches", match_count) + ruamel_safe_version = "0.17.26" + + # pylint: disable=import-outside-toplevel + from packaging.version import Version + from ruamel.yaml import __version__ as ruamel_yaml_version_str + + # pylint: enable=import-outside-toplevel + + if Version(ruamel_safe_version) > Version(ruamel_yaml_version_str): + _logger.warning( + "We detected use of `--fix` feature with a buggy ruamel-yaml %s library instead of >=%s, upgrade it before reporting any bugs like dropped comments.", + ruamel_yaml_version_str, + ruamel_safe_version, + ) + acceptable_tags = {"all", "none", *rules.known_tags()} + unknown_tags = set(options.write_list).difference(acceptable_tags) + + if unknown_tags: + _logger.error( + "Found invalid value(s) (%s) for --fix arguments, must be one of: %s", + ", ".join(unknown_tags), + ", ".join(acceptable_tags), + ) + sys.exit(RC.INVALID_CONFIG) + _do_transform(result, options) + + rerun = ["yaml"] + resolved = [] + for idx, match in reversed(list(enumerate(result.matches))): + _logger.debug("Fixing: (%s of %s) %s", match_count - idx, match_count, match) + if match.fixed: + _logger.debug("Fixed, removed: %s", match) + result.matches.pop(idx) + continue + if match.rule.id not in rerun: + _logger.debug("Not rerun eligible: %s", match) + continue + + uid = (match.rule.id, match.filename) + if uid in resolved: + _logger.debug("Previously resolved: %s", match) + result.matches.pop(idx) + continue + _logger.debug("Rerunning: %s", match) + runtime_options.tags = [match.rule.id] + runtime_options.lintables = [match.filename] + runtime_options._skip_ansible_syntax_check = True # noqa: SLF001 + new_results = get_matches(rules, runtime_options) + if not new_results.matches: + _logger.debug("Newly resolved: %s", match) + result.matches.pop(idx) + resolved.append(uid) + continue + if match in new_results.matches: + _logger.debug("Still found: %s", match) + continue + _logger.debug("Fixed, removed: %s", match) + result.matches.pop(idx) + + +# pylint: disable=too-many-locals def main(argv: list[str] | None = None) -> int: """Linter CLI entry point.""" # alter PATH if needed (venv support) - path_inject() + path_inject(argv[0] if argv and argv[0] else "") if argv is None: # pragma: no cover argv = sys.argv - initialize_options(argv[1:]) + cache_dir_lock = initialize_options(argv[1:]) console_options["force_terminal"] = options.colored reconfigure(console_options) @@ -236,7 +313,23 @@ def main(argv: list[str] | None = None) -> int: _logger.debug("Options: %s", options) _logger.debug("CWD: %s", Path.cwd()) - if not options.offline: + # checks if we have `ANSIBLE_LINT_SKIP_SCHEMA_UPDATE` set to bypass schema + # update. Also skip if in offline mode. + # env var set to skip schema refresh + skip_schema_update = ( + bool( + int( + os.environ.get( + SKIP_SCHEMA_UPDATE, + "0", + ), + ), + ) + or options.offline + or options.nodeps + ) + + if not skip_schema_update: # pylint: disable=import-outside-toplevel from ansiblelint.schemas.__main__ import refresh_schemas @@ -244,7 +337,6 @@ def main(argv: list[str] | None = None) -> int: # pylint: disable=import-outside-toplevel from ansiblelint.rules import RulesCollection - from ansiblelint.runner import _get_matches if options.list_profiles: from ansiblelint.generate_docs import profiles_as_rich @@ -265,20 +357,7 @@ def main(argv: list[str] | None = None) -> int: if isinstance(options.tags, str): options.tags = options.tags.split(",") # pragma: no cover - result = _get_matches(rules, options) - - if options.write_list: - ruamel_safe_version = "0.17.26" - from packaging.version import Version - from ruamel.yaml import __version__ as ruamel_yaml_version_str - - if Version(ruamel_safe_version) > Version(ruamel_yaml_version_str): - _logger.warning( - "We detected use of `--write` feature with a buggy ruamel-yaml %s library instead of >=%s, upgrade it before reporting any bugs like dropped comments.", - ruamel_yaml_version_str, - ruamel_safe_version, - ) - _do_transform(result, options) + result = get_matches(rules, options) mark_as_success = True @@ -292,6 +371,18 @@ def main(argv: list[str] | None = None) -> int: for match in result.matches: if match.tag in ignore_map[match.filename]: match.ignored = True + _logger.debug("Ignored: %s", match) + + if app.yamllint_config.incompatible: + logging.log( + level=logging.ERROR if options.write_list else logging.WARNING, + msg=app.yamllint_config.incompatible, + ) + + if options.write_list: + if app.yamllint_config.incompatible: + sys.exit(RC.INVALID_CONFIG) + fix(runtime_options=options, result=result, rules=rules) app.render_matches(result.matches) @@ -325,7 +416,7 @@ def _run_cli_entrypoint() -> None: raise SystemExit(exc) from exc -def path_inject() -> None: +def path_inject(own_location: str = "") -> None: """Add python interpreter path to top of PATH to fix outside venv calling.""" # This make it possible to call ansible-lint that was installed inside a # virtualenv without having to pre-activate it. Otherwise subprocess will @@ -350,6 +441,7 @@ def path_inject() -> None: inject_paths = [] userbase_bin_path = Path(site.getuserbase()) / "bin" + if ( str(userbase_bin_path) not in paths and (userbase_bin_path / "bin" / "ansible").exists() @@ -357,11 +449,23 @@ def path_inject() -> None: inject_paths.append(str(userbase_bin_path)) py_path = Path(sys.executable).parent - if str(py_path) not in paths and (py_path / "ansible").exists(): + pipx_path = os.environ.get("PIPX_HOME", "pipx") + if ( + str(py_path) not in paths + and (py_path / "ansible").exists() + and pipx_path not in str(py_path) + ): inject_paths.append(str(py_path)) + # last option, if nothing else is found, just look next to ourselves... + if own_location: + own_location = os.path.realpath(own_location) + parent = Path(own_location).parent + if (parent / "ansible").exists() and str(parent) not in paths: + inject_paths.append(str(parent)) + if not os.environ.get("PYENV_VIRTUAL_ENV", None): - if inject_paths: + if inject_paths and not all("pipx" in p for p in inject_paths): print( # noqa: T201 f"WARNING: PATH altered to include {', '.join(inject_paths)} :: This is usually a sign of broken local setup, which can cause unexpected behaviors.", file=sys.stderr, -- cgit v1.2.3