# alint://bundled/python@v1
#
# Hygiene checks for Python projects (pyproject / setuptools /
# Poetry / PDM / uv). Adopt it with:
#
#     extends:
#       - alint://bundled/python@v1
#
# Gated with `when: facts.has_python` (true if any standard Python
# project marker exists anywhere in the tree) plus a per-rule
# `scope_filter: { has_ancestor: [pyproject.toml, setup.py,
# requirements.txt] }` on per-file content rules so they only
# apply to files inside a Python package — useful in polyglot
# monorepos where Python packages sit alongside Rust / Node / Go
# subdirectories. Override `has_python` with your own `facts:`
# block if you need a different heuristic.

version: 1

facts:
  - id: has_python
    any_file_exists:
      - pyproject.toml
      - "**/pyproject.toml"
      - setup.py
      - "**/setup.py"
      - setup.cfg
      - "**/setup.cfg"
      - requirements.txt
      - "**/requirements.txt"

rules:
  # --- Manifest + lockfile ------------------------------------------
  - id: python-manifest-exists
    when: facts.has_python
    # Modern Python wants `pyproject.toml` (PEP 518 / 621), but
    # `setup.py` + `setup.cfg` are still the reality in older code
    # bases. Any one of the three satisfies the rule.
    kind: file_exists
    paths: ["pyproject.toml", "setup.py", "setup.cfg"]
    root_only: true
    level: error
    message: >-
      Python project: a `pyproject.toml` (preferred), `setup.py`,
      or `setup.cfg` at the repo root is required.
    policy_url: "https://packaging.python.org/en/latest/guides/writing-pyproject-toml/"

  - id: python-has-lockfile
    when: facts.has_python
    # Accept any of the common lockfile names from uv / Poetry /
    # Pipenv / PDM. At least one should be committed for
    # reproducible installs. Skip this rule (`level: off`) if your
    # project genuinely pins via `requirements.txt` with hashes.
    kind: file_exists
    paths:
      - uv.lock
      - poetry.lock
      - Pipfile.lock
      - pdm.lock
    root_only: true
    level: warning
    message: >-
      A lockfile should be committed for reproducible installs
      (uv.lock / poetry.lock / Pipfile.lock / pdm.lock).

  # --- pyproject.toml shape -----------------------------------------
  # Both rules are scoped to `pyproject.toml` — if a project still
  # lives on setup.py / setup.cfg without a pyproject.toml at all,
  # neither rule evaluates and the project passes. When pyproject.toml
  # does exist but the enforced field is missing, the rule fires
  # (zero-match is a violation, which is what we want here).
  - id: python-pyproject-declares-name
    when: facts.has_python
    kind: toml_path_matches
    paths: pyproject.toml
    path: "$.project.name"
    matches: '^[A-Za-z][A-Za-z0-9._-]*$'
    level: warning
    message: >-
      `pyproject.toml` has no `project.name` (PEP 621). Declare
      the distribution name so `pip install .` / `uv build` work.
    policy_url: "https://peps.python.org/pep-0621/"

  - id: python-pyproject-declares-requires-python
    when: facts.has_python
    # Bracket notation is required on keys with dashes — JSONPath
    # dot-notation segments are restricted to [A-Za-z_][A-Za-z0-9_]*.
    kind: toml_path_matches
    paths: pyproject.toml
    path: "$.project['requires-python']"
    matches: '^.+$'
    level: info
    message: >-
      `pyproject.toml` has no `project.requires-python`; declare
      a floor (e.g. `>=3.10`) so installs fail fast on
      unsupported interpreters.

  # --- Source-file conventions --------------------------------------
  - id: python-module-snake-case
    when: facts.has_python
    # PEP 8: module filenames are lowercase with underscores.
    # Scoped to `src/**` and the top level only — tests commonly
    # use `test_*.py` (already snake) and conftest.py which are
    # fine.
    kind: filename_case
    paths: ["*.py", "src/**/*.py"]
    case: snake
    level: info
    message: "Python module filenames should be snake_case (PEP 8)."
    policy_url: "https://peps.python.org/pep-0008/#package-and-module-names"

  # v0.9.18: the cosmetic-formatting rules (final-newline +
  # no-trailing-whitespace) default-exclude the canonical
  # test-fixture trees. Linters and test-suite repos
  # (cpython `Lib/test/**`, ruff `crates/*/resources/test/fixtures/**`,
  # pytest-style `tests/fixtures/**`, Go-style `**/testdata/**`)
  # carry deliberately-malformed Python files; the rules can't
  # distinguish "real source" from "intentional bad input" so
  # the exclude defaults narrow the scope. Users with an
  # unconventional fixture layout can override via per-rule
  # `paths:` in their config. The Trojan-Source check
  # (`python-sources-no-bidi`) deliberately keeps the wider
  # scope — bidi-control chars in fixtures are still a security
  # concern.
  - id: python-sources-final-newline
    when: facts.has_python
    kind: final_newline
    paths:
      include: ["**/*.py"]
      exclude:
        - "tests/fixtures/**"
        - "**/tests/fixtures/**"
        - "**/test/fixtures/**"
        - "**/testdata/**"
        - "Lib/test/**"
        - "crates/**/resources/**"
    scope_filter:
      has_ancestor: [pyproject.toml, setup.py, requirements.txt]
    level: info
    fix:
      file_append_final_newline: {}

  - id: python-sources-no-trailing-whitespace
    when: facts.has_python
    kind: no_trailing_whitespace
    paths:
      include: ["**/*.py"]
      exclude:
        - "tests/fixtures/**"
        - "**/tests/fixtures/**"
        - "**/test/fixtures/**"
        - "**/testdata/**"
        - "Lib/test/**"
        - "crates/**/resources/**"
    scope_filter:
      has_ancestor: [pyproject.toml, setup.py, requirements.txt]
    level: info
    fix:
      file_trim_trailing_whitespace: {}

  # --- Trojan Source defense on Python sources ----------------------
  - id: python-sources-no-bidi
    when: facts.has_python
    kind: no_bidi_controls
    paths: "**/*.py"
    scope_filter:
      has_ancestor: [pyproject.toml, setup.py, requirements.txt]
    level: error
    message: "Trojan Source (CVE-2021-42574): bidi override chars in Python sources are rejected."
    policy_url: "https://trojansource.codes/"
