Status: Stable — v1.4.0
Локальный грейдер для курсов «Поколение Python» на Stepik. Скачивает данные задачи с сайта и позволяет не только проверить решение локально, но и сравнить несколько решений более честно: сначала по корректности, потом по benchmark-метрикам.
Курсы:
- Поколение Python: Курс для начинающих
- Поколение Python: Курс для продвинутых
- Поколение Python: Курс для профессионалов
- Поколение Python: ООП
- Поколение Python: Курс для самураев
- Что умеет
- Архитектура модулей
- Структура проекта
- Установка
- Быстрый старт
- Работа с API Stepik
- Режимы работы
- Формат тест-кейсов
- Конфигурация
- Зависимости
- Диагностика
- Ограничения и безопасность
- Что изменилось по сравнению с оригиналом
- Эволюция версий
- Python версия
Пакет живёт в
src/stepik_grader/(Issue #35, src-layout). Пути ниже — относительноsrc/stepik_grader/.
| Модуль | Архитектурный слой | Что делает |
|---|---|---|
grader.py |
Application | Тонкий фасад обратной совместимости — реэкспортирует core/grader_core.py, core/reporter.py, cli.py |
cli.py |
Application / CLI | Интерактивное меню (режимы 0-4) и non-interactive argparse CLI, профили нагрузки; консольная команда stepik-grader |
config.py |
Application / Configuration | GraderConfig (frozen dataclass) + CONFIG singleton; переопределяется через [tool.stepik-grader] в pyproject.toml |
downloader.py |
Domain / Application | Управление конфигом и secrets, разбор URL шага, построение директорий задач (slugify, build_task_directory), сохранение файлов задачи, автоизвлечение тест-кейсов из HTML-таблицы и ZIP-архивов, оркестрация вызовов API |
diagnostic_stepik.py |
Application / Diagnostics | Диагностика: проверяет структуру ответа API и корректность токена авторизации |
core/grader_core.py |
Application | Исполнение тест-кейса в subprocess и агрегация статистики: 4 режима работы (run_tests, run_benchmark, run_microbench_mode) |
core/test_loader.py |
Application | Обнаружение файлов-решений, загрузка тест-кейсов (load_test_cases), resolve_test_dir (Issue #45 A-01) |
core/mode_detector.py |
Application | Детекция режима запуска stdin/function (_detect_run_mode, is_function_only_solution) (Issue #45 A-01) |
core/wrapper_builder.py |
Application | Генерация wrapper-скриптов для function-mode запуска (Issue #45 A-01) |
core/reporter.py |
Application / UI | rich-таблицы с цветами, вердикты AC/WA/TLE/RE, verbose-diff при WA, адаптивное форматирование времени (fmt_time) |
core/executor.py |
Infrastructure | Запускатель решений: compile + exec с таймаутом и изолированным namespace |
core/microbench_runner.py |
Infrastructure | Timeit-микробенчмарк через subprocess (python -c) + подавление stdout решения в os.devnull; peak memory через tracemalloc |
core/normalizers.py |
Infrastructure / Utilities | Нормализация вывода для сравнения: normalize_floats (округление float до 9 знаков), sort_lines, normalize_whitespace (experimental) |
core/storage.py |
Infrastructure / Utilities | Чтение и запись JSON-файлов (load_json_file, save_json_file, save_secrets); нет зависимостей от других модулей проекта |
core/stepik_client.py |
Infrastructure / HTTP | OAuth2-авторизация, requests.Session, GET-запросы к Stepik REST API, скачивание сабмишнов |
core/oauth_flow.py |
Infrastructure / Auth | OAuth2-фасад: единая точка входа для авторизации — load_secrets, load_secrets_dict, token_is_valid, authorize_and_get_token; устраняет дублирование между downloader.py и diagnostic_stepik.py |
core/parsers.py |
Infrastructure / Utilities | Парсинг тест-блоков (# TEST_N:) — единственный источник истины для grader.py и downloader.py |
Основные возможности:
- ✅ Запуск решений против наборов тест-кейсов (
tests/N+tests/N.clue) - 📋 Автоматическое извлечение тест-кейсов из HTML-таблицы в тексте задачи Stepik
- 📦 Автоскачивание тестов из ZIP-архива по ссылке в тексте задачи
- 🔗 Обнаружение ссылок на GitHub-тесты с подсказкой скачать вручную
- 📊 Сравнение нескольких решений одной задачи в таблице
- 🚀 Subprocess-бенчмарк с замером времени и памяти
- ⚡ Timeit-микробенчмарк через subprocess (
python -c) с подавлением stdout решения вos.devnull - 🎨 Цветной вывод через
rich— зелёный OK/AC, красный WA/TLE/RE, жёлтый SLOWER - 🔍 Diff при WA — сравнение ожидаемого и фактического вывода при провале теста
- ⚖️ Вердикты AC / WA / TLE / RE по каждому тест-кейсу
- 🔍 Диагностика окружения и авторизация через Stepik API
Граф зависимостей — DAG без циклов (все модули живут в src/stepik_grader/):
downloader.py ──→ core/storage.py
downloader.py ──→ core/stepik_client.py
downloader.py ──→ core/parsers.py
core/stepik_client.py ──→ core/storage.py
grader.py ──→ core/grader_core.py, core/reporter.py, cli.py (тонкий фасад)
core/grader_core.py ──→ core/executor.py, core/microbench_runner.py, core/normalizers.py
core/grader_core.py ──→ core/test_loader.py, core/mode_detector.py, core/wrapper_builder.py
core/test_loader.py ──→ core/mode_detector.py, core/parsers.py
core/mode_detector.py ──→ core/storage.py
cli.py ──→ core/grader_core.py, core/reporter.py, core/microbench_runner.py
diagnostic_stepik.py ──→ core/stepik_client.py
diagnostic_stepik.py ──→ downloader.py ← parse_stepik_step_url
downloader.py ──→ core/oauth_flow.py
diagnostic_stepik.py ──→ core/oauth_flow.py
core/oauth_flow.py ──→ core/stepik_client.py
core/oauth_flow.py ──→ core/storage.py
downloader.py больше не импортирует grader.py: дублирующая копия
_parse_testblock_file в grader.py устранена (Issue #19) — оба модуля
читают parse_testblock_file из core/parsers.py.
Слои (снизу вверх):
┌───────────────────────────────────────────────────────────────┐
│ Domain / Application (src/stepik_grader/ — точки входа) │
│ downloader.py │ grader.py (facade) │ diagnostic_stepik │
├───────────────────────────────────────────────────────────────┤
│ Application (core/, грейдер разбит по SRP — Sprint 7, A-01) │
│ core/grader_core.py (исполнение) │ core/reporter.py (вывод)│
│ core/test_loader.py │ core/mode_detector.py │ wrapper_builder │
│ cli.py (меню, публичная точка входа — stepik-grader) │
├───────────────────────────────────────────────────────────────┤
│ Infrastructure (core/) │
│ core/stepik_client.py │ core/executor.py │
│ core/microbench_runner.py │ core/oauth_flow.py │
├───────────────────────────────────────────────────────────────┤
│ Infrastructure / Utilities (core/, leaf, no deps) │
│ core/storage.py │ core/normalizers.py │
└───────────────────────────────────────────────────────────────┘
core/storage.py и core/normalizers.py — leaf-модули: не импортируют ничего из проекта, легко тестируются изолированно.
Stepik-Python-Grader/
├── src/
│ └── stepik_grader/ # src-layout (Issue #35 / CLAUDE.md Sprint 8.2)
│ ├── __init__.py
│ ├── grader.py # Тонкий фасад обратной совместимости (Sprint 7)
│ ├── cli.py # Интерактивное меню (режимы 0-4) + stepik-grader entry point
│ ├── config.py # GraderConfig, CONFIG — единая конфигурация
│ ├── downloader.py # Domain: конфиг, slugify, построение папок, оркестрация API
│ ├── diagnostic_stepik.py # Диагностика API и токена
│ └── core/ # Internal Infrastructure/Utility модули (Issue #23, #26)
│ ├── __init__.py
│ ├── grader_core.py # Исполнение тест-кейса в subprocess, агрегация статистики
│ ├── test_loader.py # Обнаружение файлов-решений, загрузка тест-кейсов (Issue #45 A-01)
│ ├── mode_detector.py # Детекция режима stdin/function (Issue #45 A-01)
│ ├── wrapper_builder.py # Генерация wrapper-скриптов для function-mode (Issue #45 A-01)
│ ├── reporter.py # rich-таблицы, вывод, verbose-diff
│ ├── executor.py # Запускатель решений: compile + exec с таймаутом
│ ├── microbench_runner.py # Timeit-микробенчмарк через subprocess + os.devnull
│ ├── normalizers.py # Нормализация вывода: округление float, sort/whitespace
│ ├── stepik_client.py # Infrastructure: OAuth2, requests.Session, Stepik API
│ ├── oauth_flow.py # Infrastructure/Auth: OAuth2-фасад поверх stepik_client
│ ├── parsers.py # Парсинг тест-блоков (# TEST_N:)
│ └── storage.py # Utilities: load/save JSON, save_secrets (нет project-зависимостей)
├── conftest.py # Добавляет src/ в sys.path для тестов
├── tests/ # 622 теста (pytest)
│ ├── test_analyzer.py
│ ├── test_downloader.py
│ ├── test_executor.py
│ ├── test_grader_core.py
│ ├── test_integration_repos.py
│ ├── test_loader.py
│ ├── test_menu_modes.py
│ ├── test_microbench.py
│ ├── test_microbench_grader.py
│ ├── test_microbench_runner_module.py
│ ├── test_normalizers.py
│ ├── test_oauth_flow.py
│ ├── test_slugify.py
│ ├── test_stepik_client.py
│ ├── test_storage.py
│ └── test_testblock.py
├── .github/workflows/ci.yml # CI: pytest + ruff на Python 3.12/3.13/3.14
├── .pre-commit-config.yaml # Pre-commit хуки (ruff check + ruff format)
├── pyproject.toml # Конфигурация проекта (ruff, mypy, pytest, зависимости, packages.find where=["src"])
├── secrets.json.example # Шаблон файла с OAuth-токеном
├── stepik_config.json.example # Шаблон конфига Stepik
├── CHANGELOG.md # История изменений
└── README.md
Локально обычно появляются:
StepikTasks/
stepik_config.json
secrets.json
errors.txt
stepik_diagnostics/
Эти файлы и папки держи в .gitignore.
Коротко для новичка: если просто хочешь пользоваться — ставь через
pipx(Способ A): он сам всё изолирует и добавит команду в PATH, никакихvenvиactivate. Способ B (из исходников) нужен только если будешь менять код.
- Python 3.12 или 3.13. Версия 3.14 — экспериментальная (может ломаться),
ставь её только осознанно. Проверить свою версию:
python --version. - Git — только для установки из исходников.
pipx ставит CLI-инструмент в отдельное окружение и сам
прописывает команду в PATH — не нужно ни venv, ни activate.
python -m pip install --user pipx
python -m pipx ensurepath # один раз добавляет pipx в PATH — ПЕРЕЗАПУСТИ терминал после этого
pipx install stepik-python-graderПроверь, что всё встало:
stepik-grader --version # должно напечатать текущую версиюПакет публикуется на PyPI (issue #70). Если нужна ещё не выпущенная версия прямо из репозитория —
pipx install git+https://github.com/ArtVsMark/Stepik-Python-Grader.git. Обычныйpip install stepik-python-graderтоже работает, ноpipxудобнее для CLI-инструмента (изоляция + PATH).
Шаг 1. Клонировать репозиторий:
git clone https://github.com/ArtVsMark/Stepik-Python-Grader.git
cd Stepik-Python-GraderШаг 2. Создать виртуальное окружение:
python -m venv .venvШаг 3. Активировать окружение:
# macOS / Linux
source .venv/bin/activate# Windows (PowerShell)
.venv\Scripts\Activate.ps1
⚠️ Windows: «выполнение сценариев отключено в этой системе» (PSSecurityException)? PowerShell по умолчанию блокирует активацию venv. Два выхода:
- Разрешить скрипты для своего пользователя (один раз):
затем сноваSet-ExecutionPolicy -Scope CurrentUser RemoteSigned.venv\Scripts\Activate.ps1.- Или не активировать вообще — звать интерпретатор из venv напрямую:
.venv\Scripts\python.exe -m pip install -e . .venv\Scripts\python.exe -m stepik_grader❗ Не пропускай активацию, если ставишь просто
pip install -e .— иначе пакет уедет в глобальный Python, а не в venv, и командаstepik-graderможет «не найтись» (её каталог не в PATH). В любом случае надёжный запуск —python -m stepik_grader(работает всегда, см. «Быстрый старт»).
Шаг 4. Установить зависимости:
pip install -e . # рантайм: requests, psutil, richДля разработки (тесты, линтер, типизация):
pip install -e ".[dev]" # + pytest, pytest-cov, ruff, mypyШаг 5. Проверить установку:
python -m stepik_grader --version # 1.2.0Проект использует src-layout (
src/stepik_grader/, Issue #35) — модули запускаются только как пакет (python -m stepik_grader) или командойstepik-grader(если её каталог в PATH). Прямогоpython grader.pyиз корня репозитория нет.
Запусти интерактивное меню:
python -m stepik_grader # надёжный способ: работает даже если stepik-grader не в PATH
# или короче, если команда в PATH (Способ A / активированный venv):
stepik-graderПри запуске появится меню (русский язык по умолчанию, issue #51 D-01;
--lang en — английский):
==================================================
Stepik Python Grader
==================================================
1. Проверить одно решение
2. Проверить все решения в папке
3. Бенчмарк решений в папке
4. Микро-бенчмарк (timeit) для папки
0. Выход
==================================================
Выберите режим [0-4]:
Проверим простое решение «прибавь 1 к числу» без Stepik — вручную.
Шаг 1. Создай файл решения task.py в любой папке:
# task.py
n = int(input())
print(n + 1)Шаг 2. Рядом создай папку tests/ с одной парой файлов — вход и ожидаемый
вывод. Имя файла со входом — просто число, ожидаемый вывод — то же имя с
.clue:
task.py
tests/
1 ← содержимое: 4 (это подаётся решению на stdin)
1.clue ← содержимое: 5 (это ожидаемый вывод)
То есть: файл
tests/1содержит строку4, файлtests/1.clue— строку5. Можно добавить ещё кейсы:tests/2+tests/2.clue, и так далее.
Шаг 3. Запусти проверку одного решения (режим 1):
python -m stepik_grader --mode 1 --file task.pyШаг 4. Прочитай результат. Колонка Passed покажет 1/1, статус — OK:
File Passed Total time Avg time Memory, MB Status Fail test
task.py 1/1 0.0123 0.0123 4.20 OK -
Если вывод решения не совпадёт с .clue, статус будет FAIL, а с флагом
--verbose грейдер покажет построчный diff «ожидалось / получено».
Откуда брать тесты для реальных задач Stepik? Их можно скачать автоматически — см. раздел Работа с API Stepik ниже. Формат папки
tests/при этом тот же самый.
Для запуска из CI/скриптов без интерактивного ввода:
stepik-grader --version # версия и выход
stepik-grader --mode 1 --file path/to/task.py # режим 1
stepik-grader --mode 2 --dir path/to/folder # режим 2
stepik-grader --mode 3 --dir path/to/folder --repeats 15 # режим 3 (по умолчанию 15)
stepik-grader --mode 4 --dir path/to/folder --number 1000 # режим 4 (по умолчанию 1000)Эквивалентно через python -m: python -m stepik_grader --version или
python -m stepik_grader.grader --version и т.д. (пакет содержит
__main__.py, поэтому короткая форма python -m stepik_grader работает —
issue #65).
Без --mode показывается обычное интерактивное меню.
Для тех, кому консоль — барьер (новички, работа из IDE), есть локальный веб-интерфейс с двумя режимами:
- Корректность — таблица AC/WA, время, память; клик по имени файла раскрывает тест-кейсы и diff при WA.
- Бенчмарк — решения ранжируются по медиане (быстрейшее первым) с вердиктом SIMILAR/SLOWER/MUCH_SLOWER, как в режиме 3 CLI.
stepik-grader --serve # http://127.0.0.1:8000
stepik-grader --serve --port 9000 # другой порт- Только localhost (
127.0.0.1) — в сеть не торчит. - Без новых зависимостей — на stdlib
http.server, переиспользует ту же логику грейдинга, что и CLI (run_tests/run_benchmark). - Поле пути по умолчанию — папка запуска; последний путь и режим запоминаются (localStorage).
- В поле пути — файл решения (
.py) или папка с решениями; тесты резолвятся так же, как в режимах 1/2/3. - Тот же threat model, что у CLI (нет OS-sandbox) — запускай свои решения.
Эпик #80 Tier 1 / issue #58. Drag-and-drop загрузка файлов — следующая итерация.
Проверять решение прямо из редактора, не переключаясь в терминал.
VS Code — сгенерировать задачи одной командой (из папки проекта):
stepik-grader --init-vscodeСоздаётся .vscode/tasks.json с задачами:
- Stepik: проверить текущий файл (дефолтная —
Ctrl+Shift+B) →--mode 1 --file ${file} - Stepik: проверить папку →
--mode 2 --dir ${fileDirname} - Stepik: бенчмарк папки →
--mode 3 --dir ${fileDirname} - Stepik: веб-интерфейс →
--serve
Запуск: Ctrl+Shift+B (проверить открытый файл) или Terminal → Run Task → «Stepik: …». Существующий tasks.json не перезатирается — команда предупредит.
PyCharm — через External Tool (настраивается вручную, один раз):
Settings → Tools → External Tools → +- Заполнить:
- Program:
stepik-grader - Arguments:
--mode 1 --file $FilePath$ - Working directory:
$FileDir$
- Program:
- Запуск: правый клик по файлу →
External Tools → …(или назначить горячую клавишу вKeymap).
Обе интеграции просто вызывают консольную команду
stepik-grader— работают сразу послеpipx install stepik-python-grader(см. «Установка»).
stepik-grader --mode 1 --file task.py --lang en # меню/сообщения на английском (по умолчанию — ru)
stepik-grader --mode 1 --file task.py --quiet # без подробного diff (режим 1 по умолчанию verbose)
stepik-grader --mode 2 --dir . --verbose # с подробным diff по каждому кейсу (режим 2 по умолчанию quiet)
stepik-grader --mode 1 --file task.py --output json # машиночитаемый JSON вместо таблицы
stepik-grader --mode 2 --dir . --output json > results.json--verbose/--quiet взаимоисключающие; управляют только режимами 1/2
(режимы 3/4 всегда печатают итоговую таблицу бенчмарка, --verbose для них
не имеет смысла). --output json печатает ровно одну JSON-строку —
структура повторяет словари, которые уже возвращают run_tests()/
run_benchmark()/run_microbench_mode() (ключи file/results/groups в
зависимости от режима), без отдельной документированной схемы.
stepik-grader --mode 2 --dir . --output csv > results.csv
stepik-grader --mode 3 --dir . --output markdown > BENCHMARK.mdТе же данные, что и в --output json, но плоской таблицей (одна строка на
файл/тест-кейс) в CSV или Markdown-таблице. Пишут в stdout, как и json —
для сохранения в файл используется обычное перенаправление шелла, отдельного
флага "сохранить в файл" нет.
pip install "stepik-grader[watch]" # опциональная зависимость: watchfiles
stepik-grader --mode 1 --file task.py --watch
stepik-grader --mode 2 --dir . --watchПерезапускает весь режим 1/2 при любом изменении внутри отслеживаемого
файла/папки (очищает экран перед повторным запуском). Работает только с
--mode 1/2 — для 3/4 (дорогой бенчмарк) неприменимо. Без установленного
watchfiles печатает сообщение с инструкцией по установке вместо падения.
stepik-grader --mode 1 --file task.py --cache # первый прогон — считает и кэширует
stepik-grader --mode 1 --file task.py --cache # без изменений — берёт из кэша (0 перезапусков)
stepik-grader --mode 2 --dir . --cache # для всей папки: "N из M из кэша"
stepik-grader --clear-cache # удалить кэш и выйтиOpt-in кэш результатов проверки для --mode 1/2. Решение пропускается и
показывается прошлый вердикт, пока не изменились ни содержимое файла решения
(sha256), ни файлы его тест-директории (sha256); любое изменение
инвалидирует запись и тест перезапускается. Кэш хранится в одном файле
.grader_cache/results.json в текущей папке (добавлен в .gitignore).
Включить по умолчанию можно через pyproject.toml:
[tool.stepik-grader]
use_cache = trueПри включённом по умолчанию кэше отдельный запуск можно форсировать флагом
--no-cache. Режимы 3/4 (бенчмарк) кэш не используют — их смысл в свежих
замерах времени.
1. Создай OAuth-приложение на Stepik
- Зайди на https://stepik.org/oauth2/applications/
- Нажми + New Application
- Заполни поля:
| Поле | Значение |
|---|---|
| Name | любое, например my-grader |
| Client type | Confidential |
| Authorization grant type | Authorization code |
| Redirect uris | http://localhost:8080/callback |
- Нажми Save — Stepik покажет
Client IDиClient Secret.
Скопируй шаблон:
cp secrets.json.example secrets.jsonЗаполни своими значениями:
{
"client_id": "<Client ID из настроек приложения Stepik>",
"client_secret": "<Client Secret из настроек приложения Stepik>",
"redirect_uri": "http://localhost:8080/callback",
"access_token": "",
"refresh_token": "",
"expires_at": 0
}| Поле | Что это |
|---|---|
client_id |
ID OAuth-приложения в Stepik |
client_secret |
секрет OAuth-приложения |
redirect_uri |
адрес для возврата после авторизации |
access_token |
текущий токен доступа, заполняется автоматически |
refresh_token |
токен обновления, заполняется автоматически |
expires_at |
время истечения access_token (Unix-timestamp), заполняется автоматически |
secrets.json— локальный файл, не должен попадать в Git. При первом запуске оставьaccess_token,refresh_token,expires_atпустыми — скрипт заполнит их сам черезstorage.save_secrets().
python -m stepik_grader.downloaderПри первом запуске:
- будет предложено выбрать корневую папку (по умолчанию
StepikTasks) и путь кsecrets.json, - откроется браузер для подтверждения доступа,
- после успешной авторизации токены сохранятся в
secrets.jsonчерезstorage.save_secrets().
Введи URL шага, например:
URL шага: https://stepik.org/lesson/569749/step/4?unit=564263
Скрипт создаст структуру:
StepikTasks/
└── название-курса/
└── название-секции/
└── название-урока/
└── 04/ # только номер, если у шага нет заголовка
└── 04-название-шага/ # номер + slug, если заголовок есть
├── task4_1.py # основное решение (из шаблона задачи или пустой)
├── task4_2.py # заготовка для альтернативного решения (всегда создаётся)
├── solution.py # последний сабмишн с сайта (если доступен)
├── meta.json # метаданные шага (id, lesson, course, ...)
├── task.md # текст задачи в Markdown/HTML
└── tests/
├── 1 # входные данные теста №1
├── 1.clue # ожидаемый вывод теста №1
├── 1.type # тип теста (только для function-style)
├── 2
├── 2.clue
└── ...
Схема именования рабочих файлов:
| Файл | Содержимое | Создаётся |
|---|---|---|
task{N}_1.py |
шаблон из задачи (или пустой, если шаблона нет) | всегда |
task{N}_2.py |
заготовка для альтернативного решения 1 | всегда (только если файл ещё не существует) |
task{N}_3.py и далее |
альтернативные решения 2, 3, … | вручную |
solution.py |
последний сабмишн с сайта | если сабмишн доступен |
Повторный запуск
downloader.pyдля того же шага не перезапишетtask{N}_2.pyи выше — твои наработки сохранятся.
downloader.py перебирает источники по приоритету — первый успешный выигрывает:
| Приоритет | Источник | Поведение |
|---|---|---|
| 1 | ZIP-ссылка в HTML задачи | Скачивается автоматически, распаковывается в tests/ |
| 2 | HTML-таблица в тексте задачи | Парсится автоматически в tests/N + tests/N.clue |
| 3 | Ссылка на GitHub в HTML | Адрес печатается в консоль — скачать вручную |
| 4 | Ничего не найдено | Предупреждение ⚠️, остальные файлы уже сохранены |
OAuth-поток полностью реализован в stepik_client.py (create_user_session, authorize_via_browser, refresh_access_token); downloader.py только оркестрирует вызовы.
Быстро прогнать одно решение:
Enter path to solution file: module1/task1/task1_1.py
File Passed Total time Avg time Memory, MB Status Fail test
task1_1.py 5/5 0.1234 0.0247 25.30 OK -
Результат выводится rich-таблицей (зелёный OK, красный FAIL); при провале
теста в verbose-режиме печатается diff ожидаемого и фактического вывода.
Проходит по всей папке, находит все task*.py и верифицирует каждый. Результаты — таблица, сгруппированная по задачам.
📂 module1/task1
--------------------------------------------------------------------
File Passed Total time Avg time Memory, MB Status Fail test
--------------------------------------------------------------------
module1/task1/task1_1.py 5/5 0.1234 0.0247 25.30 OK -
module1/task1/task1_2.py 5/5 0.1456 0.0291 24.80 OK -
Режим 2 — проверка корректности, не полноценный benchmark.
Запускает N повторений для каждого прошедшего все тесты решения через отдельный процесс. Выводит min / median / mean / max / std-dev и сравнивает решения относительно быстрейшего.
Профили нагрузки (repeats):
| # | Режим | Повторений |
|---|---|---|
| 1 | low | 5 |
| 2 | medium | 15 |
| 3 | high | 50 |
| 4 | custom | 5–100 |
Что показывает benchmark:
| Поле | Значение |
|---|---|
Runs |
всего запусков |
Min |
лучший замер |
Median |
медианное время — главный ориентир |
Mean |
среднее время |
Max |
худший замер |
Std dev |
разброс замеров (мало → стабильно) |
Memory |
пиковая память |
Relative |
относительное время к лучшему решению |
Verdict |
SIMILAR, SLOWER, MUCH SLOWER |
🚀 Benchmark: module1/task1
---------------------------------------------------------------------
File Runs Min Median Mean Max Std dev Memory Relative Verdict
---------------------------------------------------------------------
module1/task1/task1_1.py 25 0.0234 0.0249 0.0250 0.0279 0.0011 25.30 100.0% SIMILAR
module1/task1/task1_2.py 25 0.0257 0.0271 0.0273 0.0301 0.0013 24.80 108.9% SLOWER
Замеряет время через timeit.timeit внутри одного процесса — без накладных расходов на запуск интерпретатора. Поддерживает script-style (с input()) и function-only решения.
Колонка
Py-heap(неMemory). В отличие от режима 3 (RSS через psutil), режим 4 для stdin-блоков меряет пик Python-heap черезtracemalloc, а для function-блоков — RSS. Это два разных метода в одной колонке, поэтому она называетсяPy-heap, а неMemory.tracemallocне видит аллокации C-расширений (numpy и т.п.) — для чистого Python это приемлемо (issue #66).
Количество вызовов (calls per run):
| # | Режим | Вызовов |
|---|---|---|
| 1 | fast | 500 |
| 2 | normal | 1 000 |
| 3 | thorough | 5 000 |
| 4 | deep | 50 000 |
| 5 | hard | 100 000 |
| 6 | custom | 100–500 000 |
Режим
hard— только для коротких детерминированных функций.
⚡ Micro-bench (timeit): module1/task1
---------------------------------------------------------------------------
File Repeats Min, us Median, us Mean, us Max, us Std dev, us Relative Verdict
---------------------------------------------------------------------------
module1/task1/task1_1.py 1000 12.34 13.01 13.12 15.67 0.82 100.0% SIMILAR
module1/task1/task1_2.py 1000 14.21 15.34 15.45 18.90 1.12 117.9% MUCH SLOWER
| Вердикт | Значение |
|---|---|
| AC | Accepted — вывод совпал с ожидаемым |
| WA | Wrong Answer — вывод не совпал |
| TLE | Time Limit Exceeded — превышен таймаут |
| RE | Runtime Error — процесс завершился с ненулевым кодом |
module1/
└── task1/
├── task1_1.py # основное решение
├── task1_2.py # альтернативное решение 1
└── tests/
├── 1 # входные данные теста №1 (stdin)
├── 1.clue # ожидаемый вывод теста №1
├── 1.type # тип теста: файл присутствует только для function-style задач,
│ # содержит строку "function"
├── 2
├── 2.clue
└── ...
Типы тестов (*.type):
| Значение в файле | Когда создаётся | Поведение |
|---|---|---|
| (файл отсутствует) | stdin-задача | входные данные подаются через stdin |
function |
function-style задача | входные данные — объявление переменной (x = 5), передаётся через exec |
Файлы тестов читаются в кодировке UTF-8.
При скачивании задачи через
downloader.pyфайлыtests/N,tests/N.clueи при необходимостиtests/N.typeсоздаются автоматически из ZIP-архива или HTML-таблицы в тексте задачи. Если ни ZIP, ни таблицы нет — папкуtests/нужно заполнить вручную.
Грейдер автоматически распознаёт три формата:
Файлы 1, 1.clue, 2, 2.clue в папке tests/. Создаётся автоматически при скачивании через downloader.py.
input_1.txt + expected_1.txt, input_2.txt + expected_2.txt...
tests/input.txt + tests/output.txt с маркерами # TEST_N:.
Используется репозиториями python-generation/Professional, python-generation/OOP, python-generation/Samurai.
Stepik ZIP-архивы автоматически конвертируются в Format 3 при скачивании через downloader.py.
GitHub-ссылки в тексте задачи обрабатываются автоматически.
При первом запуске downloader.py предложит указать:
Укажи корневую папку для всех задач Stepik [StepikTasks]:
Укажи путь к secrets.json [secrets.json]:
Значения сохраняются в stepik_config.json. Структура директорий внутри:
StepikTasks/
└── <курс>/<секция>/<урок>/<NN>/ или <NN-шаг>/
В core/grader_core.py константа TIMEOUT_SECONDS (по умолчанию 10.0 с) защищает от зависания:
TIMEOUT_SECONDS: float = 10.0 # секундВ core/executor.py таймаут передаётся через переменную окружения EXECUTOR_TIMEOUT (по умолчанию 10 с). На Unix — signal.alarm; на Windows (где SIGALRM недоступен) защита обеспечивается таймаутом subprocess уровня core/grader_core.py (TIMEOUT_SECONDS):
TIMEOUT: int = int(os.environ.get("EXECUTOR_TIMEOUT", "10"))MEASURE_CHILD_MEMORY: bool = True # False — быстрее, но грубееTrue(по умолчанию) — мониторинг дочернего процесса черезpsutilв отдельном потоке (честнее, но медленнее)False— RSS родительского процесса (быстро, приблизительно)
MICROBENCH_MAX_CASES = 5Ограничивает число тест-кейсов при timeit-замерах для стабильного std-dev.
| Пакет | Назначение | Используется в |
|---|---|---|
requests>=2.34.2 |
HTTP-запросы к Stepik API, OAuth2, скачивание ZIP | core/stepik_client.py, downloader.py |
psutil>=5.9 |
Замер памяти и мониторинг процессов | core/grader_core.py, core/executor.py |
rich>=13.0 |
Цветные таблицы, прогресс-бар, WA diff в терминале | core/reporter.py |
Dev-зависимости (pip install -e ".[dev]"):
| Пакет | Назначение |
|---|---|
pytest>=8.2 |
Тестирование |
pytest-cov>=5.0 |
Покрытие тестами (--cov) |
ruff>=0.4 |
Линтер и форматтер |
Если downloader.py не нашёл данных шага автоматически:
python -m stepik_grader.diagnostic_stepikСкрипт сохранит в папку stepik_diagnostics/:
lesson_debug.jsonstep_debug.jsondiagnostic_result.json
diagnostic_stepik.py также позволяет:
- проверить доступность Stepik API;
- убедиться в корректности токена авторизации;
- получить информацию о курсе, уроке или задаче по ID.
Threat model: решения запускаются БЕЗ полноценного sandbox на уровне ОС.
Дочерний процесс имеет тот же доступ к файловой системе, сети и переменным
окружения, что и сам grader. Защита по времени выполнения есть всегда
(таймаут); на POSIX (Linux/macOS) есть ещё best-effort лимит памяти
(GraderConfig.max_memory_mb, по умолчанию 1024 МБ — resource.setrlimit (RLIMIT_AS) через preexec_fn); на Windows этого лимита нет (resource
недоступен), решение может использовать сколько угодно памяти. Ограничений
диска или сети нет ни на одной платформе. Запускай только доверенные решения
(свои собственные или скачанные из Stepik as-is) — grader не предназначен для
проверки произвольного untrusted-кода (см. core/executor.py, который явно
задокументирован как "нет sandbox на уровне ОС" в CLAUDE.md).
- Режимы 1–3 (
grader_core.run_single_test): решение запускается напрямую черезsubprocess.Popen(для function-mode — во временном wrapper-скрипте, импортирующем функцию решения). Единственная защита —timeout=уproc.communicate()(grader_core.TIMEOUT_SECONDS, по умолчанию 10с);core/executor.py(run_solution) реализует ту же модель сsignal.alarmна Unix, но используется только в тестах, не в самом grader'е. - Режим 4 (
core/microbench_runner.py): решения запускаются через subprocess (python -c) сtimeit.repeat, защищены фиксированнымsubprocess.run(timeout=60). Исходник передаётся через временный файл;stdinсбрасывается перед каждой итерацией, аstdoutрешения перенаправляется вos.devnullна время замера, чтобы его вывод не смешивался с числами-таймингами. - Microbench: локальный per-call таймаут отсутствует — решение, зависающее
внутри одного вызова (не в бесконечном цикле верхнего уровня), упрётся в
общий 60-секундный
subprocess.run(timeout=60)вокруг всего замера (5 повторов × N итераций), а не в индивидуальный лимит на итерацию. Сообщение об ошибке при таймауте указываетnumber=<N>(сколько итераций было в замере), чтобы хотя бы приблизительно понять масштаб зависания (issue #47 R-01).
Этот форк существенно расширяет оригинальный проект PavloOps/python_generation_grader:
| Возможность | Оригинал | Этот форк |
|---|---|---|
| Проверка одного файла | ✅ | ✅ |
| Сравнение нескольких решений | ❌ | ✅ |
| Subprocess-benchmark | ❌ | ✅ режим 3 |
| Timeit-microbench | ❌ | ✅ режим 4 |
| Разделение корректности и benchmark | ❌ | ✅ |
| Профили нагрузки | ❌ | ✅ low/medium/high/custom |
| Оценка по median (не одиночный замер) | ❌ | ✅ |
| Вердикт SIMILAR / SLOWER / MUCH SLOWER | ❌ | ✅ |
| OAuth2 + скачивание данных задачи с API | ❌ | ✅ |
| Автоизвлечение тест-кейсов из HTML-таблицы | ❌ | ✅ Sprint 4 |
| Автоскачивание тестов из ZIP-архива | ❌ | ✅ Sprint 4 |
| Обнаружение ссылок на GitHub-тесты | ❌ | ✅ Sprint 4 |
Поддержка function-style тестов (*.type) |
❌ | ✅ Sprint 4 |
| Схема файлов task{N}_1.py / task{N}_2.py | ❌ | ✅ Sprint 5 |
| Диагностика API | ❌ | ✅ |
| Поддержка function-only решений | ❌ | ✅ |
Выделенный HTTP/OAuth слой (stepik_client.py) |
❌ | ✅ Sprint 3 |
Утилиты хранилища без project-зависимостей (storage.py) |
❌ | ✅ Sprint 3 |
| pyproject.toml (ruff, pytest, зависимости) | ❌ | ✅ |
| Pre-commit хуки (ruff check + ruff format) | ❌ | ✅ |
| Unit-тесты (622 теста) | ❌ | ✅ |
OAuth2-фасад (oauth_flow.py) |
❌ | ✅ |
| GitHub Actions CI (pytest + ruff) | ❌ | ✅ |
Таблица ниже — про фундаментальные сдвиги между релизами, а не про
отдельные фичи (полный список изменений — в CHANGELOG.md).
Каждая версия — это качественный скачок в отдельной плоскости.
| v1.0.0 | v1.1.0 | v1.2.0 | v1.3.0 | v1.4.0 | |
|---|---|---|---|---|---|
| Суть релиза | Первый стабильный форк — «работает» | Зрелая архитектура, установка как пакет | Безопасность, кроссплатформа, дистрибуция, UX | Онбординг новичков + дистрибуция через PyPI | «Оболочки» — веб-интерфейс и интеграция с IDE |
| Структура кода | Плоский корень репозитория | src-layout: src/stepik_grader/ + пакет core/ |
стабилизирована | → | + web.py, ide.py |
| Запуск | python grader.py |
stepik-grader / python -m stepik_grader.X |
python -m stepik_grader |
+ нативный файловый диалог (fallback без пути) | + --serve (Web UI), --init-vscode |
| CLI | Только интерактивное меню | + argparse (--mode/--file/--dir) |
+ --output json/csv/md, --watch, --lang, --verbose/--quiet |
→ | + веб-интерфейс, задачи VS Code |
| CI | Ubuntu (pytest + ruff) | Ubuntu | Ubuntu + Windows + macOS, + mypy | → | → |
| Безопасность | Только таймаут выполнения | Только таймаут | + лимит памяти RLIMIT_AS (POSIX), явные импорты вместо wildcard |
→ | → |
| Дистрибуция | git clone + requirements.txt |
pip install -e . (единый источник — pyproject.toml) |
GitHub Releases (sdist+wheel), pipx из git |
+ PyPI: pipx install stepik-python-grader (OIDC trusted publishing) |
→ |
| Версионирование | статичная строка | importlib.metadata (единый источник) |
задокументированная схема + scripts/version.py |
→ | → |
| Тестов / покрытие | 260 / 59% | 523 / 95% | 591 / 96% | 599 / 95% | 622 / 95% |
MAJOR остаётся
1на всём протяжении: все изменения укладываются в рамки «локальный инструмент для Python-задач Stepik». Смена MAJOR (2.0) предполагается только при фундаментальном выходе за эти рамки — другие языки программирования или платформы. Подробнее — в разделе «Версионирование»CONTRIBUTING.md.
Python 3.12+