A flexible framework for generating randomized test cases with input/output validation, designed for competitive programming and algorithmic testing.
Project description
Contest Helper
contest-helper — Python-библиотека и набор CLI-инструментов для подготовки задач к импорту в Яндекс Контест.
Библиотека помогает:
- создавать заготовку новой задачи;
- генерировать входные данные и ответы с помощью эталонного решения;
- сохранять текстовые, бинарные и SQLite-тесты;
- запускать решение на локальном наборе тестов;
- добавлять примеры в условие;
- настраивать метаданные задачи и собирать ZIP-архив;
- создавать checker и postprocessor из шаблонов;
- типографировать условие с сохранением формул и inline-кода.
Требования
- Python 3.10 или новее;
- доступ к интернету для команды
ch-typograf; - пакет
tabulate, устанавливаемый автоматически как зависимость.
Установка
Из PyPI:
python3 -m pip install contest-helper
Из исходного кода для разработки:
git clone https://github.com/kirillcskaslezin/contest-helper.git
cd contest-helper
python3 -m pip install -e .
После установки становятся доступны команды с префиксом ch-.
Быстрый старт
Создайте директорию задачи:
ch-start-problem sum -l ru
cd sum
Команда создаст:
sum/
├── generator.py
├── meta.json
└── statement.md
С флагом --checker также будет создан checker.py:
ch-start-problem sum -l ru --checker
Опишите тип входных данных, эталонное решение, генератор и адаптеры в generator.py, после чего запустите:
python3 generator.py
Сгенерированные тесты появятся в директории tests:
tests/
├── 01
├── 01.a
├── 02
└── 02.a
Проверьте решение и соберите архив:
ch-test ./solution --timeout 2
ch-typograf statement.md
cd ..
ch-combine sum --time-limit 2000 --memory-limit 268435456
Результатом будет sum.zip.
Генерация тестов
Основной класс библиотеки — Generator из модуля contest_helper.basic. Он:
- очищает или создаёт директорию
tests; - обрабатывает указанные примеры;
- генерирует случайные тесты;
- передаёт каждый тест эталонному решению;
- проверяет результат через необязательный validator;
- записывает входной файл и ответ с суффиксом
.a.
Минимальный пример для задачи сложения двух чисел:
from typing import Iterable
from contest_helper.basic import Generator, TextInputAdapter, TextOutputAdapter
from contest_helper.values import CombineValues, RandomNumber
class PairInputAdapter(TextInputAdapter):
def parse_lines(self, lines: Iterable[str]) -> list[int]:
a, b = list(lines)[0].split()
return [int(a), int(b)]
def input_lines(self, data: list[int]) -> Iterable[str]:
a, b = data
return [f"{a} {b}"]
class IntegerOutputAdapter(TextOutputAdapter):
def output_lines(self, result: int) -> Iterable[str]:
return [str(result)]
def solution(data: list[int]) -> int:
return sum(data)
generator = Generator(
solution=solution,
tests_generator=CombineValues([
RandomNumber(1, 101),
RandomNumber(1, 101),
]),
tests_count=20,
input_adapter=PairInputAdapter(),
output_adapter=IntegerOutputAdapter(),
)
generator.run()
RandomNumber(start, stop, step) использует полуинтервал [start, stop). Например, RandomNumber(1, 101) генерирует целые числа от 1 до 100.
Примеры из файлов
Пути к входным файлам можно передать через samples. Адаптер разбирает содержимое, эталонное решение вычисляет ответ, а исходный формат примера сохраняется:
generator = Generator(
solution=solution,
samples=["samples/01.txt", "samples/02.txt"],
tests_generator=CombineValues([
RandomNumber(1, 101),
RandomNumber(1, 101),
]),
tests_count=10,
input_adapter=PairInputAdapter(),
output_adapter=IntegerOutputAdapter(),
)
Примеры получают имена sample01, sample02 и так далее.
Несколько групп генераторов
Для нескольких наборов входных данных передайте параллельные списки генераторов и количества тестов:
from contest_helper.basic import Generator, TextInputAdapter, TextOutputAdapter
from contest_helper.values import RandomNumber
generator = Generator(
solution=lambda value: value * value,
tests_generator=[
RandomNumber(1, 11),
RandomNumber(1_000, 10_001),
],
tests_count=[5, 10],
input_adapter=TextInputAdapter(),
output_adapter=TextOutputAdapter(),
)
generator.run()
Тесты всех групп нумеруются последовательно: 01, 02, 03 и далее.
Проверка сгенерированных тестов
validator получает входные данные и ответ эталонного решения. Если он возвращает False, тест отбрасывается и генерируется заново:
def validator(data: list[int], result: int) -> bool:
return data[0] != data[1] and result >= 0
Тест также можно отклонить непосредственно из эталонного решения:
from contest_helper.exceptions import BadTestException
def solution(data: list[int]) -> int:
if data[1] == 0:
raise BadTestException("division by zero")
return data[0] // data[1]
Генераторы значений
Генераторы находятся в contest_helper.values и являются вызываемыми объектами без аргументов.
Константы и функции
import time
from contest_helper.values import Lambda, Value
constant = Value(42)
timestamp = Lambda(time.time)
print(constant())
print(timestamp())
Случайные значения
from contest_helper.values import RandomNumber, RandomValue, RandomWord
color = RandomValue(["red", "green", "blue"])
number = RandomNumber(0, 100)
word = RandomWord(min_length=5, max_length=12)
Доступны:
RandomValue(sequence)— случайный элемент последовательности;RandomNumber(start, stop, step=1)— число из заданного диапазона;RandomWord(...)— случайная строка;RandomSentence(...)— последовательность случайных слов;RandomList(generator, length)— список;RandomSet(generator, length)— множество уникальных элементов;RandomDict(key_generator, value_generator, length)— словарь;CombineValues(sequence)— список результатов нескольких генераторов.
Пример матрицы 5 × 5:
from contest_helper.values import RandomList, RandomNumber
matrix = RandomList(
RandomList(RandomNumber(0, 10), length=5),
length=5,
)
print(matrix())
Размер коллекции также может быть генератором:
values = RandomList(
RandomNumber(0, 100),
length=RandomNumber(1, 11),
)
При использовании RandomSet, RandomDict или уникальных столбцов базы данных генератор должен уметь выдать достаточное количество разных значений.
Даты и время
Дополнительные генераторы импортируются из contest_helper.extra.datetime:
from contest_helper.extra.datetime import RandomDate, RandomDateTime, RandomTime
date_generator = RandomDate(
"2025-01-01",
"2025-12-31",
strftime="%d.%m.%Y",
)
time_generator = RandomTime("09:00", "18:00", step_seconds=60)
datetime_generator = RandomDateTime(
"2025-01-01 00:00:00",
"2025-01-31 23:59:59",
)
Границы диапазонов даты и времени включаются.
Адаптеры ввода и вывода
Адаптеры отделяют структуру данных Python от формата тестовых файлов.
Текстовые адаптеры
Для входных данных наследуйте TextInputAdapter и при необходимости переопределите:
parse_lines(lines)— чтение примера из файла;input_lines(data)— сериализацию сгенерированного теста.
Для ответа наследуйте TextOutputAdapter и переопределите output_lines(result).
from typing import Iterable
from contest_helper.basic import TextInputAdapter, TextOutputAdapter
class ListInputAdapter(TextInputAdapter):
def parse_lines(self, lines: Iterable[str]) -> list[int]:
return [int(value) for value in list(lines)[1].split()]
def input_lines(self, values: list[int]) -> Iterable[str]:
return [str(len(values)), " ".join(map(str, values))]
class ListOutputAdapter(TextOutputAdapter):
def output_lines(self, values: list[int]) -> Iterable[str]:
return [" ".join(map(str, values))]
Бинарные адаптеры
Для бинарных тестов используются BinaryInputAdapter и BinaryOutputAdapter:
from contest_helper.basic import BinaryInputAdapter, BinaryOutputAdapter
class BytesInputAdapter(BinaryInputAdapter):
def parse_bytes(self, blob: bytes) -> bytes:
return blob
def input_bytes(self, data: bytes):
return [data]
class BytesOutputAdapter(BinaryOutputAdapter):
def output_bytes(self, result: bytes):
return [result]
Генерация SQLite-баз
Модуль contest_helper.extra.db позволяет генерировать связанные таблицы и использовать SQLite-файл как вход или ответ задачи.
import random
from contest_helper.extra.db import (
ColumnSpec,
ForeignKey,
SQLiteConnectionDataBase,
Table,
)
users = Table(
name="users",
rows=10,
columns={
"id": ColumnSpec(lambda: random.randint(1, 1_000_000), unique=True),
"name": ColumnSpec(lambda: f"user_{random.randint(1, 9999)}"),
},
)
posts = Table(
name="posts",
rows=20,
columns={
"id": ColumnSpec(lambda: random.randint(1, 1_000_000), unique=True),
"user_id": ColumnSpec(ForeignKey("users", "id")),
"title": ColumnSpec(lambda: f"post_{random.randint(1, 9999)}"),
},
)
database = SQLiteConnectionDataBase(users, posts)
connection = database()
Для записи базы в тестовый файл используйте SQLiteConnInputAdapter или SQLiteConnOutputAdapter.
CLI-команды
ch-start-problem
Создаёт новую директорию задачи из встроенных шаблонов:
ch-start-problem DIRECTORY [options]
Основные параметры:
| Параметр | Назначение |
|---|---|
-l, --language {en,ru} |
Язык шаблона условия |
-c, --checker |
Создать checker.py |
-i, --input-type {text,binary} |
Тип входного адаптера |
-o, --output-type {text,binary} |
Тип выходного адаптера |
Пример:
ch-start-problem graph -l ru -c
ch-test
Запускает решение на всех входных файлах из локальной директории tests. Для каждого входного файла ожидается файл ответа с тем же именем и суффиксом .a.
ch-test SOLUTION [-t SECONDS] [-c CHECKER] [-i INTERPRETER]
Примеры:
ch-test ./solution
ch-test ./solution --timeout 3 --checker ./checker
ch-test solution.py --interpreter python3
Без checker результаты сравниваются как текст после удаления пробельных символов по краям всего вывода.
Файл решения должен быть исполняемым даже при использовании --interpreter; при необходимости выполните chmod +x solution.py.
ch-statement-preview
Находит файлы tests/sampleNN и tests/sampleNN.a и добавляет оформленные примеры в условие:
ch-statement-preview DIRECTORY --lang ru
Результат можно записать в другой файл:
ch-statement-preview DIRECTORY --lang ru --output preview.md
Команда дописывает примеры к существующему statement.md. При повторном запуске в тот же файл ранее добавленные примеры автоматически не удаляются.
ch-typograf
Отправляет содержимое файла в веб-сервис «Типограф» Студии Артемия Лебедева и перезаписывает файл результатом:
ch-typograf statement.md
Команда сохраняет без изменений:
- формулы
$...$; - блочные формулы
$$...$$; - inline-code в обратных кавычках;
- многострочные блоки кода с ограждениями
```или~~~; - маркеры маркированных, нумерованных и task-списков Markdown;
- пустые строки, разделяющие блоки Markdown.
Перед запуском рекомендуется сохранить изменения в системе контроля версий: файл обновляется на месте. Команде требуется сетевой доступ к typograf.artlebedev.ru.
ch-combine
Обновляет meta.json по содержимому директории и параметрам командной строки, после чего создаёт ZIP-архив для импорта:
ch-combine DIRECTORY [options]
Пример:
ch-combine sum \
--time-limit 2000 \
--memory-limit 268435456 \
--checker-files checker.py \
--solutions python3_13:solution.py
Поддерживаемые настройки:
| Параметр | Назначение |
|---|---|
--checker-files ... |
Файлы checker |
--compile-files ... |
Файлы, добавляемые при компиляции |
--run-files ... |
Файлы, добавляемые при запуске |
--post-files ... |
Файлы postprocessor |
--solutions ... |
Авторские решения в формате compiler_id:path |
--time-limit |
Ограничение времени, мс |
--idleness-limit |
Ограничение бездействия, мс |
--memory-limit |
Ограничение памяти, байты |
--output-limit |
Ограничение вывода, байты |
--input-file |
Имя входного файла |
--output-file |
Имя выходного файла |
--disable-stdin |
Отключить перенаправление stdin |
--disable-stdout |
Отключить перенаправление stdout |
--hide-limits |
Скрыть ограничения в условии |
--hide-io |
Скрыть секции ввода и вывода |
--hide-samples |
Скрыть примеры |
Команда изменяет DIRECTORY/meta.json перед упаковкой. Архив создаётся рядом с директорией и получает имя DIRECTORY.zip.
ch-make-checker
Создаёт checker.py из встроенного шаблона в текущей директории:
ch-make-checker
ch-make-postprocessor
Создаёт настроенный postprocessor.py в текущей директории:
ch-make-postprocessor --max-value 100 --groups "3,5,2" --by-groups
Параметры:
--max-value— максимальный балл;--groups— размеры групп через запятую;--different— дифференцированное оценивание;--by-groups— оценивание по группам.
ch-compilers
Ищет доступные компиляторы по части названия без учёта регистра:
ch-compilers python
ch-compilers "c++"
Команда выводит идентификатор компилятора, который можно использовать в --solutions при сборке задачи.
Структура проекта задачи
Типичная рабочая директория выглядит так:
problem/
├── checker.py
├── generator.py
├── meta.json
├── postprocessor.py
├── solution.py
├── statement.md
└── tests/
├── sample01
├── sample01.a
├── 01
├── 01.a
└── ...
Обязательный для ch-combine файл — meta.json. Остальные файлы добавляются в архив при наличии.
Разработка
Запуск тестов:
python3 -m unittest discover -s tests -v
Проверка синтаксиса пакета:
python3 -m compileall -q contest_helper tests
Лицензия
Проект распространяется по лицензии MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file contest_helper-0.6.13.tar.gz.
File metadata
- Download URL: contest_helper-0.6.13.tar.gz
- Upload date:
- Size: 56.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2d62d1ad5b65713004b8ac83816ed97cd2c795188d662a59310be48490703f82
|
|
| MD5 |
9d1817ad0ad827c68c3983fbfc50ddd1
|
|
| BLAKE2b-256 |
e9d058b3cd0587fe09ae42c2a452b28f2694fc36aeb88edd08cecd08f8ba7e4b
|
File details
Details for the file contest_helper-0.6.13-py3-none-any.whl.
File metadata
- Download URL: contest_helper-0.6.13-py3-none-any.whl
- Upload date:
- Size: 58.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1dd9a68bf200921a6637a700b7f3ed8c7093293042f87a4207a4c0a1e1252f21
|
|
| MD5 |
4aa6a51f7b1a6d07493a453942a2297e
|
|
| BLAKE2b-256 |
4f97a2d4ddfdd0c05de33425298b237cc27fe3514642423d870f283b62497077
|