Skip to main content

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. Он:

  1. очищает или создаёт директорию tests;
  2. обрабатывает указанные примеры;
  3. генерирует случайные тесты;
  4. передаёт каждый тест эталонному решению;
  5. проверяет результат через необязательный validator;
  6. записывает входной файл и ответ с суффиксом .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 в обратных кавычках.

Перед запуском рекомендуется сохранить изменения в системе контроля версий: файл обновляется на месте. Команде требуется сетевой доступ к 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

contest_helper-0.6.12.tar.gz (55.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

contest_helper-0.6.12-py3-none-any.whl (58.2 kB view details)

Uploaded Python 3

File details

Details for the file contest_helper-0.6.12.tar.gz.

File metadata

  • Download URL: contest_helper-0.6.12.tar.gz
  • Upload date:
  • Size: 55.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.5

File hashes

Hashes for contest_helper-0.6.12.tar.gz
Algorithm Hash digest
SHA256 4af0997042219768e18383cec00bc9bbd6947894ed5bdbfefe08ce40c2929295
MD5 76ebd6a9514e6bf6c72f206358ec3435
BLAKE2b-256 3736fce8a52d44f5b19dc3ccc81840f72f5000942b96626022b8c4b0802c2785

See more details on using hashes here.

File details

Details for the file contest_helper-0.6.12-py3-none-any.whl.

File metadata

File hashes

Hashes for contest_helper-0.6.12-py3-none-any.whl
Algorithm Hash digest
SHA256 5958ec1cb842481fa4015c60ad5ba6f885ae2d0caad6a9091c6b034e535d6868
MD5 fa1554b0b18807a2dbb7bb98500023ac
BLAKE2b-256 c2bc46805b57a76afcebe53e83fe10dba7f6d0cad73c73c6eb15b563b2cf4648

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page