Skip to main content
py-checks

py-checks

Архитектурные соглашения проекта, проверяемые как код.

ci python правил pre-commit ruff pyright license


Линтер знает язык, но не знает ваш проект. Он не скажет, что ORM-модель уехала в сценарий, что колонка Numeric осталась без CHECK, что datetime.now() позвали в домене, а не в порте. Это не ошибки языка — это нарушенные соглашения, и до сих пор их ловило ревью: глазами, у каждого свои, каждый раз заново.

py-checks — движок для таких соглашений. Библиотека везёт правила, проект везёт свою архитектуру: имена слоёв, список запечатанных зон, где живёт ORM, чем ограничена колонка. Без таблиц проекта правила молчат — библиотека не догадывается за вас, как называется ваш домен.

src/app/modules/cashout/application/offer.py:34:9: determinism: uuid4() не детерминирован;
    идентификатор выдают на краю и передают внутрь
src/app/infra/database/models/bet.py:51:5: model-columns: stake — Numeric без ограничений;
    деньги описывают Numeric(18, 4)
src/app/presentation/api/v1/routers/bets.py:22:1: endpoint-declarations: POST /bets
    не назвал response_model

Зачем

  • Соглашение перестаёт быть устным. Правило записано один раз, с причиной, и проверяется на каждом коммите — а не вспоминается на ревью тем, кто его помнит.
  • Правило универсально, таблица — ваша. Одно правило «этот вызов живёт только здесь» закрывает и границу транзакции, и запрет float в домене. Библиотека не содержит ни одного имени вашего проекта.
  • Отказ объясняет себя. Сообщение говорит, что не так и чем это заменить, а не «нарушение правила №14».
  • Исключение стоит одной строки, но требует причины. # check-ok: raw-sql: проба живости, формы ORM нет — пометка без причины сама становится нарушением.
  • Чужую работу мы не делаем. Что умеют ruff, pyright и import-linter — остаётся за ними; что и почему туда отдано, записано в docs/service.md.

Установка

uv add --dev python-checks

Ставится как python-checks, зовётся py-checks: на PyPI живёт сосед по имени, а команда, секция настроек и пакет остались прежними.

Нужен Python 3.14+. Зависимости: libcst, pydantic, pydantic-settings, rich, typer.

За минуту

Положите рядом с pyproject.toml файл py-checks.toml:

src = "src"

[module-length]
max-lines = 300

# Правила, зависящие от места, работают только в названных зонах.
[model-columns]
zones = ["infra/database/models"]
types = { Numeric = "деньги описывают Numeric(18, 4)" }

[determinism]
zones = ["modules/*/domain", "modules/*/application"]
sources = { "datetime.now" = "часы берут портом", "uuid4" = "идентификатор выдают на краю" }

и запустите:

py-checks run          # проверить `src`
py-checks run --fix    # и починить то, что чинится само
py-checks list         # какие правила есть и что включено
py-checks explain determinism   # что правило требует и какие у него настройки

Настройки можно держать и секцией [tool.py-checks] в pyproject.toml — но одно из двух: два места разом библиотека считает ошибкой, а не слиянием.

Что проверяется

Двадцать семь правил в девяти группах:

Группа О чём
imports какой пакет где разрешён, какая зона запечатана
placement что лежит в этой директории и как устроена операция
signatures длина модуля, глубина вложенности, форма подписи
types границы у полей, форма аннотаций, неизменяемость
database граница модели, материал колонки, форма запроса, схема
effects часы, случайность и имя события в логе
api чем маршрут отвечает и что обязан объявить
calls функция, у которой есть список мест, откуда её зовут
hygiene потолок у зависимости
Все двадцать семь
Код Что падает Вид
confined-imports пакет импортируется вне отведённых ему мест файл
sealed-imports запечатанная зона импортирует чужой пакет файл
class-modules в модуле лежит то, чего его директория не допускает файл
class-placement класс лежит не там, где лежат классы его вида файл
operation-shape операция устроена не как операция файл
required-class модуль не объявил класс, ради которого лежит в этой директории файл
keyword-only-arguments подпись записана не полностью файл
function-length функция длиннее лимита файл
module-length модуль длиннее лимита файл
nesting управляющие конструкции вложены глубже предела файл
signature-layout список из двух и более элементов записан в одну строку файл
annotation-shapes форма названа так, что поля в ней безымянные файл
config-fields поле настроек ничем не ограничено файл
confined-types поле в этой части дерева объявлено запрещённым здесь типом файл
constant-annotations константа не сказала типом, что она константа файл
frozen-dataclasses dataclass в зоне объявлен без нужных аргументов файл
bound-checks ограниченная колонка не повторила своё ограничение как CHECK файл
confined-calls названный метод позвали не там, где ему место файл
model-boundary ORM-модель объявлена, собрана или отдана не там файл
model-columns колонка собрана не из того материала файл
raw-sql SQL написан строкой там, где хватило бы выражения файл
statement-keys запрос называет колонку строкой или ходит в базу в цикле файл
schema-drift модели и миграции описывают уже разные схемы среда
determinism код сам читает часы, случайность или новый идентификатор файл
log-events событие в логе названо чем-то кроме члена перечисления файл
endpoint-declarations маршрут не сказал, чем он отвечает файл
confined-functions названная функция позвана не оттуда, откуда ей можно файл
dependency-bounds зависимость может уехать на версию, которую никто не запускал проект

Каждое правило объясняет себя целиком — py-checks explain <код> печатает докстринг с причиной и список настроек. Заготовка таблиц для типового сервиса лежит в docs/service.md.

Три вида правил

Что правилу дают на суд, оно объявляет само — полем scope:

  • файл — разобранный исходник; таких большинство;
  • проект — корень: манифест, согласие файлов репозитория между собой;
  • среда — то же, но нужна живая база или долгий прогон. В обычный прогон такое не входит: py-checks run --all или по имени, место ему в CI.

Пометки

Снять правило со строки можно, но придётся объяснить зачем:

text("SELECT 1")  # db-ok: raw-sql: проба живости, формы ORM нет

Каноническая форма — # check-ok: <код>: <причина>, она снимает ровно одно правило. У каждой группы есть своё короткое слово (# db-ok, # type-ok, # signature-ok, …): человек помнит группу, а не двадцать семь кодов.

Пометок в строке может стоять несколько — подпись в столбик собирает их на последней строке:

    ) -> object:  # signature-ok: так зовёт pydantic  # type-ok: сырой ввод

Пометка без кода, с опечаткой в коде или без причины — сама нарушение. Молча неработающая пометка выглядит как отключённая проверка, а на деле проверка работает и просто её не видит.

pre-commit

- repo: https://github.com/Armontex/py-checks
  rev: v0.1.0
  hooks:
    - id: py-checks
      args: [--fix]          # починить то, что чинится само
    - id: py-checks-sync     # контракты и `.env.example` собраны заново

Хук один, а не по хуку на правило: что включено, решает конфиг проекта. Набор правил зовут args: [--select, "<код>,<код>"]; повторённый флаг делает то же самое.

Ставить его нужно до ruff-format: автофикс ставит символы, а не колонки, и раскладывает подпись форматтер проекта.

Всё остальное — ruff, pyright, import-linter, commitizen — проект объявляет сам: у каждого есть свой хук, написанный его же авторами.

Контракты импортов

Слои проекта описываются один раз, а .importlinter под них собирается:

[contracts.layers]
domain = ["domain"]
application = ["application", "domain"]
presentation = ["presentation", "application"]
py-checks sync           # собрать
py-checks sync --check   # упасть, если файл отстал

Собирается он не только из таблицы, но и из того, что лежит на диске: слой, которого нет, в контракт не попадает — иначе import-linter упал бы на первом же несуществующем модуле. Гоняет граф по собранному файлу хук самого import-linter.

Шапку собранного файла можно написать свою — header в [contracts], вместе с #; как и у .env.example, нужна она проекту с комментариями на другом языке.

Пример окружения

Имя переменной знает поле настроек — оно объявляет его validation_alias, и за этим следит config-fields. Значит, .env.example выводится из тех же классов, что переменные и читают, и вести его рядом руками незачем: расходится он молча, а замечают это, когда переменной не оказалось на проде.

[env-example]
settings = ["myservice.config.settings:Settings"]

Тот же py-checks sync — файл собирается вместе с контрактами. Достаточно корневого класса: свои секции он уже перечислил собственными полями, и повторять их список в настройках значит завести второй, который с первым разойдётся. Поле-секция переменной не становится — у него своего имени в окружении нет. В файл едут имя переменной, значение по умолчанию, первый абзац докстринга класса и description поля, если оно у него есть, — так объяснение живёт рядом с полем, а не в файле, который его переживёт.

Шапку файла можно написать свою — header = "# Generated by py-checks.", вместе с #. Нужна она проекту, который пишет комментарии на другом языке; пустая оставляет шапку библиотеки.

Классы импортируются, а не читаются текстом: имя переменной — значение атрибута, собранное вызовом, и прочитать его исходником значит выполнить этот вызов самому.

Своё правило

Правило — это класс с четырьмя полями и run, объявленный через entry points. Форкать библиотеку не нужно:

# myproject_checks/_no_print.py
import ast
from collections.abc import Iterator
from typing import ClassVar, Final

from py_checks.config import CheckSettings
from py_checks.core import ParsedFile, Scope, Violation

CODE: Final = "no-print"


class NoPrint:
    """Падает, если в исходнике остался `print`."""

    code: ClassVar[str] = CODE
    Settings: ClassVar[type[CheckSettings]] = CheckSettings
    scope: ClassVar[Scope] = Scope.FILE
    marker: ClassVar[str] = "# my-ok"

    @classmethod
    def run(cls, *, file: ParsedFile, settings: CheckSettings) -> Iterator[Violation]:
        for node in ast.walk(file.tree):
            if isinstance(node, ast.Call) and getattr(node.func, "id", "") == "print":
                yield Violation.from_node(
                    node=node,
                    path=file.path,
                    code=CODE,
                    message="print в исходнике; событие пишут логом",
                )
[project.entry-points."py_checks.checks"]
no-print = "myproject_checks:NoPrint"

Дальше оно ведёт себя как родное: попадает в list, в explain, слушается ignore и снимается пометкой.

Команды

Команда
py-checks run [пути] прогнать проверки; 0 — чисто, 1 — есть нарушения
py-checks run --fix наложить правки, которые однозначны
py-checks run --select <код>,<код> только названные правила, несмотря на ignore
py-checks run --all вместе с правилами, которым нужна живая среда
py-checks list все правила: код, состояние, строка описания
py-checks explain <код> что правило требует и какие у него настройки
py-checks sync [--check] собрать контракты импортов и .env.example

Разработка

uv sync
uv run pre-commit install
uv run pytest -q

Проверка описывается не тестом, а папками с примерами: tests/checks/<код>/ с ok/ и bad/ внутри, снимок вывода сверяет syrupy. У правил про проект — tests/projects/<код>__<вариант>/. Библиотека проверяет сама себя: правило, которое не выдерживает свой же репозиторий, до чужого доезжать не должно.

Соглашения репозитория — в AGENTS.md.

Лицензия

MIT — © 2026 Armontex.

Release files for python-checks 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for python-checks 0.3.0
File Size Uploaded
python_checks-0.3.0.tar.gz 86.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for python-checks 0.3.0
File Interpreter ABI Platform
python_checks-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 234.9 kB

Release files / python_checks-0.3.0.tar.gz

Download URL python_checks-0.3.0.tar.gz
Size 86.2 kB
Tags Source
SHA-256 checksum
How to use checksums
567e56e04f2a92a436fcb244f84d57c0ba79638f8e51b6f678ab7ad667799343
BLAKE2b-256 checksum
How to use checksums
92ac5f4f4a5bf259f21298f5f03696c82f286f84e0952b850e6799d1cc6d4c56
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release files / python_checks-0.3.0-py3-none-any.whl

Download URL python_checks-0.3.0-py3-none-any.whl
Size 148.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
83edfd145790516e7ec51d94d846cde7dd7bb6f91fd769e7a3114e7a2d85ea43
BLAKE2b-256 checksum
How to use checksums
e729f47c5b32b59225d71496699a9705f7e4c8a650d6de0df39a5d217796b31d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release history Release notifications | RSS feed

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.3.2

2 release files

0.3.1

2 release files

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page