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 в обратных кавычках;
  • многострочные блоки кода с ограждениями ``` или ~~~;
  • маркеры маркированных, нумерованных и 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


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.13.tar.gz (56.8 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.13-py3-none-any.whl (58.4 kB view details)

Uploaded Python 3

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

Hashes for contest_helper-0.6.13.tar.gz
Algorithm Hash digest
SHA256 2d62d1ad5b65713004b8ac83816ed97cd2c795188d662a59310be48490703f82
MD5 9d1817ad0ad827c68c3983fbfc50ddd1
BLAKE2b-256 e9d058b3cd0587fe09ae42c2a452b28f2694fc36aeb88edd08cecd08f8ba7e4b

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for contest_helper-0.6.13-py3-none-any.whl
Algorithm Hash digest
SHA256 1dd9a68bf200921a6637a700b7f3ed8c7093293042f87a4207a4c0a1e1252f21
MD5 4aa6a51f7b1a6d07493a453942a2297e
BLAKE2b-256 4f97a2d4ddfdd0c05de33425298b237cc27fe3514642423d870f283b62497077

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