python-checks 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_checks-0.1.0/LICENSE +21 -0
- python_checks-0.1.0/PKG-INFO +327 -0
- python_checks-0.1.0/README.md +311 -0
- python_checks-0.1.0/pyproject.toml +76 -0
- python_checks-0.1.0/pyproject.toml.orig +76 -0
- python_checks-0.1.0/src/py_checks/__init__.py +2 -0
- python_checks-0.1.0/src/py_checks/checks/__init__.py +20 -0
- python_checks-0.1.0/src/py_checks/checks/_kind.py +184 -0
- python_checks-0.1.0/src/py_checks/checks/_location.py +172 -0
- python_checks-0.1.0/src/py_checks/checks/_names.py +86 -0
- python_checks-0.1.0/src/py_checks/checks/api/__init__.py +13 -0
- python_checks-0.1.0/src/py_checks/checks/api/_endpoint_declarations.py +204 -0
- python_checks-0.1.0/src/py_checks/checks/api/_marker.py +5 -0
- python_checks-0.1.0/src/py_checks/checks/calls/__init__.py +13 -0
- python_checks-0.1.0/src/py_checks/checks/calls/_confined_functions.py +102 -0
- python_checks-0.1.0/src/py_checks/checks/calls/_marker.py +5 -0
- python_checks-0.1.0/src/py_checks/checks/database/__init__.py +39 -0
- python_checks-0.1.0/src/py_checks/checks/database/_bound_checks.py +186 -0
- python_checks-0.1.0/src/py_checks/checks/database/_confined_calls.py +115 -0
- python_checks-0.1.0/src/py_checks/checks/database/_marker.py +5 -0
- python_checks-0.1.0/src/py_checks/checks/database/_model_boundary.py +236 -0
- python_checks-0.1.0/src/py_checks/checks/database/_model_columns.py +241 -0
- python_checks-0.1.0/src/py_checks/checks/database/_raw_sql.py +108 -0
- python_checks-0.1.0/src/py_checks/checks/database/_schema_drift.py +271 -0
- python_checks-0.1.0/src/py_checks/checks/database/_statement_keys.py +169 -0
- python_checks-0.1.0/src/py_checks/checks/effects/__init__.py +19 -0
- python_checks-0.1.0/src/py_checks/checks/effects/_determinism.py +105 -0
- python_checks-0.1.0/src/py_checks/checks/effects/_log_events.py +120 -0
- python_checks-0.1.0/src/py_checks/checks/effects/_marker.py +5 -0
- python_checks-0.1.0/src/py_checks/checks/hygiene/__init__.py +14 -0
- python_checks-0.1.0/src/py_checks/checks/hygiene/_dependency_bounds.py +185 -0
- python_checks-0.1.0/src/py_checks/checks/hygiene/_marker.py +5 -0
- python_checks-0.1.0/src/py_checks/checks/imports/__init__.py +25 -0
- python_checks-0.1.0/src/py_checks/checks/imports/_confined.py +93 -0
- python_checks-0.1.0/src/py_checks/checks/imports/_marker.py +7 -0
- python_checks-0.1.0/src/py_checks/checks/imports/_sealed.py +100 -0
- python_checks-0.1.0/src/py_checks/checks/imports/_statements.py +52 -0
- python_checks-0.1.0/src/py_checks/checks/placement/__init__.py +38 -0
- python_checks-0.1.0/src/py_checks/checks/placement/_class_modules.py +106 -0
- python_checks-0.1.0/src/py_checks/checks/placement/_class_placement.py +129 -0
- python_checks-0.1.0/src/py_checks/checks/placement/_marker.py +7 -0
- python_checks-0.1.0/src/py_checks/checks/placement/_operation_shape.py +387 -0
- python_checks-0.1.0/src/py_checks/checks/placement/_required_class.py +179 -0
- python_checks-0.1.0/src/py_checks/checks/signatures/__init__.py +33 -0
- python_checks-0.1.0/src/py_checks/checks/signatures/_function_length.py +90 -0
- python_checks-0.1.0/src/py_checks/checks/signatures/_functions.py +92 -0
- python_checks-0.1.0/src/py_checks/checks/signatures/_keyword_only.py +148 -0
- python_checks-0.1.0/src/py_checks/checks/signatures/_marker.py +7 -0
- python_checks-0.1.0/src/py_checks/checks/signatures/_module_length.py +64 -0
- python_checks-0.1.0/src/py_checks/checks/signatures/_nesting.py +156 -0
- python_checks-0.1.0/src/py_checks/checks/signatures/_signature_layout.py +231 -0
- python_checks-0.1.0/src/py_checks/checks/types/__init__.py +36 -0
- python_checks-0.1.0/src/py_checks/checks/types/_annotation_shapes.py +127 -0
- python_checks-0.1.0/src/py_checks/checks/types/_config_fields.py +236 -0
- python_checks-0.1.0/src/py_checks/checks/types/_confined_types.py +117 -0
- python_checks-0.1.0/src/py_checks/checks/types/_constant_annotations.py +128 -0
- python_checks-0.1.0/src/py_checks/checks/types/_frozen_dataclasses.py +112 -0
- python_checks-0.1.0/src/py_checks/checks/types/_marker.py +5 -0
- python_checks-0.1.0/src/py_checks/cli/__init__.py +10 -0
- python_checks-0.1.0/src/py_checks/cli/_app.py +21 -0
- python_checks-0.1.0/src/py_checks/cli/_protocols.py +19 -0
- python_checks-0.1.0/src/py_checks/cli/commands/__init__.py +23 -0
- python_checks-0.1.0/src/py_checks/cli/commands/_explain.py +28 -0
- python_checks-0.1.0/src/py_checks/cli/commands/_list.py +62 -0
- python_checks-0.1.0/src/py_checks/cli/commands/_run.py +167 -0
- python_checks-0.1.0/src/py_checks/cli/commands/_summary.py +20 -0
- python_checks-0.1.0/src/py_checks/cli/commands/_sync.py +80 -0
- python_checks-0.1.0/src/py_checks/config/__init__.py +31 -0
- python_checks-0.1.0/src/py_checks/config/_base.py +26 -0
- python_checks-0.1.0/src/py_checks/config/_config.py +76 -0
- python_checks-0.1.0/src/py_checks/config/_constants.py +31 -0
- python_checks-0.1.0/src/py_checks/config/_errors.py +12 -0
- python_checks-0.1.0/src/py_checks/config/_loader.py +133 -0
- python_checks-0.1.0/src/py_checks/config/_toml.py +24 -0
- python_checks-0.1.0/src/py_checks/contracts/__init__.py +26 -0
- python_checks-0.1.0/src/py_checks/contracts/_constants.py +18 -0
- python_checks-0.1.0/src/py_checks/contracts/_layout.py +63 -0
- python_checks-0.1.0/src/py_checks/contracts/_render.py +217 -0
- python_checks-0.1.0/src/py_checks/contracts/_settings.py +37 -0
- python_checks-0.1.0/src/py_checks/core/__init__.py +56 -0
- python_checks-0.1.0/src/py_checks/core/_constants.py +15 -0
- python_checks-0.1.0/src/py_checks/core/_discovery.py +57 -0
- python_checks-0.1.0/src/py_checks/core/_edit.py +92 -0
- python_checks-0.1.0/src/py_checks/core/_errors.py +35 -0
- python_checks-0.1.0/src/py_checks/core/_fixer.py +55 -0
- python_checks-0.1.0/src/py_checks/core/_format.py +30 -0
- python_checks-0.1.0/src/py_checks/core/_markers.py +220 -0
- python_checks-0.1.0/src/py_checks/core/_protocols.py +88 -0
- python_checks-0.1.0/src/py_checks/core/_registry.py +77 -0
- python_checks-0.1.0/src/py_checks/core/_report.py +45 -0
- python_checks-0.1.0/src/py_checks/core/_runner.py +178 -0
- python_checks-0.1.0/src/py_checks/core/_settings.py +44 -0
- python_checks-0.1.0/src/py_checks/core/_source.py +94 -0
- python_checks-0.1.0/src/py_checks/core/_violation.py +73 -0
- python_checks-0.1.0/src/py_checks/environment/__init__.py +14 -0
- python_checks-0.1.0/src/py_checks/environment/_constants.py +9 -0
- python_checks-0.1.0/src/py_checks/environment/_render.py +227 -0
- python_checks-0.1.0/src/py_checks/environment/_settings.py +35 -0
- python_checks-0.1.0/src/py_checks/py.typed +0 -0
- python_checks-0.1.0/src/py_checks/sync/__init__.py +16 -0
- python_checks-0.1.0/src/py_checks/sync/_sync.py +60 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Armontex
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-checks
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Проверки архитектурных соглашений проекта
|
|
5
|
+
Author: Armontex
|
|
6
|
+
Author-email: Armontex <windle1337@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: libcst>=1.9,<2
|
|
10
|
+
Requires-Dist: pydantic>=2.13.5,<3
|
|
11
|
+
Requires-Dist: pydantic-settings>=2.15.0,<3
|
|
12
|
+
Requires-Dist: rich>=15.0.0,<16.0.0
|
|
13
|
+
Requires-Dist: typer>=0.27.2,<0.28
|
|
14
|
+
Requires-Python: >=3.14
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
<div align="center">
|
|
18
|
+
|
|
19
|
+
<img src="docs/logo.svg" width="112" alt="py-checks">
|
|
20
|
+
|
|
21
|
+
# py-checks
|
|
22
|
+
|
|
23
|
+
**Архитектурные соглашения проекта, проверяемые как код.**
|
|
24
|
+
|
|
25
|
+
[](https://github.com/Armontex/py-checks/actions/workflows/ci.yml)
|
|
26
|
+
[](https://www.python.org/)
|
|
27
|
+
[](#что-проверяется)
|
|
28
|
+
[](#pre-commit)
|
|
29
|
+
[](https://docs.astral.sh/ruff/)
|
|
30
|
+
[](https://microsoft.github.io/pyright/)
|
|
31
|
+
[](LICENSE)
|
|
32
|
+
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
Линтер знает язык, но не знает ваш проект. Он не скажет, что ORM-модель уехала
|
|
38
|
+
в сценарий, что колонка `Numeric` осталась без CHECK, что `datetime.now()`
|
|
39
|
+
позвали в домене, а не в порте. Это не ошибки языка — это нарушенные
|
|
40
|
+
соглашения, и до сих пор их ловило ревью: глазами, у каждого свои, каждый раз
|
|
41
|
+
заново.
|
|
42
|
+
|
|
43
|
+
`py-checks` — движок для таких соглашений. Библиотека везёт правила, проект
|
|
44
|
+
везёт свою архитектуру: имена слоёв, список запечатанных зон, где живёт ORM,
|
|
45
|
+
чем ограничена колонка. Без таблиц проекта правила молчат — библиотека не
|
|
46
|
+
догадывается за вас, как называется ваш домен.
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
src/app/modules/cashout/application/offer.py:34:9: determinism: uuid4() не детерминирован;
|
|
50
|
+
идентификатор выдают на краю и передают внутрь
|
|
51
|
+
src/app/infra/database/models/bet.py:51:5: model-columns: stake — Numeric без ограничений;
|
|
52
|
+
деньги описывают Numeric(18, 4)
|
|
53
|
+
src/app/presentation/api/v1/routers/bets.py:22:1: endpoint-declarations: POST /bets
|
|
54
|
+
не назвал response_model
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Зачем
|
|
58
|
+
|
|
59
|
+
- **Соглашение перестаёт быть устным.** Правило записано один раз, с причиной, и проверяется на каждом коммите — а не вспоминается на ревью тем, кто его помнит.
|
|
60
|
+
- **Правило универсально, таблица — ваша.** Одно правило «этот вызов живёт только здесь» закрывает и границу транзакции, и запрет `float` в домене. Библиотека не содержит ни одного имени вашего проекта.
|
|
61
|
+
- **Отказ объясняет себя.** Сообщение говорит, что не так и чем это заменить, а не «нарушение правила №14».
|
|
62
|
+
- **Исключение стоит одной строки, но требует причины.** `# check-ok: raw-sql: проба живости, формы ORM нет` — пометка без причины сама становится нарушением.
|
|
63
|
+
- **Чужую работу мы не делаем.** Что умеют ruff, pyright и import-linter — остаётся за ними; что и почему туда отдано, записано в [`docs/service.md`](docs/service.md).
|
|
64
|
+
|
|
65
|
+
## Установка
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
uv add --dev python-checks
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Ставится как `python-checks`, зовётся `py-checks`: на PyPI живёт сосед по
|
|
72
|
+
имени, а команда, секция настроек и пакет остались прежними.
|
|
73
|
+
|
|
74
|
+
Нужен Python 3.14+. Зависимости: `libcst`, `pydantic`, `pydantic-settings`, `rich`, `typer`.
|
|
75
|
+
|
|
76
|
+
## За минуту
|
|
77
|
+
|
|
78
|
+
Положите рядом с `pyproject.toml` файл `py-checks.toml`:
|
|
79
|
+
|
|
80
|
+
```toml
|
|
81
|
+
src = "src"
|
|
82
|
+
|
|
83
|
+
[module-length]
|
|
84
|
+
max-lines = 300
|
|
85
|
+
|
|
86
|
+
# Правила, зависящие от места, работают только в названных зонах.
|
|
87
|
+
[model-columns]
|
|
88
|
+
zones = ["infra/database/models"]
|
|
89
|
+
types = { Numeric = "деньги описывают Numeric(18, 4)" }
|
|
90
|
+
|
|
91
|
+
[determinism]
|
|
92
|
+
zones = ["modules/*/domain", "modules/*/application"]
|
|
93
|
+
sources = { "datetime.now" = "часы берут портом", "uuid4" = "идентификатор выдают на краю" }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
и запустите:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
py-checks run # проверить `src`
|
|
100
|
+
py-checks run --fix # и починить то, что чинится само
|
|
101
|
+
py-checks list # какие правила есть и что включено
|
|
102
|
+
py-checks explain determinism # что правило требует и какие у него настройки
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Настройки можно держать и секцией `[tool.py-checks]` в `pyproject.toml` — но
|
|
106
|
+
одно из двух: два места разом библиотека считает ошибкой, а не слиянием.
|
|
107
|
+
|
|
108
|
+
## Что проверяется
|
|
109
|
+
|
|
110
|
+
Двадцать семь правил в девяти группах:
|
|
111
|
+
|
|
112
|
+
| Группа | О чём |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `imports` | какой пакет где разрешён, какая зона запечатана |
|
|
115
|
+
| `placement` | что лежит в этой директории и как устроена операция |
|
|
116
|
+
| `signatures` | длина модуля, глубина вложенности, форма подписи |
|
|
117
|
+
| `types` | границы у полей, форма аннотаций, неизменяемость |
|
|
118
|
+
| `database` | граница модели, материал колонки, форма запроса, схема |
|
|
119
|
+
| `effects` | часы, случайность и имя события в логе |
|
|
120
|
+
| `api` | чем маршрут отвечает и что обязан объявить |
|
|
121
|
+
| `calls` | функция, у которой есть список мест, откуда её зовут |
|
|
122
|
+
| `hygiene` | потолок у зависимости |
|
|
123
|
+
|
|
124
|
+
<details>
|
|
125
|
+
<summary>Все двадцать семь</summary>
|
|
126
|
+
|
|
127
|
+
| Код | Что падает | Вид |
|
|
128
|
+
|---|---|:-:|
|
|
129
|
+
| `confined-imports` | пакет импортируется вне отведённых ему мест | файл |
|
|
130
|
+
| `sealed-imports` | запечатанная зона импортирует чужой пакет | файл |
|
|
131
|
+
| `class-modules` | в модуле лежит то, чего его директория не допускает | файл |
|
|
132
|
+
| `class-placement` | класс лежит не там, где лежат классы его вида | файл |
|
|
133
|
+
| `operation-shape` | операция устроена не как операция | файл |
|
|
134
|
+
| `required-class` | модуль не объявил класс, ради которого лежит в этой директории | файл |
|
|
135
|
+
| `keyword-only-arguments` | подпись записана не полностью | файл |
|
|
136
|
+
| `function-length` | функция длиннее лимита | файл |
|
|
137
|
+
| `module-length` | модуль длиннее лимита | файл |
|
|
138
|
+
| `nesting` | управляющие конструкции вложены глубже предела | файл |
|
|
139
|
+
| `signature-layout` | список из двух и более элементов записан в одну строку | файл |
|
|
140
|
+
| `annotation-shapes` | форма названа так, что поля в ней безымянные | файл |
|
|
141
|
+
| `config-fields` | поле настроек ничем не ограничено | файл |
|
|
142
|
+
| `confined-types` | поле в этой части дерева объявлено запрещённым здесь типом | файл |
|
|
143
|
+
| `constant-annotations` | константа не сказала типом, что она константа | файл |
|
|
144
|
+
| `frozen-dataclasses` | dataclass в зоне объявлен без нужных аргументов | файл |
|
|
145
|
+
| `bound-checks` | ограниченная колонка не повторила своё ограничение как CHECK | файл |
|
|
146
|
+
| `confined-calls` | названный метод позвали не там, где ему место | файл |
|
|
147
|
+
| `model-boundary` | ORM-модель объявлена, собрана или отдана не там | файл |
|
|
148
|
+
| `model-columns` | колонка собрана не из того материала | файл |
|
|
149
|
+
| `raw-sql` | SQL написан строкой там, где хватило бы выражения | файл |
|
|
150
|
+
| `statement-keys` | запрос называет колонку строкой или ходит в базу в цикле | файл |
|
|
151
|
+
| `schema-drift` | модели и миграции описывают уже разные схемы | среда |
|
|
152
|
+
| `determinism` | код сам читает часы, случайность или новый идентификатор | файл |
|
|
153
|
+
| `log-events` | событие в логе названо чем-то кроме члена перечисления | файл |
|
|
154
|
+
| `endpoint-declarations` | маршрут не сказал, чем он отвечает | файл |
|
|
155
|
+
| `confined-functions` | названная функция позвана не оттуда, откуда ей можно | файл |
|
|
156
|
+
| `dependency-bounds` | зависимость может уехать на версию, которую никто не запускал | проект |
|
|
157
|
+
|
|
158
|
+
</details>
|
|
159
|
+
|
|
160
|
+
Каждое правило объясняет себя целиком — `py-checks explain <код>` печатает
|
|
161
|
+
докстринг с причиной и список настроек. Заготовка таблиц для типового сервиса
|
|
162
|
+
лежит в [`docs/service.md`](docs/service.md).
|
|
163
|
+
|
|
164
|
+
### Три вида правил
|
|
165
|
+
|
|
166
|
+
Что правилу дают на суд, оно объявляет само — полем `scope`:
|
|
167
|
+
|
|
168
|
+
- **файл** — разобранный исходник; таких большинство;
|
|
169
|
+
- **проект** — корень: манифест, согласие файлов репозитория между собой;
|
|
170
|
+
- **среда** — то же, но нужна живая база или долгий прогон. В обычный прогон такое не входит: `py-checks run --all` или по имени, место ему в CI.
|
|
171
|
+
|
|
172
|
+
## Пометки
|
|
173
|
+
|
|
174
|
+
Снять правило со строки можно, но придётся объяснить зачем:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
text("SELECT 1") # db-ok: raw-sql: проба живости, формы ORM нет
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Каноническая форма — `# check-ok: <код>: <причина>`, она снимает ровно одно
|
|
181
|
+
правило. У каждой группы есть своё короткое слово (`# db-ok`, `# type-ok`,
|
|
182
|
+
`# signature-ok`, …): человек помнит группу, а не двадцать семь кодов.
|
|
183
|
+
|
|
184
|
+
Пометка без кода, с опечаткой в коде или без причины — сама нарушение. Молча
|
|
185
|
+
неработающая пометка выглядит как отключённая проверка, а на деле проверка
|
|
186
|
+
работает и просто её не видит.
|
|
187
|
+
|
|
188
|
+
## pre-commit
|
|
189
|
+
|
|
190
|
+
```yaml
|
|
191
|
+
- repo: https://github.com/Armontex/py-checks
|
|
192
|
+
rev: v0.1.0
|
|
193
|
+
hooks:
|
|
194
|
+
- id: py-checks
|
|
195
|
+
args: [--fix] # починить то, что чинится само
|
|
196
|
+
- id: py-checks-sync # контракты и `.env.example` собраны заново
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Хук один, а не по хуку на правило: что включено, решает конфиг проекта. Набор
|
|
200
|
+
правил зовут `args: [--select, "<код>,<код>"]`; повторённый флаг делает то же
|
|
201
|
+
самое.
|
|
202
|
+
|
|
203
|
+
Ставить его нужно **до** `ruff-format`: автофикс ставит символы, а не колонки,
|
|
204
|
+
и раскладывает подпись форматтер проекта.
|
|
205
|
+
|
|
206
|
+
Всё остальное — ruff, pyright, import-linter, commitizen — проект объявляет
|
|
207
|
+
сам: у каждого есть свой хук, написанный его же авторами.
|
|
208
|
+
|
|
209
|
+
## Контракты импортов
|
|
210
|
+
|
|
211
|
+
Слои проекта описываются один раз, а `.importlinter` под них собирается:
|
|
212
|
+
|
|
213
|
+
```toml
|
|
214
|
+
[contracts.layers]
|
|
215
|
+
domain = ["domain"]
|
|
216
|
+
application = ["application", "domain"]
|
|
217
|
+
presentation = ["presentation", "application"]
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
py-checks sync # собрать
|
|
222
|
+
py-checks sync --check # упасть, если файл отстал
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Собирается он не только из таблицы, но и из того, что лежит на диске: слой,
|
|
226
|
+
которого нет, в контракт не попадает — иначе import-linter упал бы на первом же
|
|
227
|
+
несуществующем модуле. Гоняет граф по собранному файлу хук самого import-linter.
|
|
228
|
+
|
|
229
|
+
## Пример окружения
|
|
230
|
+
|
|
231
|
+
Имя переменной знает поле настроек — оно объявляет его `validation_alias`, и
|
|
232
|
+
за этим следит `config-fields`. Значит, `.env.example` выводится из тех же
|
|
233
|
+
классов, что переменные и читают, и вести его рядом руками незачем: расходится
|
|
234
|
+
он молча, а замечают это, когда переменной не оказалось на проде.
|
|
235
|
+
|
|
236
|
+
```toml
|
|
237
|
+
[env-example]
|
|
238
|
+
settings = ["myservice.config.settings:Settings"]
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Тот же `py-checks sync` — файл собирается вместе с контрактами. Достаточно
|
|
242
|
+
корневого класса: свои секции он уже перечислил собственными полями, и
|
|
243
|
+
повторять их список в настройках значит завести второй, который с первым
|
|
244
|
+
разойдётся. Поле-секция переменной не становится — у него своего имени в
|
|
245
|
+
окружении нет. В файл едут имя переменной, значение по умолчанию, первый абзац
|
|
246
|
+
докстринга класса и `description` поля, если оно у него есть, — так объяснение
|
|
247
|
+
живёт рядом с полем, а не в файле, который его переживёт.
|
|
248
|
+
|
|
249
|
+
Классы импортируются, а не читаются текстом: имя переменной — значение
|
|
250
|
+
атрибута, собранное вызовом, и прочитать его исходником значит выполнить этот
|
|
251
|
+
вызов самому.
|
|
252
|
+
|
|
253
|
+
## Своё правило
|
|
254
|
+
|
|
255
|
+
Правило — это класс с четырьмя полями и `run`, объявленный через entry points.
|
|
256
|
+
Форкать библиотеку не нужно:
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
# myproject_checks/_no_print.py
|
|
260
|
+
import ast
|
|
261
|
+
from collections.abc import Iterator
|
|
262
|
+
from typing import ClassVar, Final
|
|
263
|
+
|
|
264
|
+
from py_checks.config import CheckSettings
|
|
265
|
+
from py_checks.core import ParsedFile, Scope, Violation
|
|
266
|
+
|
|
267
|
+
CODE: Final = "no-print"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class NoPrint:
|
|
271
|
+
"""Падает, если в исходнике остался `print`."""
|
|
272
|
+
|
|
273
|
+
code: ClassVar[str] = CODE
|
|
274
|
+
Settings: ClassVar[type[CheckSettings]] = CheckSettings
|
|
275
|
+
scope: ClassVar[Scope] = Scope.FILE
|
|
276
|
+
marker: ClassVar[str] = "# my-ok"
|
|
277
|
+
|
|
278
|
+
@classmethod
|
|
279
|
+
def run(cls, *, file: ParsedFile, settings: CheckSettings) -> Iterator[Violation]:
|
|
280
|
+
for node in ast.walk(file.tree):
|
|
281
|
+
if isinstance(node, ast.Call) and getattr(node.func, "id", "") == "print":
|
|
282
|
+
yield Violation.from_node(
|
|
283
|
+
node=node,
|
|
284
|
+
path=file.path,
|
|
285
|
+
code=CODE,
|
|
286
|
+
message="print в исходнике; событие пишут логом",
|
|
287
|
+
)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
```toml
|
|
291
|
+
[project.entry-points."py_checks.checks"]
|
|
292
|
+
no-print = "myproject_checks:NoPrint"
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Дальше оно ведёт себя как родное: попадает в `list`, в `explain`, слушается
|
|
296
|
+
`ignore` и снимается пометкой.
|
|
297
|
+
|
|
298
|
+
## Команды
|
|
299
|
+
|
|
300
|
+
| Команда | |
|
|
301
|
+
|---|---|
|
|
302
|
+
| `py-checks run [пути]` | прогнать проверки; `0` — чисто, `1` — есть нарушения |
|
|
303
|
+
| `py-checks run --fix` | наложить правки, которые однозначны |
|
|
304
|
+
| `py-checks run --select <код>,<код>` | только названные правила, несмотря на `ignore` |
|
|
305
|
+
| `py-checks run --all` | вместе с правилами, которым нужна живая среда |
|
|
306
|
+
| `py-checks list` | все правила: код, состояние, строка описания |
|
|
307
|
+
| `py-checks explain <код>` | что правило требует и какие у него настройки |
|
|
308
|
+
| `py-checks sync [--check]` | собрать контракты импортов и `.env.example` |
|
|
309
|
+
|
|
310
|
+
## Разработка
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
uv sync
|
|
314
|
+
uv run pre-commit install
|
|
315
|
+
uv run pytest -q
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Проверка описывается не тестом, а папками с примерами: `tests/checks/<код>/`
|
|
319
|
+
с `ok/` и `bad/` внутри, снимок вывода сверяет syrupy. У правил про проект —
|
|
320
|
+
`tests/projects/<код>__<вариант>/`. Библиотека проверяет сама себя: правило,
|
|
321
|
+
которое не выдерживает свой же репозиторий, до чужого доезжать не должно.
|
|
322
|
+
|
|
323
|
+
Соглашения репозитория — в [`AGENTS.md`](AGENTS.md).
|
|
324
|
+
|
|
325
|
+
## Лицензия
|
|
326
|
+
|
|
327
|
+
[MIT](LICENSE) — © 2026 Armontex.
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="docs/logo.svg" width="112" alt="py-checks">
|
|
4
|
+
|
|
5
|
+
# py-checks
|
|
6
|
+
|
|
7
|
+
**Архитектурные соглашения проекта, проверяемые как код.**
|
|
8
|
+
|
|
9
|
+
[](https://github.com/Armontex/py-checks/actions/workflows/ci.yml)
|
|
10
|
+
[](https://www.python.org/)
|
|
11
|
+
[](#что-проверяется)
|
|
12
|
+
[](#pre-commit)
|
|
13
|
+
[](https://docs.astral.sh/ruff/)
|
|
14
|
+
[](https://microsoft.github.io/pyright/)
|
|
15
|
+
[](LICENSE)
|
|
16
|
+
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
Линтер знает язык, но не знает ваш проект. Он не скажет, что ORM-модель уехала
|
|
22
|
+
в сценарий, что колонка `Numeric` осталась без CHECK, что `datetime.now()`
|
|
23
|
+
позвали в домене, а не в порте. Это не ошибки языка — это нарушенные
|
|
24
|
+
соглашения, и до сих пор их ловило ревью: глазами, у каждого свои, каждый раз
|
|
25
|
+
заново.
|
|
26
|
+
|
|
27
|
+
`py-checks` — движок для таких соглашений. Библиотека везёт правила, проект
|
|
28
|
+
везёт свою архитектуру: имена слоёв, список запечатанных зон, где живёт ORM,
|
|
29
|
+
чем ограничена колонка. Без таблиц проекта правила молчат — библиотека не
|
|
30
|
+
догадывается за вас, как называется ваш домен.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
src/app/modules/cashout/application/offer.py:34:9: determinism: uuid4() не детерминирован;
|
|
34
|
+
идентификатор выдают на краю и передают внутрь
|
|
35
|
+
src/app/infra/database/models/bet.py:51:5: model-columns: stake — Numeric без ограничений;
|
|
36
|
+
деньги описывают Numeric(18, 4)
|
|
37
|
+
src/app/presentation/api/v1/routers/bets.py:22:1: endpoint-declarations: POST /bets
|
|
38
|
+
не назвал response_model
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Зачем
|
|
42
|
+
|
|
43
|
+
- **Соглашение перестаёт быть устным.** Правило записано один раз, с причиной, и проверяется на каждом коммите — а не вспоминается на ревью тем, кто его помнит.
|
|
44
|
+
- **Правило универсально, таблица — ваша.** Одно правило «этот вызов живёт только здесь» закрывает и границу транзакции, и запрет `float` в домене. Библиотека не содержит ни одного имени вашего проекта.
|
|
45
|
+
- **Отказ объясняет себя.** Сообщение говорит, что не так и чем это заменить, а не «нарушение правила №14».
|
|
46
|
+
- **Исключение стоит одной строки, но требует причины.** `# check-ok: raw-sql: проба живости, формы ORM нет` — пометка без причины сама становится нарушением.
|
|
47
|
+
- **Чужую работу мы не делаем.** Что умеют ruff, pyright и import-linter — остаётся за ними; что и почему туда отдано, записано в [`docs/service.md`](docs/service.md).
|
|
48
|
+
|
|
49
|
+
## Установка
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv add --dev python-checks
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Ставится как `python-checks`, зовётся `py-checks`: на PyPI живёт сосед по
|
|
56
|
+
имени, а команда, секция настроек и пакет остались прежними.
|
|
57
|
+
|
|
58
|
+
Нужен Python 3.14+. Зависимости: `libcst`, `pydantic`, `pydantic-settings`, `rich`, `typer`.
|
|
59
|
+
|
|
60
|
+
## За минуту
|
|
61
|
+
|
|
62
|
+
Положите рядом с `pyproject.toml` файл `py-checks.toml`:
|
|
63
|
+
|
|
64
|
+
```toml
|
|
65
|
+
src = "src"
|
|
66
|
+
|
|
67
|
+
[module-length]
|
|
68
|
+
max-lines = 300
|
|
69
|
+
|
|
70
|
+
# Правила, зависящие от места, работают только в названных зонах.
|
|
71
|
+
[model-columns]
|
|
72
|
+
zones = ["infra/database/models"]
|
|
73
|
+
types = { Numeric = "деньги описывают Numeric(18, 4)" }
|
|
74
|
+
|
|
75
|
+
[determinism]
|
|
76
|
+
zones = ["modules/*/domain", "modules/*/application"]
|
|
77
|
+
sources = { "datetime.now" = "часы берут портом", "uuid4" = "идентификатор выдают на краю" }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
и запустите:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
py-checks run # проверить `src`
|
|
84
|
+
py-checks run --fix # и починить то, что чинится само
|
|
85
|
+
py-checks list # какие правила есть и что включено
|
|
86
|
+
py-checks explain determinism # что правило требует и какие у него настройки
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Настройки можно держать и секцией `[tool.py-checks]` в `pyproject.toml` — но
|
|
90
|
+
одно из двух: два места разом библиотека считает ошибкой, а не слиянием.
|
|
91
|
+
|
|
92
|
+
## Что проверяется
|
|
93
|
+
|
|
94
|
+
Двадцать семь правил в девяти группах:
|
|
95
|
+
|
|
96
|
+
| Группа | О чём |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `imports` | какой пакет где разрешён, какая зона запечатана |
|
|
99
|
+
| `placement` | что лежит в этой директории и как устроена операция |
|
|
100
|
+
| `signatures` | длина модуля, глубина вложенности, форма подписи |
|
|
101
|
+
| `types` | границы у полей, форма аннотаций, неизменяемость |
|
|
102
|
+
| `database` | граница модели, материал колонки, форма запроса, схема |
|
|
103
|
+
| `effects` | часы, случайность и имя события в логе |
|
|
104
|
+
| `api` | чем маршрут отвечает и что обязан объявить |
|
|
105
|
+
| `calls` | функция, у которой есть список мест, откуда её зовут |
|
|
106
|
+
| `hygiene` | потолок у зависимости |
|
|
107
|
+
|
|
108
|
+
<details>
|
|
109
|
+
<summary>Все двадцать семь</summary>
|
|
110
|
+
|
|
111
|
+
| Код | Что падает | Вид |
|
|
112
|
+
|---|---|:-:|
|
|
113
|
+
| `confined-imports` | пакет импортируется вне отведённых ему мест | файл |
|
|
114
|
+
| `sealed-imports` | запечатанная зона импортирует чужой пакет | файл |
|
|
115
|
+
| `class-modules` | в модуле лежит то, чего его директория не допускает | файл |
|
|
116
|
+
| `class-placement` | класс лежит не там, где лежат классы его вида | файл |
|
|
117
|
+
| `operation-shape` | операция устроена не как операция | файл |
|
|
118
|
+
| `required-class` | модуль не объявил класс, ради которого лежит в этой директории | файл |
|
|
119
|
+
| `keyword-only-arguments` | подпись записана не полностью | файл |
|
|
120
|
+
| `function-length` | функция длиннее лимита | файл |
|
|
121
|
+
| `module-length` | модуль длиннее лимита | файл |
|
|
122
|
+
| `nesting` | управляющие конструкции вложены глубже предела | файл |
|
|
123
|
+
| `signature-layout` | список из двух и более элементов записан в одну строку | файл |
|
|
124
|
+
| `annotation-shapes` | форма названа так, что поля в ней безымянные | файл |
|
|
125
|
+
| `config-fields` | поле настроек ничем не ограничено | файл |
|
|
126
|
+
| `confined-types` | поле в этой части дерева объявлено запрещённым здесь типом | файл |
|
|
127
|
+
| `constant-annotations` | константа не сказала типом, что она константа | файл |
|
|
128
|
+
| `frozen-dataclasses` | dataclass в зоне объявлен без нужных аргументов | файл |
|
|
129
|
+
| `bound-checks` | ограниченная колонка не повторила своё ограничение как CHECK | файл |
|
|
130
|
+
| `confined-calls` | названный метод позвали не там, где ему место | файл |
|
|
131
|
+
| `model-boundary` | ORM-модель объявлена, собрана или отдана не там | файл |
|
|
132
|
+
| `model-columns` | колонка собрана не из того материала | файл |
|
|
133
|
+
| `raw-sql` | SQL написан строкой там, где хватило бы выражения | файл |
|
|
134
|
+
| `statement-keys` | запрос называет колонку строкой или ходит в базу в цикле | файл |
|
|
135
|
+
| `schema-drift` | модели и миграции описывают уже разные схемы | среда |
|
|
136
|
+
| `determinism` | код сам читает часы, случайность или новый идентификатор | файл |
|
|
137
|
+
| `log-events` | событие в логе названо чем-то кроме члена перечисления | файл |
|
|
138
|
+
| `endpoint-declarations` | маршрут не сказал, чем он отвечает | файл |
|
|
139
|
+
| `confined-functions` | названная функция позвана не оттуда, откуда ей можно | файл |
|
|
140
|
+
| `dependency-bounds` | зависимость может уехать на версию, которую никто не запускал | проект |
|
|
141
|
+
|
|
142
|
+
</details>
|
|
143
|
+
|
|
144
|
+
Каждое правило объясняет себя целиком — `py-checks explain <код>` печатает
|
|
145
|
+
докстринг с причиной и список настроек. Заготовка таблиц для типового сервиса
|
|
146
|
+
лежит в [`docs/service.md`](docs/service.md).
|
|
147
|
+
|
|
148
|
+
### Три вида правил
|
|
149
|
+
|
|
150
|
+
Что правилу дают на суд, оно объявляет само — полем `scope`:
|
|
151
|
+
|
|
152
|
+
- **файл** — разобранный исходник; таких большинство;
|
|
153
|
+
- **проект** — корень: манифест, согласие файлов репозитория между собой;
|
|
154
|
+
- **среда** — то же, но нужна живая база или долгий прогон. В обычный прогон такое не входит: `py-checks run --all` или по имени, место ему в CI.
|
|
155
|
+
|
|
156
|
+
## Пометки
|
|
157
|
+
|
|
158
|
+
Снять правило со строки можно, но придётся объяснить зачем:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
text("SELECT 1") # db-ok: raw-sql: проба живости, формы ORM нет
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Каноническая форма — `# check-ok: <код>: <причина>`, она снимает ровно одно
|
|
165
|
+
правило. У каждой группы есть своё короткое слово (`# db-ok`, `# type-ok`,
|
|
166
|
+
`# signature-ok`, …): человек помнит группу, а не двадцать семь кодов.
|
|
167
|
+
|
|
168
|
+
Пометка без кода, с опечаткой в коде или без причины — сама нарушение. Молча
|
|
169
|
+
неработающая пометка выглядит как отключённая проверка, а на деле проверка
|
|
170
|
+
работает и просто её не видит.
|
|
171
|
+
|
|
172
|
+
## pre-commit
|
|
173
|
+
|
|
174
|
+
```yaml
|
|
175
|
+
- repo: https://github.com/Armontex/py-checks
|
|
176
|
+
rev: v0.1.0
|
|
177
|
+
hooks:
|
|
178
|
+
- id: py-checks
|
|
179
|
+
args: [--fix] # починить то, что чинится само
|
|
180
|
+
- id: py-checks-sync # контракты и `.env.example` собраны заново
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Хук один, а не по хуку на правило: что включено, решает конфиг проекта. Набор
|
|
184
|
+
правил зовут `args: [--select, "<код>,<код>"]`; повторённый флаг делает то же
|
|
185
|
+
самое.
|
|
186
|
+
|
|
187
|
+
Ставить его нужно **до** `ruff-format`: автофикс ставит символы, а не колонки,
|
|
188
|
+
и раскладывает подпись форматтер проекта.
|
|
189
|
+
|
|
190
|
+
Всё остальное — ruff, pyright, import-linter, commitizen — проект объявляет
|
|
191
|
+
сам: у каждого есть свой хук, написанный его же авторами.
|
|
192
|
+
|
|
193
|
+
## Контракты импортов
|
|
194
|
+
|
|
195
|
+
Слои проекта описываются один раз, а `.importlinter` под них собирается:
|
|
196
|
+
|
|
197
|
+
```toml
|
|
198
|
+
[contracts.layers]
|
|
199
|
+
domain = ["domain"]
|
|
200
|
+
application = ["application", "domain"]
|
|
201
|
+
presentation = ["presentation", "application"]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
py-checks sync # собрать
|
|
206
|
+
py-checks sync --check # упасть, если файл отстал
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Собирается он не только из таблицы, но и из того, что лежит на диске: слой,
|
|
210
|
+
которого нет, в контракт не попадает — иначе import-linter упал бы на первом же
|
|
211
|
+
несуществующем модуле. Гоняет граф по собранному файлу хук самого import-linter.
|
|
212
|
+
|
|
213
|
+
## Пример окружения
|
|
214
|
+
|
|
215
|
+
Имя переменной знает поле настроек — оно объявляет его `validation_alias`, и
|
|
216
|
+
за этим следит `config-fields`. Значит, `.env.example` выводится из тех же
|
|
217
|
+
классов, что переменные и читают, и вести его рядом руками незачем: расходится
|
|
218
|
+
он молча, а замечают это, когда переменной не оказалось на проде.
|
|
219
|
+
|
|
220
|
+
```toml
|
|
221
|
+
[env-example]
|
|
222
|
+
settings = ["myservice.config.settings:Settings"]
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Тот же `py-checks sync` — файл собирается вместе с контрактами. Достаточно
|
|
226
|
+
корневого класса: свои секции он уже перечислил собственными полями, и
|
|
227
|
+
повторять их список в настройках значит завести второй, который с первым
|
|
228
|
+
разойдётся. Поле-секция переменной не становится — у него своего имени в
|
|
229
|
+
окружении нет. В файл едут имя переменной, значение по умолчанию, первый абзац
|
|
230
|
+
докстринга класса и `description` поля, если оно у него есть, — так объяснение
|
|
231
|
+
живёт рядом с полем, а не в файле, который его переживёт.
|
|
232
|
+
|
|
233
|
+
Классы импортируются, а не читаются текстом: имя переменной — значение
|
|
234
|
+
атрибута, собранное вызовом, и прочитать его исходником значит выполнить этот
|
|
235
|
+
вызов самому.
|
|
236
|
+
|
|
237
|
+
## Своё правило
|
|
238
|
+
|
|
239
|
+
Правило — это класс с четырьмя полями и `run`, объявленный через entry points.
|
|
240
|
+
Форкать библиотеку не нужно:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
# myproject_checks/_no_print.py
|
|
244
|
+
import ast
|
|
245
|
+
from collections.abc import Iterator
|
|
246
|
+
from typing import ClassVar, Final
|
|
247
|
+
|
|
248
|
+
from py_checks.config import CheckSettings
|
|
249
|
+
from py_checks.core import ParsedFile, Scope, Violation
|
|
250
|
+
|
|
251
|
+
CODE: Final = "no-print"
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class NoPrint:
|
|
255
|
+
"""Падает, если в исходнике остался `print`."""
|
|
256
|
+
|
|
257
|
+
code: ClassVar[str] = CODE
|
|
258
|
+
Settings: ClassVar[type[CheckSettings]] = CheckSettings
|
|
259
|
+
scope: ClassVar[Scope] = Scope.FILE
|
|
260
|
+
marker: ClassVar[str] = "# my-ok"
|
|
261
|
+
|
|
262
|
+
@classmethod
|
|
263
|
+
def run(cls, *, file: ParsedFile, settings: CheckSettings) -> Iterator[Violation]:
|
|
264
|
+
for node in ast.walk(file.tree):
|
|
265
|
+
if isinstance(node, ast.Call) and getattr(node.func, "id", "") == "print":
|
|
266
|
+
yield Violation.from_node(
|
|
267
|
+
node=node,
|
|
268
|
+
path=file.path,
|
|
269
|
+
code=CODE,
|
|
270
|
+
message="print в исходнике; событие пишут логом",
|
|
271
|
+
)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
```toml
|
|
275
|
+
[project.entry-points."py_checks.checks"]
|
|
276
|
+
no-print = "myproject_checks:NoPrint"
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Дальше оно ведёт себя как родное: попадает в `list`, в `explain`, слушается
|
|
280
|
+
`ignore` и снимается пометкой.
|
|
281
|
+
|
|
282
|
+
## Команды
|
|
283
|
+
|
|
284
|
+
| Команда | |
|
|
285
|
+
|---|---|
|
|
286
|
+
| `py-checks run [пути]` | прогнать проверки; `0` — чисто, `1` — есть нарушения |
|
|
287
|
+
| `py-checks run --fix` | наложить правки, которые однозначны |
|
|
288
|
+
| `py-checks run --select <код>,<код>` | только названные правила, несмотря на `ignore` |
|
|
289
|
+
| `py-checks run --all` | вместе с правилами, которым нужна живая среда |
|
|
290
|
+
| `py-checks list` | все правила: код, состояние, строка описания |
|
|
291
|
+
| `py-checks explain <код>` | что правило требует и какие у него настройки |
|
|
292
|
+
| `py-checks sync [--check]` | собрать контракты импортов и `.env.example` |
|
|
293
|
+
|
|
294
|
+
## Разработка
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
uv sync
|
|
298
|
+
uv run pre-commit install
|
|
299
|
+
uv run pytest -q
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Проверка описывается не тестом, а папками с примерами: `tests/checks/<код>/`
|
|
303
|
+
с `ok/` и `bad/` внутри, снимок вывода сверяет syrupy. У правил про проект —
|
|
304
|
+
`tests/projects/<код>__<вариант>/`. Библиотека проверяет сама себя: правило,
|
|
305
|
+
которое не выдерживает свой же репозиторий, до чужого доезжать не должно.
|
|
306
|
+
|
|
307
|
+
Соглашения репозитория — в [`AGENTS.md`](AGENTS.md).
|
|
308
|
+
|
|
309
|
+
## Лицензия
|
|
310
|
+
|
|
311
|
+
[MIT](LICENSE) — © 2026 Armontex.
|