Adds ``S`` (flake8-bandit) to the strict ruleset. ``PTH`` was scoped
out: it surfaced 1000+ violations with no auto-fix support (most PTH
sub-rules are warning-only because each ``str`` ↔ ``Path`` conversion
is context-sensitive). Reserved for its own dedicated commit.
Config tuning
=============
Global ``ignore`` additions with rationale:
- ``S603`` (subprocess call without ``shell=False``) and ``S607`` (start
process with partial path) are over-eager for a project whose CLI
invokes ``make`` / ``git`` / ``docker`` / ``ansible-playbook`` from
PATH using list-form args. The list-form already prevents shell
injection (Python's ``subprocess`` does not parse argv[0] as a shell
command), and resolving every binary's absolute path in the source
would couple the code to the host filesystem layout. The genuinely
dangerous shell-injection patterns (``shell=True``) are still caught
by S602 / S605.
New ``[tool.ruff.lint.per-file-ignores]`` for ``tests/**``:
- ``S101`` — ``assert`` is the unittest idiom.
- ``S102`` — ``exec`` is used to load script-under-test by file path.
- ``S105`` / ``S106`` — hardcoded credentials in fixtures / dummy creds.
- ``S108`` — ``/tmp`` paths in tempdir fixtures.
- ``S110`` / ``S112`` — ``try``/``except``/``pass``/``continue`` in cleanup helpers.
Hand-fixes (38 across 23 files)
================================
S101 × 3
Type-narrowing asserts (``cli/deploy/development/proc.py`` —
``p.stdout is not None`` after ``stdout=PIPE``) and an upstream-Odoo
invariant assert in ``roles/web-app-odoo/.../res_users.py``.
S105 × 1
``cli/core/app.py`` argv parser comparing ``token == "--log"`` —
``token`` is a CLI argv element, not a credential. Inline noqa.
S106 × 1
``utils/cache/users.py`` Jinja template string ``"{{ 42 |
strong_password }}"`` rendered to a real password at runtime; not a
literal credential. Inline noqa.
S108 × 6
Container-internal paths bind-mounted from the host:
- ``/tmp/infinito/ca/root-ca.crt`` and ``/tmp/infinito/bin/with-ca-trust.sh``
in ``roles/sys-svc-compose-ca/files/compose_ca.py`` and
``roles/sys-svc-container/files/container.py`` (CA-injection
contract between role and wrapper).
- ``/tmp/cache_nginx_general/`` and ``/tmp/cache_nginx_image/`` in
``plugins/lookup/nginx.py`` (well-known nginx cache dirs).
S110 × 1
``roles/web-app-bigbluebutton/filter_plugins/compose_mods.py``: filter
intentionally swallows YAML parse errors and falls back to the
unmodified text. Inline noqa.
S112 × 5
Filter plugins / inventory builders iterating over role config files
with skip-on-malformed semantics. The pattern is intentional
throughout (CLI tools / Ansible plugins shouldn't crash on a single
bad role file). Per-line noqa with rationale.
S324 × 2
``utils/cache/base.py`` uses md5 as a fast non-cryptographic
fingerprint for cache keying; ``utils/manager/value_generator.py``
uses sha1 when the caller selects ``algorithm == "sha1"`` for legacy
app compatibility. Both gained ``usedforsecurity=False`` (Python 3.9+
hashlib API) which silences the rule structurally; comments document
the non-security purpose.
S310 × 8
``urlopen`` calls to fixed HTTPS registries (``api.github.com``,
``hub.docker.com``, ``ghcr.io``). URL prefix is a literal; only the
``namespace`` / ``image`` segment varies and comes from the project's
own ``meta/services.yml``, never user input. No ``file://`` scheme
exposure. Inline noqa per call site.
S602 / S605 / S606 × 8
Role-deployed scripts that legitimately use ``shell=True`` /
``os.system`` for pipes/``&&``, or ``os.execvp`` in list-form (safe by
construction — no shell parses argv). Each call site gained an inline
noqa with a per-call rationale identifying where the command string
is sourced (Ansible-templated config, host-trusted, never untrusted
external input).
Verification
============
- ``ruff check .`` → All checks passed
- ``ruff format --check .`` → 894 files formatted
- 1621 unit tests → all pass
- Integration suite: 44 baseline failures (unchanged)
23 files changed, +97 / −38. Combined with the prior commits the
branch satisfies 21 ruff rule families end-to-end. ``PTH``
(modernise ``os.path`` to ``pathlib``) is the remaining big-impact
candidate, deferred to its own session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds ``[tool.ruff.lint]`` with ``select = ["E", "F", "I", "B", "UP"]``
and ``target-version = "py311"``. ``E501`` is ignored because
``ruff format`` already reflows what it can; the remaining long lines
are unsplittable string literals / URLs.
Auto-fixed (2753 issues, behavior-preserving):
- UP006/UP035: ``List[X]`` → ``list[X]``, ``Dict[X]`` → ``dict[X]``,
``from typing import …`` redirected where superseded by builtins.
- UP045: ``Optional[X]`` → ``X | None``.
- UP010/UP009/UP015: dropped Python-2-era pragmas (``from __future__
import absolute_import, division, print_function``,
``# -*- coding: utf-8 -*-``), redundant ``open(..., "r")`` mode.
- UP004: dropped implicit ``class X(object):``.
- UP022/UP008/UP032: stdlib idiom modernization (``capture_output``,
super-call simplification, etc.).
- B905: ``zip(...)`` → ``zip(..., strict=False)`` (preserves prior
behavior; switching to ``strict=True`` is a separate semantic
choice not made here).
- I001: import-block ordering.
Hand-fixed (66 issues):
- B904 × 54: appended ``from <bound>`` (when the enclosing ``except``
binds a name) or ``from None`` (when it doesn't) so chained
exceptions stay distinguishable from errors-during-exception-
handling. Applied via a small AST-aware script that walked back to
the enclosing ``except`` clause and matched the binding.
- B017 × 4: narrowed broad ``assertRaises(Exception)`` to the actual
``AnsibleError`` / ``AnsibleFilterError`` raised by the code under
test.
- B023 × 2: bound captured loop variables via default args
(``def _record(name, *, _path_real=path_real, ...)``) so each
closure gets its own snapshot.
- B007 × 2: unused loop control variables renamed to ``_`` /
rewritten to ``.values()``.
- UP007 × 2: ``Union[X, Y]`` → ``X | Y`` (plus dropping the now-unused
``from typing import Union``).
- B018 × 1: Odoo addon manifests are bare dict expressions at module
level by design — suppressed with an inline ``# noqa: B018`` and
rationale comment.
- UP031 × 1: printf-style format → f-string.
The 65 ``# noqa: …`` markers introduced by the auto-fixer all carry
real ruff codes (``B018``, ``E402``, ``F401``, etc.), so
``test_noqa_only_ruff_codes`` stays green.
Verification
============
- ``ruff check .`` → All checks passed
- ``ruff format --check .`` → 894 files already formatted
- ``tests.lint.repository.{test_project_root_import,
test_no_direct_yaml_calls, test_noqa_only_ruff_codes}`` and
``tests.unit.utils.annotations.test_suppress`` → 20 tests OK
- 1621 unit tests → all pass
- Integration suite: 44 failures (pre-existing baseline, none from
this change), 1 skipped.
Going forward, the new rules block regressions on E (style), F
(pyflakes), I (import order), B (likely-bug patterns), and UP
(modernization to py311) for any new code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>