Skip to main content

sarj-python-lint

Custom Python lint rules via stdlib ast. Designed for pre-commit. For SQL rules see sarj-sql-lint.

uv tool install sarj-python-lint

Pre-commit

- repo: https://github.com/sarj-ai/standards
  rev: python-v0.2.0
  hooks:
    - id: sarj-no-sequential-await
    - id: sarj-inefficient-string-concat-in-loop
    - id: sarj-prefer-str-enum
    - id: sarj-no-fat-try-blocks
    - id: sarj-pydantic-at-boundaries
    - id: sarj-prefer-class-row
    - id: sarj-prefer-timedelta-for-durations
    - id: sarj-prefer-struct-over-namedtuple
    - id: sarj-no-comment-cruft
    - id: sarj-no-fstring-in-log

Test-quality rules (0.15.0)

Mined from an AST audit of ~7,500 test functions across two production repos. Every one is scoped to test files and carries the false-positive guard that made it shippable; the module docstring for each records the population it was measured against.

    - id: sarj-mock-without-spec                   # SARJ040
    - id: sarj-test-loops-over-literal-cases       # SARJ041
    - id: sarj-parametrize-case-needs-id           # SARJ042
    - id: sarj-zero-assertion-test                 # SARJ043
    - id: sarj-fixture-returns-bare-tuple          # SARJ044
    - id: sarj-kwarg-heavy-construction-in-test    # SARJ045
    - id: sarj-xfail-requires-strict               # SARJ046
    - id: sarj-sleep-with-computed-arg-in-test     # SARJ047

Private access, first-party only (0.19.0)

    - id: sarj-no-first-party-private-import       # SARJ048

Reaching past a module's public surface is a design finding when the module is ours and an unavoidable fact of life when it is not: a dependency that moves an API private in a minor release leaves no edit that satisfies the lint.

SARJ048 fires only when the module declaring the private name resolves to a package inside your own project. Third-party privates are never flagged.

It replaces ruff's PLC2701 import-private-name, whose only exemption is same top-level package — a different question, and one that cannot separate from bulbul.stores.task_store import _row_to_task (real; export it) from from livekit.agents.inference_runner import _InferenceRunner (no fix exists). sarj-lint-configs ≥ 0.8.0 ships PLC2701 in its ignore list for exactly this reason; if you take that config, turn this hook on, or you lose the check entirely.

Attribute access (session._stt) is out of scope and stays with ruff's SLF001, which cannot make the distinction either — see the rationale in ruff.strict.toml.

Comment-hygiene rules (0.20.0)

From a 37,918-comment, nine-repo measurement study. All three are deletion-class, so each was validated against pydantic / trio / attrs as well as the maintained repos before shipping — the counts and the false-positive classes each guard was built from are recorded in the rule module docstrings.

    - id: sarj-no-restated-comment                 # SARJ049
    - id: sarj-redundant-docstring                 # SARJ050
    - id: sarj-trailing-value-narration            # SARJ051

redundant-docstring finds real volume on a codebase that has never had it (105 in noura-be), so the same baseline ratchet applies.

House conventions moved out of consumer repos (0.21.0)

    - id: sarj-no-stdlib-logging                   # SARJ052
    - id: sarj-no-gen-random-uuid-in-sql           # SARJ053
    - id: sarj-no-file-level-escape-hatch-noqa     # SARJ054

SARJ052 bans importing stdlib logging in application code, because the house logger is loguru and two logger hierarchies mean two handler chains: the records written to the one nobody configured skip the JSON formatter, the redaction patcher and the error reporter, and — since the stdlib root defaults to WARNING — usually vanish in production while looking fine locally.

The one legitimate reason to touch stdlib logging in a loguru house is to bridge it, and the bridge cannot be written without naming both loggers, so a module importing loguru is exempt. Measured across two production repos that exemption is exact: all four sites that import stdlib logging (bulbul/__init__.py, bulbul/configure_logging.py, agent/main.py, noura-be's common/logging.py) are bridges, all four import loguru, and no other module in either repo imports stdlib logging at all. Tests, scripts/, notebooks/, generated files and if TYPE_CHECKING: imports are also exempt.

This is a house-convention rule, not a universal one. A library should log through stdlib logging precisely so it does not impose a sink on its callers — trio's three sites are correct for trio. Enable it in applications only.

SARJ053 flags gen_random_uuid() in SQL embedded in a Python string literal: UUIDv4 keys scatter B-tree inserts across every leaf page, where uuidv7() (Postgres 18) is time-ordered and appends. It is the embedded-SQL third of a policy the stack already states twice — ruff.strict.toml bans uuid.uuid4, and sarj-sql-lint's SARJ109 prefer-uuidv7-default covers .sql migration files (41 sites in bulbul, 14 in noura-be, all of them a primary-key DEFAULT). A literal only counts when it is SQL-shaped, so prose naming the function is not a finding.

SARJ054 is SARJ038's scoped sibling. SARJ038 bans the unscoped blanket (# ruff: noqa); this bans a scoped file-level exemption that names an escape-hatch code — a code whose remediation ruff.strict.toml spells as an inline # noqa: CODE — <reason>, which today is TID251 alone, ruff's only banned-API code. Hoisting that to the top of a file turns N reviewed per-site decisions into one unreviewable one and pre-authorizes every mock added later. Scoped exemptions for mechanical codes (E501, F401, UP035) are never flagged — measured across five repos those are the entire population.

Suppression ratchet (sarj-ratchet, 0.21.0)

    - id: sarj-suppression-ratchet

One tool replacing the per-repo ratchet scripts. It counts every escape hatch in the tree and enforces three ceilings that may only shrink:

  • per codenoqa:TID251 going 40 → 41 is a regression even if the total falls
  • per package — one package's headroom must not finance another's debt
  • per file — a global cap so new suppressions cannot pile into one hot spot; pre-existing hot spots are grandfathered at their then-current counts

All four dialects are counted under distinct key prefixes, so moving a suppression between spellings can never hide it: noqa:CODE, sarj-noqa:CODE, pyright:CODE, type-ignore:CODE / bare type-ignore, plus the file-level file-noqa:CODE / file-noqa:<blanket> and file-pyright:RULE.

sarj-ratchet --update python/          # seed (or lock in a drop)
sarj-ratchet python/                   # gate
sarj-ratchet --update --allow-increase python/   # a reviewed ceiling raise

--update refuses to raise a ceiling unless --allow-increase says the raise was reviewed, and it drops a per-file grandfather clause as soon as the file falls back under the global cap, so an allowance cannot outlive its debt.

Two conventions that stayed pygrep

sarj-fakes-in-shared-location and sarj-no-raw-connection-in-tests ship as pygrep hooks, not SARJ rules, and both need a files:/exclude: from the consumer. An AST port of each was built and measured, and the boundary each encodes turned out to be repo-specific rather than shared: "shared fake" flagged 9/9 single-use test doubles in noura-be that are idiomatic where they sit, and "raw connection in a test" flagged 46 sites in bulbul of which every one is already an intentional exemption (store tests asserting DB state, pool-lifecycle tests, retention tests where physical deletion is the subject). SARJ036 no-raw-sql-in-tests remains the corpus-validated shared rule for raw SQL in tests.

Adopting these against an existing suite is easier through the baseline ratchet than as a big-bang fix — snapshot the current counts, then let them only shrink:

sarj-python-lint check --rule mock-without-spec --update-baseline test-quality-baseline.json python/
sarj-python-lint check --rule mock-without-spec --baseline test-quality-baseline.json python/

CLI

sarj-python-lint check --rule no-sequential-await path/to/file.py
sarj-python-lint list-rules

Diagnostic format is path:line:col: CODE message — Ruff-compatible.

Suppression

Inline # sarj-noqa: SARJ00X — <reason> on the offending line.

Each rule's source under src/sarj_python_lint/rules/ carries its own description and diagnostic message.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sarj_python_lint-0.21.0.tar.gz (190.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sarj_python_lint-0.21.0-py3-none-any.whl (242.5 kB view details)

Uploaded Python 3

File details

Details for the file sarj_python_lint-0.21.0.tar.gz.

File metadata

  • Download URL: sarj_python_lint-0.21.0.tar.gz
  • Upload date:
  • Size: 190.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for sarj_python_lint-0.21.0.tar.gz
Algorithm Hash digest
SHA256 d7d1c83eb7ede0a8b0700d99873151a64cd2d9889d2e604b4b8ba5c7cf566f12
MD5 d8607eb40f096ba091c13b656412d1b0
BLAKE2b-256 aa3c1b24e254dd23a683e141a048e744982c30f7e37c24979f667680010efaf6

See more details on using hashes here.

Provenance

The following attestation bundles were made for sarj_python_lint-0.21.0.tar.gz:

Publisher: release.yml on sarj-ai/standards

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file sarj_python_lint-0.21.0-py3-none-any.whl.

File metadata

File hashes

Hashes for sarj_python_lint-0.21.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ff7864636c86cec41f6e0d61b0c733084c3ce1ae3e4f5adb8536e5994b7ec0e2
MD5 de5a076a0c6e5b2ab6c0e6e7a0a2a167
BLAKE2b-256 cc55b737a0f8f528e3823be42d42bcfef4f384c233c23d74bc7fe5ed8b63a1e3

See more details on using hashes here.

Provenance

The following attestation bundles were made for sarj_python_lint-0.21.0-py3-none-any.whl:

Publisher: release.yml on sarj-ai/standards

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.77.1

2 files

0.77.0

2 files

0.76.0

2 files

0.75.0

2 files

0.74.0

2 files

0.73.2

2 files

0.73.1

2 files

0.73.0

2 files

0.72.0

2 files

0.71.0

2 files

0.70.1

2 files

0.70.0

2 files

0.69.1

2 files

0.69.0

2 files

0.68.0

2 files

0.67.14

2 files

0.67.13

2 files

0.67.12

2 files

0.67.11

2 files

0.67.10

2 files

0.67.9

2 files

0.67.8

2 files

0.67.7

2 files

0.67.6

2 files

0.67.5

2 files

0.67.4

2 files

0.67.3

2 files

0.67.2

2 files

0.67.0

1 file

0.66.0

1 file

0.65.0

1 file

0.64.0

1 file

0.63.0

1 file

0.62.0

1 file

0.61.0

1 file

0.60.0

1 file

0.59.0

1 file

0.58.0

1 file

0.57.0

1 file

0.56.0

1 file

0.55.0

1 file

0.54.1

1 file

0.54.0

1 file

0.53.0

1 file

0.52.1

1 file

0.52.0

1 file

0.51.3

1 file

0.51.1

1 file

0.51.0

1 file

0.50.0

1 file

0.49.0

1 file

0.48.0

1 file

0.47.1

1 file

0.47.0

1 file

0.46.0

2 files

0.45.0

2 files

0.44.0

2 files

0.43.2

2 files

0.43.0

2 files

0.42.0

2 files

0.41.0

2 files

0.40.0

2 files

0.39.0

2 files

0.38.0

2 files

0.37.0

2 files

0.36.1

2 files

0.36.0

2 files

0.35.0

2 files

0.34.0

2 files

0.33.0

2 files

0.32.0

2 files

0.31.0

2 files

0.30.0

2 files

0.29.0

2 files

0.28.0

2 files

0.27.0

2 files

0.26.0

2 files

0.25.0

2 files

0.24.0

2 files

0.23.0

2 files

This release

0.21.0 This release

2 files

0.20.0

2 files

0.19.0

2 files

0.18.1

2 files

0.18.0

2 files

0.17.0

2 files

0.16.0

2 files

0.15.1

2 files

0.15.0

2 files

0.14.0

2 files

0.13.1

2 files

0.12.2

2 files

0.12.1

2 files

0.12.0

2 files

0.11.1

2 files

0.11.0

2 files

0.10.1

2 files

0.10.0

2 files

0.9.0

2 files

0.8.0

2 files

0.7.0

2 files

0.6.0

2 files

0.5.0

2 files

0.4.1

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page