линтеры скриптов: ruff и pyrefly через uv, починены 41 находка

Скрипты остаются на голом python 3.12 без зависимостей: pyproject.toml живёт
только в этом репозитории и держит линтеры, а не зависимости скриптов.

Ноль зависимостей охраняется дважды: banned-api у ruff ловит частые соблазны по
имени, pyrefly видит окружение без ничего и не разрешает любой сторонний импорт.

Версии прибиты точно, uv.lock под git: обновление линтера меняет набор находок,
а находки правятся руками в скриптах, которые уезжают в чужие проекты.

RUF001–003 выключены — весь текст русский, 311 срабатываний из 338 шум.
av-dev-backlog исключён: заморожен до удаления, правка без выгоды.

Из 41 находки содержательных две: мёртвая ques в check и два места, где
find_entry_index может вернуть None прямо в list.pop и range. По ревью там
стоит raise, а не continue: тихий пропуск превратил бы сломанный инвариант в
отчёт «индексы согласованы».

os из tasks.py ушёл целиком, fail() в docs.py объявлен NoReturn.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-08-03 15:28:10 +03:00
co-authored by Claude Opus 5
parent 68218208b7
commit c692436b91
8 changed files with 278 additions and 42 deletions
+2
View File
@@ -1,2 +1,4 @@
*.pyc
__pycache__/
.venv/
.ruff_cache/
+64
View File
@@ -695,3 +695,67 @@ healthlog.
желательным. Пересобрать на шаге 1.7.
39. **Замер — единственный шаг, который нельзя переставить.** Всё остальное в
порядке 1–5 можно тасовать; шаг 3 стоит перед шагом 5 жёстко.
## 9. Линтеры скриптов (2026-08-03)
### Что было
Три скрипта на python, 3600 строк, ни одной проверки. `tasks.py` — 2450 строк,
которые ходят по файловой системе, переименовывают и удаляют файлы задач.
Требование к самим скриптам прежнее и не обсуждается: **голый `python3` 3.12,
ноль внешних зависимостей** — они лежат рядом со скиллами и запускаются в
чужом проекте, где ничего ставить нельзя.
### Решено
**DD. `pyproject.toml` в корне `dev-skills`, зависимости через `uv`.** Файл
живёт только здесь и не уезжает никуда: он держит **линтеры**, а не зависимости
скриптов. Скрипты остаются запускаемыми любым `python3` — это проверено прогоном
всех операций через `/usr/bin/python3`, а не через `.venv`.
**EE. Ноль зависимостей охраняется двумя способами, и главный — второй.**
`banned-api` у ruff ловит частые соблазны по имени (`requests`, `yaml`,
`pydantic`, `click`, `rich`) — список заведомо неполный. Настоящий страж —
pyrefly: в окружении нет ничего, кроме линтеров, поэтому **любой** сторонний
импорт у него не разрешается. Первый способ даёт понятное сообщение, второй —
полноту.
**FF. Версии линтеров прибиты точно** (`ruff==0.16.1`, `pyrefly==1.2.0`) плюс
`uv.lock` в git. Обновление линтера меняет набор находок, а находки правятся
руками в скриптах, которые уезжают в чужие проекты. Обновление обязано быть
отдельной осознанной правкой, а не побочным эффектом `uv sync`.
**GG. `RUF001``RUF003` выключены.** Весь текст скриптов русский: сообщения,
докстроки, комментарии. «Похожая на латиницу кириллица» здесь норма, а не
опечатка, и три этих правила давали **311 срабатываний из 338** — шум, в котором
тонут остальные 27.
**HH. `av-dev-backlog` исключён из проверки.** Плагин помечен устаревшим и живёт
до перевода последнего проекта, после чего удаляется целиком. Шесть его находок
косметические (`os.replace`, `l` как имя), а правка замороженного кода без
тестов — риск без выгоды. Исключение уходит вместе с плагином.
**II. Голый `except Exception` разрешён только помеченный.** Правило `BLE`
включено, а два места последнего рубежа (`main` обоих скриптов, код выхода 4 по
словарю) несут `# noqa: BLE001` с причиной. Так третий такой except не
появляется молча.
### Что из этого следует
40. **Найдено и починено 27 находок ruff и 14 pyrefly.** Содержательных две:
мёртвая переменная `ques` в `check` (вычислялась и не использовалась —
вопросы проверяет `questions_open`) и два места в `check --fix`, где
`find_entry_index` может вернуть `None`, а результат идёт прямо в
`list.pop` и в `range`. Оба сегодня недостижимы, и недостижимость держалась
на рассуждении о вызывающем коде, а не на проверке.
*Поправлено по ревью:* там стоит `raise`, а не `continue`. Тихий пропуск
превратил бы сломанный инвариант в отчёт «индексы согласованы» — то есть в
враньё; громкий отказ кодом 4 честнее.
41. **`os` из `tasks.py` ушёл целиком.** `os.replace``Path.replace`,
`os.path.basename``Path.name`; импорт стал не нужен.
42. **`fail()` в `docs.py` объявлен `NoReturn`.** Без этого `read_config`
выглядел как возвращающий неинициализированное значение — и это ровно то,
что читатель кода тоже не мог знать наверняка.
43. **Проверка не входит ни в один гейт.** CI у репозитория нет, хука нет;
запускается руками командой из README. Заводить хук ради двух скриптов,
которые правятся раз в месяц, — плата ритуалом без выгоды.
+23
View File
@@ -113,4 +113,27 @@ openspec/
<plugin>/skills/<skill>/references/ что читается по ссылке из скилла
<plugin>/skills/<skill>/scripts/ tasks.py, docs.py
<plugin>/agents/ charter'ы сабагентов
pyproject.toml линтеры скриптов, только для этого репозитория
```
## Проверка скриптов
`tasks.py` и `docs.py` запускаются **где угодно голым `python3` 3.12 без
установки чего-либо** — они лежат рядом со скиллами и работают в любом проекте.
`pyproject.toml` в корне не меняет этого: он живёт только здесь и держит
линтеры, а не зависимости скриптов.
```
uv sync # ставит ruff и pyrefly в .venv, версии прибиты точно
uv run ruff check . # правила; --fix для безопасных починок
uv run pyrefly check # типы
```
Ноль внешних зависимостей охраняется двумя способами: `banned-api` у ruff ловит
частые соблазны по имени, а pyrefly видит окружение, где нет ничего кроме
линтеров, и любой сторонний импорт у него не разрешается. Список запретов —
не перечень мира, настоящий страж второй.
`av-dev-backlog` из проверки исключён намеренно: плагин помечен устаревшим и
живёт до перевода последнего проекта, после чего удаляется целиком. Правки в
замороженный код — риск без выгоды.
+9
View File
@@ -72,6 +72,15 @@
- [x] `REMAINING.md` пересобрать: пункт 2 отменён, четыре вопроса закрыты,
калибровка стала обязательной (38)
### 1.7 Линтеры скриптов (тема 9)
- [x] `pyproject.toml`: ruff + pyrefly через `uv`, версии прибиты (DD, FF)
- [x] запрет внешних зависимостей двумя способами: `banned-api` + пустое
окружение pyrefly (EE)
- [x] `RUF001``RUF003` выключены, `av-dev-backlog` исключён (GG, HH)
- [x] починены 27 находок ruff и 14 pyrefly; `os` из `tasks.py` ушёл (40, 41)
- [x] раздел «Проверка скриптов» в `README.md`
## 2. healthlog — первая боевая проверка
- [ ] `canon adopt`; `docs/backlog/``docs/tasks/`
+2 -1
View File
@@ -23,6 +23,7 @@ import subprocess
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import NoReturn
CANON_VERSION = 1
@@ -118,7 +119,7 @@ class Report:
self.skipped.append(msg)
def fail(code: int, msg: str) -> None:
def fail(code: int, msg: str) -> NoReturn:
print(f"ОТКАЗ: {msg}", file=sys.stderr)
sys.exit(code)
+47 -41
View File
@@ -79,7 +79,6 @@ av-dev, и подгоняется под него проект. Имена вн
import argparse
import datetime
import json
import os
import re
import subprocess
import sys
@@ -210,7 +209,7 @@ def dir_within_cwd(root: Path) -> bool:
def write_atomic(path: Path, text: str) -> None:
tmp = path.with_name(path.name + ".tmp")
tmp.write_text(text, encoding="utf-8")
os.replace(tmp, path)
tmp.replace(path)
class Plan:
@@ -255,7 +254,7 @@ class Plan:
tmp.write_text(text, encoding="utf-8")
staged.append((tmp, path))
for tmp, path in staged:
os.replace(tmp, path)
tmp.replace(path)
for path in self.deletes:
path.unlink(missing_ok=True)
@@ -310,7 +309,7 @@ def _read_json(path: Path) -> dict:
try:
data = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as e:
raise Env(f"{path}: не разбирается как JSON — {e}")
raise Env(f"{path}: не разбирается как JSON — {e}") from e
if not isinstance(data, dict):
raise Env(f"{path}: ожидался объект с настройками")
return data
@@ -411,7 +410,7 @@ def parse_entries(lines: list[str]) -> tuple[dict[str, dict], list[str]]:
m = INDEX_ENTRY.match(line)
if m:
title, target, hook = m.group(1), m.group(2), (m.group(3) or "").strip()
entries[os.path.basename(target)] = {
entries[Path(target).name] = {
"title": title, "section": section, "hook": hook,
"line": num, "target": target}
return entries, sections
@@ -434,7 +433,7 @@ def index_lint(lines: list[str], label: str) -> list[str]:
errors.append(f"{label}:{num}: строка-пункт не по формату"
f" «- [Заголовок](items/slug.md) — хук»")
continue
target = os.path.basename(m.group(2))
target = Path(m.group(2)).name
if section is None:
errors.append(f"{label}:{num}: {target} стоит до первой секции")
if target in seen:
@@ -527,7 +526,7 @@ def touched_map(lay: Layout) -> dict[str, str]:
if DATE_RE.fullmatch(line):
cur = line # лог новейшие сверху → первая дата и есть последняя правка
elif cur:
dates.setdefault(os.path.basename(line), cur)
dates.setdefault(Path(line).name, cur)
return dates
@@ -650,7 +649,6 @@ def check(lay: Layout, fix: bool = False) -> int:
known = {k: {s.lower() for s in sections[k]} for k in lay.indexes}
goal_slugs = {p[:-3] for p, t in tasks.items() if t["type"] == GOAL}
goal_of_sprint, _ = sprint_goal(lay)
ques = lay.cfg["questions_heading"].lower()
for s in sections["backlog"]:
if s.lower() in BLOCKER_SECTIONS:
@@ -692,7 +690,7 @@ def check(lay: Layout, fix: bool = False) -> int:
elif task["section"] not in known[home]:
errors.append(f"{name}: секция «{task['section']}» не совпадает ни с одной"
f" секцией {label[home]} ({', '.join(sections[home])})")
elif place and place != "sprint" and entry["section"] \
elif place is not None and place != "sprint" and entry and entry["section"] \
and entry["section"].lower() != task["section"]:
errors.append(f"{name}: секция в файле «{task['section']}»,"
f" а в {label[place]} — «{entry['section']}»")
@@ -708,7 +706,7 @@ def check(lay: Layout, fix: bool = False) -> int:
errors.append(f"{name}: хук разошёлся (истина в файле)\n"
f" файл: {task['hook']}\n"
f" индекс: {entry['hook']}")
elif entry and not task["hook"] and entry["hook"]:
elif place is not None and entry and not task["hook"] and entry["hook"]:
errors.append(f"{name}: хук есть в {label[place]}, а в файле нет —"
f" `check --fix` перенесёт его в мета-строку")
elif not task["hook"]:
@@ -944,7 +942,7 @@ def build_meta(section: str, reason: str, hook: str, tags: list[str]) -> str:
def section_headers(lines: list[str]) -> list[tuple[int, str]]:
return [(i, m.group(1)) for i, l in enumerate(lines) if (m := SECTION.match(l))]
return [(i, m.group(1)) for i, line in enumerate(lines) if (m := SECTION.match(line))]
def find_section(lines: list[str], name: str) -> tuple[int | None, str]:
@@ -955,8 +953,8 @@ def find_section(lines: list[str], name: str) -> tuple[int | None, str]:
def find_entry_index(lines: list[str], slug: str) -> int | None:
for i, l in enumerate(lines):
m = INDEX_ENTRY.match(l)
for i, line in enumerate(lines):
m = INDEX_ENTRY.match(line)
if m and Path(m.group(2)).name == f"{slug}.md":
return i
return None
@@ -1011,11 +1009,13 @@ def meta_updated(path: Path, section: str | None = None, reason: str | None = No
mi = next((i for i in range(1, len(flines)) if flines[i].strip()), None)
if mi is None or not META_FIELD.match(flines[mi].strip()):
return None
chunks: list[str | None] = flines[mi].split("·")
chunks: list[str | None] = [*flines[mi].split("·")]
seen_section = seen_tags = seen_hook = False
tags_at = None
for idx, chunk in enumerate(chunks):
f = META_FIELD.match((chunk or "").strip())
if chunk is None: # вычеркнутое поле — своим же проходом ниже
continue
f = META_FIELD.match(chunk.strip())
if not f:
continue
key = f.group(1).strip().lower()
@@ -1230,7 +1230,7 @@ def cmd_edit(lay: Layout, a: argparse.Namespace) -> int:
plan.file(path, new_text)
if old_home != new_home:
old_lines, ei = places[old_home]
entry = old_lines.pop(ei)
old_lines.pop(ei) # строка в новом индексе собирается заново
plan.index(lay, old_home, old_lines)
target_lines = read_lines(lay.index(new_home))
insert_entry(target_lines, section, entry_line(lay, h1, a.slug, hook))
@@ -1286,8 +1286,8 @@ def cmd_move(lay: Layout, a: argparse.Namespace) -> int:
entry = lines.pop(ei)
try:
insert_entry(lines, section, entry, a.after, a.first)
except KeyError:
raise Usage(f"--after {a.after}: такой строки в секции «{section}» нет")
except KeyError as e:
raise Usage(f"--after {a.after}: такой строки в секции «{section}» нет") from e
plan = Plan()
plan.file(path, new_text)
@@ -1481,8 +1481,8 @@ def cmd_sprint_start(lay: Layout, a: argparse.Namespace) -> int:
if any(f"{SPRINT_TAG}{slug}" in t["tags"] for t in tasks.values()):
print(f" внимание: тег {SPRINT_TAG}{slug} уже стоит на задачах прошлого спринта"
f" — урожаи склеятся; задай другой `--slug`")
lines = sprint_header(lay, a.goal, goal["title"], date, slug) \
+ [f"## {lay.cfg['sprint_section']}", ""]
lines = [*sprint_header(lay, a.goal, goal["title"], date, slug),
f"## {lay.cfg['sprint_section']}", ""]
plan = Plan()
plan.index(lay, "sprint", lines)
plan.commit()
@@ -1591,8 +1591,8 @@ def cmd_sprint_drop(lay: Layout, a: argparse.Namespace) -> int:
plan.index(lay, "sprint", sprint_lines)
plan.commit()
print(f"вышло из спринта: {', '.join(dropped)} — причина записана в мета-строку")
print(f" живого предложения оставаться не должно; наработки, которые жалко,"
f" переносятся в тело задачи текстом")
print(" живого предложения оставаться не должно; наработки, которые жалко,"
" переносятся в тело задачи текстом")
return EXIT_OK
@@ -1656,22 +1656,22 @@ def apply_fixes(lay: Layout) -> tuple[list[str], list[str]]:
for kind, lines in idx.items():
seen: set[str] = set()
out: list[str] = []
for l in lines:
m = INDEX_ENTRY.match(l)
for line in lines:
m = INDEX_ENTRY.match(line)
if m and Path(m.group(2)).name in seen:
fixed.append(f"{lay.name(kind)}: убран дубль строки {Path(m.group(2)).name}")
dirty.add(kind)
continue
if m:
seen.add(Path(m.group(2)).name)
out.append(l)
out.append(line)
idx[kind] = out
# 2. Хук: истина в файле. Если в файле его нет, а в индексе есть — это
# старый формат, и единственный экземпляр надо спасти в файл.
for kind, lines in idx.items():
for i, l in enumerate(lines):
m = INDEX_ENTRY.match(l)
for i, line in enumerate(lines):
m = INDEX_ENTRY.match(line)
if not m:
continue
name = Path(m.group(2)).name
@@ -1731,8 +1731,10 @@ def apply_fixes(lay: Layout) -> tuple[list[str], list[str]]:
f" (есть: {', '.join(n for _, n in section_headers(idx[home]))})")
continue
ei = find_entry_index(idx[kind], name[:-3])
entry = idx[kind].pop(ei)
insert_entry(idx[home], section, entry)
if ei is None: # сюда попали по where — строка обязана быть
raise RuntimeError(f"{name}: строка в {lay.name(kind)} пропала посреди"
f" прохода — чинить нечего, отчёт был бы враньём")
insert_entry(idx[home], section, idx[kind].pop(ei))
fixed.append(f"{name}: строка перенесена {lay.name(kind)}{lay.name(home)}"
f" (секция «{section}») — по типу «{task['type']}» её место там")
dirty.add(kind)
@@ -1744,8 +1746,11 @@ def apply_fixes(lay: Layout) -> tuple[list[str], list[str]]:
if hi is None:
continue
ei = find_entry_index(idx[kind], name[:-3])
cur = next((SECTION.match(idx[kind][j]).group(1)
for j in range(ei, -1, -1) if SECTION.match(idx[kind][j])), None)
if ei is None:
raise RuntimeError(f"{name}: строка в {lay.name(kind)} пропала посреди"
f" прохода — чинить нечего, отчёт был бы враньём")
cur = next((m.group(1) for j in range(ei, -1, -1)
if (m := SECTION.match(idx[kind][j]))), None)
if cur and cur.lower() != section.lower():
insert_entry(idx[kind], section, idx[kind].pop(ei))
fixed.append(f"{lay.name(kind)}: перенесена в секцию «{section}»: {name}")
@@ -1800,7 +1805,7 @@ def init_files(lay: Layout, sections: list[str], plan_sections: list[str],
out[lay.index("rejected")] = (
"# Ушедшее без реализации\n\n"
"Задачи, покинувшие беклог **без реализации**, с причиной и датой.\n"
f"Пишется `tasks.py close --reason`. Реализованные сюда не идут — у них\n"
"Пишется `tasks.py close --reason`. Реализованные сюда не идут — у них\n"
"есть коммит. Это первое место, куда смотрит дедупликация при заведении.\n\n"
"<!-- - ГГГГ-ММ-ДД `slug` — Заголовок. Причина: … Была секция: … -->\n")
return out
@@ -1910,12 +1915,13 @@ def scan_old_backlog(src: Path) -> dict:
"questions_heading": qh,
"source": str(path),
})
for name, entry in entries.items():
if name not in seen:
found["unclassified"].append({
"what": f"строка индекса «{entry['title']}» → {name}",
"where": f"{src / index_name}:{entry['line']}",
"why": "файла нет — переносить нечего, текст только в строке"})
if index_name:
for name, entry in entries.items():
if name not in seen:
found["unclassified"].append({
"what": f"строка индекса «{entry['title']}» → {name}",
"where": f"{src / index_name}:{entry['line']}",
"why": "файла нет — переносить нечего, текст только в строке"})
if graveyard:
for line in read_lines(src / graveyard):
if line.startswith("- "):
@@ -2140,7 +2146,7 @@ def cmd_adopt_apply(a: argparse.Namespace) -> int:
try:
pl = json.loads(plan_path.read_text(encoding="utf-8"))
except json.JSONDecodeError as e:
raise Usage(f"{plan_path}: не разбирается как JSON — {e}")
raise Usage(f"{plan_path}: не разбирается как JSON — {e}") from e
root = Path(pl["target"])
if not dir_within_cwd(root):
@@ -2264,7 +2270,7 @@ def cmd_adopt_apply(a: argparse.Namespace) -> int:
wr.commit()
# --- перекрёстные ссылки: тем же проходом, иначе они останутся битыми ---
ref_paths: list[Path] = list(lay.items.glob("*.md")) + [lay.index("rejected")]
ref_paths: list[Path] = [*lay.items.glob("*.md"), lay.index("rejected")]
for r in (a.refs or []):
p = Path(r)
ref_paths += sorted(p.rglob("*.md")) if p.is_dir() else [p]
@@ -2446,6 +2452,6 @@ if __name__ == "__main__":
sys.exit(EXIT_ENV)
except KeyboardInterrupt:
sys.exit(EXIT_INTERNAL)
except Exception as e: # noqa: BLE001
except Exception as e: # noqa: BLE001 — последний рубеж, код 4 по словарю
print(f"внутренний сбой ({type(e).__name__}): {e}", file=sys.stderr)
sys.exit(EXIT_INTERNAL)
+65
View File
@@ -0,0 +1,65 @@
[project]
name = "dev-skills"
version = "0"
description = "Маркетплейс плагинов av-dev; здесь только линтеры скриптов скиллов"
requires-python = ">=3.12"
dependencies = []
# Версии прибиты точно: обновление линтера меняет набор находок, а находки тут
# правятся руками в скриптах, которые уезжают в чужие проекты. Обновление —
# отдельная осознанная правка, а не побочный эффект `uv sync`.
[dependency-groups]
dev = ["ruff==0.16.1", "pyrefly==1.2.0"]
[tool.uv]
package = false
[tool.ruff]
target-version = "py312"
line-length = 88
# backlog.py заморожен: плагин помечен УСТАРЕЛ и живёт до перевода последнего
# проекта, после чего удаляется целиком. Правки в него — риск без выгоды.
exclude = ["av-dev-backlog"]
[tool.ruff.lint]
select = [
"F", # pyflakes: неиспользованное, неопределённое, битые f-строки
"E", "W",# pycodestyle
"I", # порядок импортов
"UP", # устаревшие для 3.12 конструкции
"B", # bugbear: изменяемый дефолт, забытый raise, цикл с замыканием
"SIM", # упрощения
"RET", # возвраты
"PTH", # os.path → pathlib
"RUF", # правила самого ruff
"TID", # запрет внешних зависимостей, см. banned-api ниже
"BLE", # голый except Exception — только там, где помечен noqa
]
ignore = [
"E501", # длину строк держит formatter, а не проверка
# Весь текст скриптов — русский: сообщения, докстроки, комментарии.
# «Похожая на латиницу кириллица» здесь не опечатка, а норма, и три этих
# правила дают 311 срабатываний из 338 на пустом месте.
"RUF001", "RUF002", "RUF003",
]
[tool.ruff.lint.flake8-tidy-imports.banned-api]
# Скрипты обязаны работать на голом python 3.12 без установки чего-либо:
# они лежат рядом со скиллами и запускаются в любом проекте. Список — не
# полный перечень мира, а частые соблазны; настоящий страж — pyrefly, у
# которого в окружении нет ничего, кроме линтеров.
"requests".msg = "внешняя зависимость; скрипты работают на голом python 3.12"
"yaml".msg = "внешняя зависимость; для json есть stdlib, для yaml — разбор руками"
"pydantic".msg = "внешняя зависимость; скрипты работают на голом python 3.12"
"click".msg = "внешняя зависимость; аргументы разбирает argparse"
"rich".msg = "внешняя зависимость; вывод — обычный print"
[tool.ruff.format]
quote-style = "double"
[tool.pyrefly]
project-includes = [
"av-dev-pm/skills/tasks/scripts/tasks.py",
"av-dev-pm/skills/canon/scripts/docs.py",
]
python-version = "3.12"
Generated
+66
View File
@@ -0,0 +1,66 @@
version = 1
revision = 3
requires-python = ">=3.12"
[[package]]
name = "dev-skills"
version = "0"
source = { virtual = "." }
[package.dev-dependencies]
dev = [
{ name = "pyrefly" },
{ name = "ruff" },
]
[package.metadata]
[package.metadata.requires-dev]
dev = [
{ name = "pyrefly", specifier = "==1.2.0" },
{ name = "ruff", specifier = "==0.16.1" },
]
[[package]]
name = "pyrefly"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/89/01/a86e9f24722b095c3f88e3616132b75a21b0df53804bdc6a45314dd4d93c/pyrefly-1.2.0.tar.gz", hash = "sha256:5485f960fc2481617068c918335c39ab1507ef90b6b5bd35bf57726e60e73185", size = 6243654, upload-time = "2026-08-01T02:56:27.592Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7d/9d/3c0ef1d4843987b22f996ed381ec9cf5a3b1273e29804db276252e4c95eb/pyrefly-1.2.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7f46d983ac49ddd2b043694960a01dc6a19a5cfd8eec609d6bd9c42866f91b4e", size = 14026305, upload-time = "2026-08-01T02:56:02.611Z" },
{ url = "https://files.pythonhosted.org/packages/0a/06/03bbb78fbea54cdc65b626619f3597d5611aca4fdef11e72a4e8360e7e63/pyrefly-1.2.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:756f669b5555090f5c1a4fef30db1785fabe657764f7e4e6dc88994dfb8ca82d", size = 13463880, upload-time = "2026-08-01T02:56:04.93Z" },
{ url = "https://files.pythonhosted.org/packages/13/5a/7d8bc00a38e93bbc9c3e7bd14d305f7948717e667c9bcddeab9dd42fd255/pyrefly-1.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3465812ce5ef4781fb592edbf2724547296f0a3124be115d73c7e8b2401862d", size = 13907329, upload-time = "2026-08-01T02:56:07.104Z" },
{ url = "https://files.pythonhosted.org/packages/be/94/9e08b4bf799d0b8f36b55a2783c7ba5f51730cf0632a85a67b5b5ed876cd/pyrefly-1.2.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5de7b2ad2bba5c8055181681a84b74143eac2234a48ba5d1b7ed7e7a722b02bd", size = 15039020, upload-time = "2026-08-01T02:56:09.208Z" },
{ url = "https://files.pythonhosted.org/packages/5b/bd/bca5fd0c80f4daf8ee6903a29df9f3de1feb05ff0946b8f35ec8c5096b13/pyrefly-1.2.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25822ea9505f589ea8a725e4268b475132fb89e038fbf092e446510443ac142a", size = 14986199, upload-time = "2026-08-01T02:56:11.924Z" },
{ url = "https://files.pythonhosted.org/packages/97/f7/f07087f3d185ad2eced0c56cef89ca5474dfb4ff25f146cd50a861c97553/pyrefly-1.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90efe75e17491ef5d636e10469e9278d7d0256b3b4c5e1f4750069bf3ae0f5d1", size = 14393715, upload-time = "2026-08-01T02:56:14.143Z" },
{ url = "https://files.pythonhosted.org/packages/d3/70/0d142c320e284b9e3ce35e9b1e58b8ce2ee1f578f2a7234bc30e5022b94f/pyrefly-1.2.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:368aaf7eee4f511ddc0f8e564cf14e01ab2f10b0db9105c6d5b153bf498d07bf", size = 13933008, upload-time = "2026-08-01T02:56:16.525Z" },
{ url = "https://files.pythonhosted.org/packages/5d/e8/e84f11b6e1f63fd453ad3654213b9a0f6f4de8cef6b58038eef2d0d5955d/pyrefly-1.2.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d52d5da7bc65fb7675fbaa80eda879d4f8787c494f04cac21603330d3abbdbbe", size = 14431827, upload-time = "2026-08-01T02:56:18.645Z" },
{ url = "https://files.pythonhosted.org/packages/0f/06/810d31380f66c75e1c0779a408d3b16117b1b368b57894f6aa66bef21686/pyrefly-1.2.0-py3-none-win32.whl", hash = "sha256:8c90751de8506d938e8f802659c74cf35bd7a0036510ee6c634a38eebb280bfa", size = 13229447, upload-time = "2026-08-01T02:56:20.921Z" },
{ url = "https://files.pythonhosted.org/packages/ed/98/4dafa3c7a1caed2dc8cc708dde09ba27963c7736508f55b626fff3024113/pyrefly-1.2.0-py3-none-win_amd64.whl", hash = "sha256:8a8964c224ccc4882730130955815de21ff443c1ac3f0b90685b19bf63848170", size = 14087387, upload-time = "2026-08-01T02:56:23.188Z" },
{ url = "https://files.pythonhosted.org/packages/1b/1c/df3cb0a2e5591660ded7a1836cd2f29dc48c91adb1c0a3a700a96f6d09e1/pyrefly-1.2.0-py3-none-win_arm64.whl", hash = "sha256:3a90bb8df39dfbac74b1f3b2e9d7c526b8f80568884c3944d955023a73ebf61e", size = 13430873, upload-time = "2026-08-01T02:56:25.425Z" },
]
[[package]]
name = "ruff"
version = "0.16.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/70/25/7113f6d5498888c5fb7db34081cba7d5971c4cb1bfb26819966eee68f003/ruff-0.16.1.tar.gz", hash = "sha256:fedad7c801dabd3fb9741d76aca39246e6ddd9ca446a015875207bf19f1e6bc7", size = 4877500, upload-time = "2026-07-30T19:37:01.379Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/bd/694da69368e0973de65df2ddc73ab18d43c469d5963d9b150911de6bc513/ruff-0.16.1-py3-none-linux_armv6l.whl", hash = "sha256:58edb313b88f0c5460a26adf5f39a37a3be789494a15e3e411e35fa78b89f9a0", size = 10839126, upload-time = "2026-07-30T19:36:13.697Z" },
{ url = "https://files.pythonhosted.org/packages/3f/f0/b626e5d5bd0dd9576263658ef12885e2288afd1029a48e26ffed65ec1ac1/ruff-0.16.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fde5a99e2f97479af66edd6622c6d5a2a7592c77cf4153d9e4428f5eeb55b60c", size = 11070253, upload-time = "2026-07-30T19:36:17.14Z" },
{ url = "https://files.pythonhosted.org/packages/83/63/f40acfb6b35b88623e71684942b552c3edd96035f5d98f313815f7b277de/ruff-0.16.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0d4c20532fca4f7fa609369161d968dd28f65d83dabbd61d8e9c7edbf7001f6", size = 10561425, upload-time = "2026-07-30T19:36:20.04Z" },
{ url = "https://files.pythonhosted.org/packages/aa/dd/14ec0e9c2b4d315547dd38765004b4863e354e1b52cb308272215d9f6f6d/ruff-0.16.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30affbcedf59ad5703d9c91f82266e02b47739f797e1a7b6e158e5526a6dae38", size = 10948879, upload-time = "2026-07-30T19:36:22.476Z" },
{ url = "https://files.pythonhosted.org/packages/33/e9/9d870cbae575030fdef595f04b4b97573c525b5497cce4f4498cf2f85446/ruff-0.16.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24e9c631573cbca9d20f1283f8f479b2afa4a8503504822bd71a293889f16743", size = 10643691, upload-time = "2026-07-30T19:36:24.914Z" },
{ url = "https://files.pythonhosted.org/packages/c4/09/12743d544e2173f53ecd27217c65f90d2bc0f8424a66a60339e56bbc0457/ruff-0.16.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b41bdd48fb420987a9b5212e4957c26ad4abce401fa9ea9d4d85843727945f4f", size = 11435354, upload-time = "2026-07-30T19:36:28.447Z" },
{ url = "https://files.pythonhosted.org/packages/7f/89/a1652b2daee52083c9554a6333b678a8b01d0400f976827bb87857f9449a/ruff-0.16.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0d1e1393b7648079e13669de1c1f4fde06d4583e84d8fd5c1551e0a77a2aa75", size = 12259033, upload-time = "2026-07-30T19:36:31.326Z" },
{ url = "https://files.pythonhosted.org/packages/16/96/ecdcb8c54ee7b123b487f807eb014e6e019155a0b81dfb669acd52f28ce3/ruff-0.16.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07bf434b1c95f4e093be4532068ef4fcf00924eb2ade8796075980902d6fd54a", size = 11667981, upload-time = "2026-07-30T19:36:34.394Z" },
{ url = "https://files.pythonhosted.org/packages/cd/90/c52e12e0d862e9572f2a33aa227409143520abe53111e9a6babbac7b4af8/ruff-0.16.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39897739f112253ee4fdd2e8aa9a4f9ded99fb2be367d5f31dfa4ded6025584c", size = 11468183, upload-time = "2026-07-30T19:36:37.339Z" },
{ url = "https://files.pythonhosted.org/packages/2c/6b/4ffb7ad1d83eb16cf8cbb3c8815d3f11c88460fd162d4b372a2059be1c2a/ruff-0.16.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82ae3c0c0d74daf17b968a10b7b3bb3ef297ab7de0c1f749646b25e690ccb150", size = 11470071, upload-time = "2026-07-30T19:36:39.91Z" },
{ url = "https://files.pythonhosted.org/packages/9c/72/32ae7db4c0b5e32ab611787caa19d1546800676d79f7483b7100a3561bf4/ruff-0.16.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4d5f2ed10f8242d83fc08d521301089364e3375375705356f20c0e31606ef3ef", size = 10919503, upload-time = "2026-07-30T19:36:42.65Z" },
{ url = "https://files.pythonhosted.org/packages/f7/ca/3d901ba6ad6fc38da39c3448fc6c59ac945679293a17c3ceb6d6c1cba13e/ruff-0.16.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a4665b309891f83f3e3c25447935f1213e9abbd4b5640af7a1f2def9f8d413c1", size = 10649861, upload-time = "2026-07-30T19:36:45.18Z" },
{ url = "https://files.pythonhosted.org/packages/92/79/894ef1ced26552d5f8c9cf6d85b0687840e1128c55aeab7b9c2d54a0d880/ruff-0.16.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26e9ca5c9bc3971f20d3cf18a957f52ffd6a5f6564ff15c4912a144dcac22494", size = 11148137, upload-time = "2026-07-30T19:36:47.936Z" },
{ url = "https://files.pythonhosted.org/packages/2d/69/3609a09fa1cb46cc28b762363e440a354204e5dff01bd0c8d7437874d6b9/ruff-0.16.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:67e1e1e3fa4f0c82f0e36d4cd61e661f6e7a6196cb1aa92fe0828fa7b8f257cd", size = 11559211, upload-time = "2026-07-30T19:36:50.448Z" },
{ url = "https://files.pythonhosted.org/packages/fc/8a/fb22af2fd78a736e241fabf67e30ce1799a64244026377a49e133af90762/ruff-0.16.1-py3-none-win32.whl", hash = "sha256:d31765e131295b8445caf301e3e8a85b34d1b9b211b4109b7ba457888b051806", size = 10838258, upload-time = "2026-07-30T19:36:53.298Z" },
{ url = "https://files.pythonhosted.org/packages/d4/35/e57fd9fb5d423961df087a00b12d42c0a830288dc2f3b45ecca299158b4f/ruff-0.16.1-py3-none-win_amd64.whl", hash = "sha256:09b05e8b90c2cb06ad63464350e7a45e8e44a2dfe52072ebfba6666ca8d3f596", size = 11961111, upload-time = "2026-07-30T19:36:56.107Z" },
{ url = "https://files.pythonhosted.org/packages/cb/46/240ea004bf6dc4feb40e9832f2205a476a47dd5b8a3f8211a5fc5f95e20e/ruff-0.16.1-py3-none-win_arm64.whl", hash = "sha256:dbaadaac38c70239f056d306b7476f246b0bf000fa6b3876402acbf5b227eaf8", size = 11309414, upload-time = "2026-07-30T19:36:58.79Z" },
]