diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4ee1de6..8081d69 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.22 + rev: v0.16.1 hooks: - id: ruff-check args: [--fix] @@ -28,7 +28,7 @@ repos: - id: actionlint - repo: https://github.com/zizmorcore/zizmor-pre-commit - rev: v1.28.0 + rev: v1.29.0 hooks: - id: zizmor args: [--no-progress] diff --git a/src/env_auditor/__init__.py b/src/env_auditor/__init__.py index 95b6bc0..d1076a6 100644 --- a/src/env_auditor/__init__.py +++ b/src/env_auditor/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -from importlib.metadata import version, PackageNotFoundError +from importlib.metadata import PackageNotFoundError, version try: __version__: str = version("env-auditor") diff --git a/src/env_auditor/cli.py b/src/env_auditor/cli.py index 6679b73..ccd1a44 100644 --- a/src/env_auditor/cli.py +++ b/src/env_auditor/cli.py @@ -4,7 +4,7 @@ import re import sys from pathlib import Path -from typing import NoReturn, Optional +from typing import NoReturn from env_auditor import __version__ from env_auditor.colors import supports_color @@ -19,7 +19,6 @@ from env_auditor.reporter import render_json, render_text from env_auditor.scanner import scan_directory - # ────────────────────────────────────────────────────────────────────────────── # Argument parser # ────────────────────────────────────────────────────────────────────────────── @@ -130,7 +129,7 @@ def _resolve_scan_root(raw_path: str) -> Path: Raises: SystemExit(2): If the path does not exist or is not a directory. """ - resolved: Optional[Path] = None + resolved: Path | None = None try: resolved = Path(raw_path).resolve() except (OSError, ValueError) as exc: @@ -319,9 +318,7 @@ def _run_audit( # "Result: PASS" while the process still exited 1. effective_undoc = diff.undocumented - ignore_keys effective_stale = diff.stale - ignore_keys - if effective_undoc or diff.required_missing: - exit_code = 1 - elif cfg.strict and effective_stale: + if effective_undoc or diff.required_missing or cfg.strict and effective_stale: exit_code = 1 else: exit_code = 0 @@ -357,7 +354,7 @@ def _run_audit( # ────────────────────────────────────────────────────────────────────────────── -def main(argv: Optional[list[str]] = None) -> None: +def main(argv: list[str] | None = None) -> None: """Parse arguments, run the audit, and exit with the appropriate code. Exit codes: diff --git a/src/env_auditor/config.py b/src/env_auditor/config.py index 519ac28..35b82b3 100644 --- a/src/env_auditor/config.py +++ b/src/env_auditor/config.py @@ -2,9 +2,10 @@ import re import sys -from dataclasses import dataclass, field, replace as dataclass_replace +from dataclasses import dataclass, field +from dataclasses import replace as dataclass_replace from pathlib import Path -from typing import Any, Optional +from typing import Any # Config file is searched in this order within the scan root. CONFIG_FILENAMES = (".env-auditorrc", "env-auditor.toml", "pyproject.toml") @@ -107,7 +108,7 @@ def load_config_from_file(path: Path) -> EnvAuditorConfig: return _dict_to_config(raw, path) -def _read_config_raw(path: Path) -> tuple[Optional[dict[str, Any]], bool]: +def _read_config_raw(path: Path) -> tuple[dict[str, Any] | None, bool]: """Validate size, parse path as TOML, and return its env-auditor section. Shared by :func:`load_config` (auto-discovery) and @@ -155,7 +156,7 @@ def _read_config_raw(path: Path) -> tuple[Optional[dict[str, Any]], bool]: return raw, False -def _parse_toml_file(path: Path, is_pyproject: bool) -> Optional[dict[str, Any]]: +def _parse_toml_file(path: Path, is_pyproject: bool) -> dict[str, Any] | None: """Parse *path* as TOML and return the env-auditor section, or None. Uses stdlib ``tomllib`` (Python 3.11+) with ``tomli`` fallback, @@ -250,9 +251,12 @@ def _minimal_toml_parse(path: Path) -> dict[str, Any]: elif value.startswith("["): # Use pre-compiled regex — not constructed from user input current_node[key] = _LIST_ITEMS_RE.findall(value) - elif value.startswith('"') and value.endswith('"'): - current_node[key] = value[1:-1] - elif value.startswith("'") and value.endswith("'"): + elif ( + value.startswith('"') + and value.endswith('"') + or value.startswith("'") + and value.endswith("'") + ): current_node[key] = value[1:-1] else: current_node[key] = value @@ -340,12 +344,12 @@ def _dict_to_config(raw: dict[str, Any], source: Path) -> EnvAuditorConfig: def merge_cli_into_config( cfg: EnvAuditorConfig, *, - env_files: Optional[list[str]] = None, - exclude_dirs: Optional[list[str]] = None, - ignore_stale: Optional[bool] = None, - ignore_missing: Optional[bool] = None, - strict: Optional[bool] = None, - output_format: Optional[str] = None, + env_files: list[str] | None = None, + exclude_dirs: list[str] | None = None, + ignore_stale: bool | None = None, + ignore_missing: bool | None = None, + strict: bool | None = None, + output_format: str | None = None, ) -> EnvAuditorConfig: """Apply CLI overrides onto *cfg*, returning a new merged config. diff --git a/src/env_auditor/parser.py b/src/env_auditor/parser.py index 2c6a3f9..1b73786 100644 --- a/src/env_auditor/parser.py +++ b/src/env_auditor/parser.py @@ -4,7 +4,6 @@ import sys from dataclasses import dataclass, field from pathlib import Path -from typing import Optional # Pre-compiled constants — never constructed from user input. _VALID_KEY_RE: re.Pattern[str] = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") @@ -37,7 +36,7 @@ def empty_keys(self) -> frozenset[str]: return frozenset(k for k, v in self.keys_with_values.items() if v == "") -def parse_env_file(path: Path) -> Optional[ParsedEnvFile]: +def parse_env_file(path: Path) -> ParsedEnvFile | None: """Parse a dotenv-format file and return its keys. Sensitive value protection: values are stored only to detect emptiness. diff --git a/src/env_auditor/patterns.py b/src/env_auditor/patterns.py index 85a0011..ec1c3b2 100644 --- a/src/env_auditor/patterns.py +++ b/src/env_auditor/patterns.py @@ -1,8 +1,8 @@ from __future__ import annotations import re +from collections.abc import Sequence from dataclasses import dataclass, field -from typing import Optional, Sequence # Env var name: uppercase, starts with letter, underscores/digits allowed ENV_VAR_NAME = r"([A-Z][A-Z0-9_]*)" @@ -20,7 +20,7 @@ class LanguagePattern: extensions: Sequence[str] static_patterns: Sequence[re.Pattern[str]] dynamic_patterns: Sequence[re.Pattern[str]] = field(default_factory=list) - line_filter: Optional[re.Pattern[str]] = None + line_filter: re.Pattern[str] | None = None """If set, static_patterns/dynamic_patterns are only applied to lines that match this filter first. Used by Docker: the KEY= continuation pattern (which has no anchor of its own) must not fire on RUN/LABEL/CMD lines that diff --git a/src/env_auditor/reporter.py b/src/env_auditor/reporter.py index 72d0417..3b89575 100644 --- a/src/env_auditor/reporter.py +++ b/src/env_auditor/reporter.py @@ -1,7 +1,7 @@ from __future__ import annotations import json -from typing import Any, Optional +from typing import Any from env_auditor.colors import get_colors from env_auditor.differ import DiffResult @@ -16,7 +16,7 @@ def render_text( use_color: bool = True, ignore_stale: bool = False, ignore_missing: bool = False, - ignore_keys: Optional[set[str]] = None, + ignore_keys: set[str] | None = None, ) -> str: """Render a human-readable audit report. @@ -124,7 +124,7 @@ def render_json( passed: bool, ignore_stale: bool = False, ignore_missing: bool = False, - ignore_keys: Optional[set[str]] = None, + ignore_keys: set[str] | None = None, ) -> str: """Render a machine-readable JSON audit report. diff --git a/src/env_auditor/scanner.py b/src/env_auditor/scanner.py index 103c6c7..c21e253 100644 --- a/src/env_auditor/scanner.py +++ b/src/env_auditor/scanner.py @@ -5,7 +5,6 @@ import sys from dataclasses import dataclass, field from pathlib import Path -from typing import Optional from env_auditor.patterns import ( DOCKERFILE_PATTERN, @@ -109,7 +108,7 @@ def sanitize_raw(text: str) -> str: def scan_directory( root: Path, - extra_exclude: Optional[list[Path]] = None, + extra_exclude: list[Path] | None = None, ) -> ScanResult: """Walk *root* recursively and extract all env var references. diff --git a/tests/test_cli.py b/tests/test_cli.py index 1cfe34b..ef0911a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -8,7 +8,6 @@ from env_auditor.cli import main - # ────────────────────────────────────────────────────────────────────────────── # Helpers # ────────────────────────────────────────────────────────────────────────────── diff --git a/tests/test_colors.py b/tests/test_colors.py index 10bbc25..fcb718d 100644 --- a/tests/test_colors.py +++ b/tests/test_colors.py @@ -1,6 +1,5 @@ from __future__ import annotations - from env_auditor.colors import Colors, NoColors, get_colors, supports_color diff --git a/tests/test_config.py b/tests/test_config.py index 8241fd3..1dcedf4 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -3,7 +3,6 @@ import textwrap from pathlib import Path - from env_auditor.config import ( CONFIG_FILE_SIZE_LIMIT, EnvAuditorConfig, @@ -15,7 +14,6 @@ merge_cli_into_config, ) - # ────────────────────────────────────────────────────────────────────────────── # Helpers # ────────────────────────────────────────────────────────────────────────────── diff --git a/tests/test_differ.py b/tests/test_differ.py index c201f85..91b08f3 100644 --- a/tests/test_differ.py +++ b/tests/test_differ.py @@ -1,6 +1,5 @@ from __future__ import annotations - from env_auditor.differ import diff_keys @@ -90,10 +89,11 @@ def test_returns_frozensets(): def test_ignore_keys_removes_from_undocumented(): + import json + from env_auditor.differ import diff_keys + from env_auditor.reporter import render_json, render_text from env_auditor.scanner import ScanResult - from env_auditor.reporter import render_text, render_json - import json code = frozenset({"FOO", "IGNORED"}) documented = frozenset({"FOO"}) @@ -113,8 +113,8 @@ def test_ignore_keys_removes_from_undocumented(): def test_ignore_keys_removes_from_stale(): from env_auditor.differ import diff_keys - from env_auditor.scanner import ScanResult from env_auditor.reporter import render_text + from env_auditor.scanner import ScanResult code = frozenset({"FOO"}) documented = frozenset({"FOO", "STALE_BUT_IGNORED"}) diff --git a/tests/test_parser.py b/tests/test_parser.py index 1431ef2..ab4f8d8 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -7,7 +7,6 @@ from env_auditor.parser import parse_env_file, parse_env_files - # ────────────────────────────────────────────────────────────────────────────── # Helpers # ────────────────────────────────────────────────────────────────────────────── diff --git a/tests/test_scanner.py b/tests/test_scanner.py index 0899174..ce9f310 100644 --- a/tests/test_scanner.py +++ b/tests/test_scanner.py @@ -4,8 +4,7 @@ import pytest -from env_auditor.scanner import scan_directory, FILE_SIZE_LIMIT - +from env_auditor.scanner import FILE_SIZE_LIMIT, scan_directory # ────────────────────────────────────────────────────────────────────────────── # Helpers @@ -382,9 +381,10 @@ def fake_stat(self): def test_scan_file_unicode_error_handled(tmp_path, monkeypatch): """Files that raise on read are skipped and logged.""" - import env_auditor.scanner as sc from pathlib import Path + import env_auditor.scanner as sc + p = tmp_path / "bad.py" p.write_text('os.environ["GOOD_KEY"]\n', encoding="utf-8")