Skip to content
Draft

Draft #433

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
2e3ed1e
Implement constants (#431)
mzuenni Feb 26, 2025
6045182
Add bt upgrade (#432)
mzuenni Mar 3, 2025
0ed40b1
Split up `problem_statement/` into `statement/`, `solution/`, and `pr…
mzuenni Mar 6, 2025
579fd1f
Output validator (#435)
mzuenni Mar 9, 2025
d000d4f
[problem] Update parsing of problem.yaml based on Kattis/problem-pack…
mpsijm Mar 16, 2025
dbe63f3
Add .download samples and ans_is_output flag (#436)
mzuenni Mar 23, 2025
e8e8cab
fix yaml handling
mzuenni Mar 24, 2025
b0f8d0d
readd empty test
mzuenni Mar 25, 2025
f4b76e1
[validate] Remove {input,answer}_format_validators (#443)
mzuenni Mar 30, 2025
57163d9
Legacy export (#441)
mzuenni Mar 30, 2025
186ea29
Use Validator.source_dir at all places (#444)
mzuenni Mar 30, 2025
ea019a9
change order
mzuenni Mar 30, 2025
f2c573d
Drop support for `data/bad` (#445)
mzuenni Mar 31, 2025
09b549f
fix a lot of typing issues
mzuenni Mar 31, 2025
5b65e91
more typing
mzuenni Mar 31, 2025
10c1c85
use config.SPEC_VERSION
mzuenni Mar 31, 2025
6c59478
no need to select language
mzuenni Mar 31, 2025
80b600f
Order in contest yaml (#446)
mzuenni Apr 2, 2025
7a63c54
change typing
mzuenni Apr 2, 2025
f088e36
Add secondary sort-keys to `--order-from-ccs` (#447)
mzuenni Apr 5, 2025
30a6a9a
Draft visualizer (#448)
mzuenni Apr 24, 2025
99d49fa
allow latex in <contest_dir>/latex
mzuenni May 3, 2025
1594ca1
.interaction file is based on output_validator
mzuenni May 6, 2025
734c460
make legacy export more robust
mzuenni May 7, 2025
bd5170f
less indent
mzuenni May 7, 2025
0904843
fix new problem
mzuenni May 13, 2025
96dd840
handle errors properly
mzuenni May 17, 2025
b027bdc
run generate before time_limit
mzuenni May 17, 2025
4738553
ensure that yaml_data only contains ruamel like types
mzuenni Jun 25, 2025
034f16b
improve testcase hashing
mzuenni Jun 25, 2025
1b7dc86
Add update_problems_yaml --number to use numeric Sxx labels
RagnarGrootKoerkamp Jul 12, 2025
51a8ca2
improve warning
mzuenni Jul 14, 2025
7dcb38e
improve log message
mzuenni Jul 15, 2025
d71516e
wsl is just slow?
mzuenni Jul 15, 2025
3b078e8
at least parse the allow_file_writing flag
mzuenni Jul 31, 2025
26a98fb
shortened code
mzuenni Aug 1, 2025
ccc23b9
Extend `bt fuzz` / update signal handling (#455)
mzuenni Aug 5, 2025
fab5899
check uuids
mzuenni Aug 10, 2025
afe6ded
i hate python default arguments
mzuenni Aug 11, 2025
30b7a27
[skel] Make source_name optional
mzuenni Aug 11, 2025
abb49f8
Allow parsing Person as map of name/email/kattis/orcid keys
mpsijm Aug 6, 2025
c5adbcf
contest.yaml: Rename key testsession to test_session
mpsijm Aug 13, 2025
2ce5c87
simplified code
mzuenni Aug 26, 2025
5394b82
Rename testdata.yaml to test_group.yaml and implement Test Case Confi…
mpsijm Aug 27, 2025
afbd529
Rename Test Case Visualizer back to Input Visualizer (#458)
mpsijm Aug 28, 2025
07c9605
always print all warnings
mzuenni Aug 28, 2025
beead26
avoid Path.open()
mzuenni Aug 30, 2025
05dd5f2
nicer error messages
mzuenni Aug 30, 2025
161fc05
fix git permission issue inside docker
mzuenni Aug 31, 2025
75f0dad
guard open
mzuenni Sep 1, 2025
fbb3d1c
guard open
mzuenni Sep 1, 2025
e821afa
do all checks in MiB
mzuenni Sep 1, 2025
942dc3b
simplify code
mzuenni Sep 1, 2025
45b3869
fix comment
mzuenni Sep 1, 2025
702448e
fix keaboard interrupt from bt test on interactive problems
mzuenni Sep 1, 2025
5f3769e
[upgrade] Use shlex.split() to migrate validator args from str to list
mpsijm Sep 1, 2025
8695cd4
[upgrade] Avoid crash on legacy empty test case
mpsijm Sep 1, 2025
8ccb971
set flow style
mzuenni Sep 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jobs:
container: ragnargrootkoerkamp/bapctools
steps:
- uses: actions/checkout@v4
- run: git config --global --add safe.directory "$(pwd)"
- run: bash test/yaml/generators/test_schemata.sh
- run: pytest

Expand Down Expand Up @@ -50,5 +51,7 @@ jobs:
lmodern
texlive-science
latexmk
texlive-lang-german
asymptote
- shell: wsl-bash {0}
run: pytest
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.9
hooks:
- id: ruff-format
- id: ruff
args: [ --fix ]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.15.0
hooks:
Expand Down
39 changes: 28 additions & 11 deletions bin/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import re
from pathlib import Path
from collections.abc import Mapping, Sequence
from typing import Final, Literal, Optional
from typing import Any, Final, Literal, Optional

SPEC_VERSION: Final[str] = "2023-07-draft"

# return values
RTV_AC: Final[int] = 42
Expand All @@ -32,9 +34,20 @@
# When --table is set, this threshold determines the number of identical profiles needed to get flagged.
TABLE_THRESHOLD: Final[int] = 4

FILE_NAME_REGEX: Final[str] = "[a-zA-Z0-9][a-zA-Z0-9_.-]*[a-zA-Z0-9]"
FILE_NAME_REGEX: Final[str] = "[a-zA-Z0-9][a-zA-Z0-9_.-]{0,253}[a-zA-Z0-9]"
COMPILED_FILE_NAME_REGEX: Final[re.Pattern[str]] = re.compile(FILE_NAME_REGEX)

CONSTANT_NAME_REGEX = "[a-zA-Z_][a-zA-Z0-9_]*"
COMPILED_CONSTANT_NAME_REGEX: Final[re.Pattern[str]] = re.compile(CONSTANT_NAME_REGEX)
CONSTANT_SUBSTITUTE_REGEX: Final[re.Pattern[str]] = re.compile(
f"\\{{\\{{({CONSTANT_NAME_REGEX})\\}}\\}}"
)

BAPCTOOLS_SUBSTITUTE_REGEX: Final[re.Pattern[str]] = re.compile(
f"\\{{%({CONSTANT_NAME_REGEX})%\\}}"
)


KNOWN_TESTCASE_EXTENSIONS: Final[Sequence[str]] = [
".in",
".ans",
Expand All @@ -48,14 +61,18 @@
".pdf",
]

KNOWN_SAMPLE_TESTCASE_EXTENSIONS: Final[Sequence[str]] = [
".in.statement",
".ans.statement",
".in.download",
".ans.download",
]

KNOWN_TEXT_DATA_EXTENSIONS: Final[Sequence[str]] = [
*KNOWN_TESTCASE_EXTENSIONS,
*KNOWN_SAMPLE_TESTCASE_EXTENSIONS,
".interaction",
".hint",
".desc",
".in.statement",
".ans.statement",
#'.args',
".yaml",
]

KNOWN_DATA_EXTENSIONS: Final[Sequence[str]] = [
Expand All @@ -67,7 +84,6 @@
"invalid_input",
"invalid_answer",
"invalid_output",
"bad",
]


Expand All @@ -86,11 +102,12 @@

args = argparse.Namespace()

DEFAULT_ARGS: Final[Mapping] = {
DEFAULT_ARGS: Final[Mapping[str, Any]] = {
"jobs": (os.cpu_count() or 1) // 2,
"time": 600, # Used for `bt fuzz`
"verbose": 0,
"languages": None,
"action": None,
"no_visualizer": True,
}


Expand All @@ -101,7 +118,7 @@
grep -Ev '^(h|jobs|time|verbose)$' | sed 's/^/"/;s/$/",/' | tr '\n' ' ' | sed 's/^/ARGS_LIST: Final[Sequence[str]] = [/;s/, $/]\n/'
"""
# fmt: off
ARGS_LIST: Final[Sequence[str]] = ["1", "add", "all", "answer", "api", "author", "check_deterministic", "clean", "colors", "contest", "contest_id", "contestname", "cp", "defaults", "default_solution", "depth", "directory", "error", "force", "force_build", "generic", "input", "interaction", "interactive", "invalid", "kattis", "language", "latest_bt", "memory", "more", "move_to", "no_bar", "no_generate", "no_solution", "no_solutions", "no_testcase_sanity_checks", "no_time_limit", "no_validators", "no_visualizer", "open", "order", "order_from_ccs", "overview", "password", "post_freeze", "problem", "problemname", "remove", "reorder", "samples", "sanitizer", "skel", "skip", "sort", "submissions", "table", "testcases", "time_limit", "timeout", "token", "tree", "type", "username", "valid_output", "watch", "web", "write"]
ARGS_LIST: Final[Sequence[str]] = ["1", "add", "all", "answer", "api", "author", "check_deterministic", "clean", "colors", "contest", "contest_id", "contestname", "cp", "defaults", "default_solution", "depth", "directory", "error", "force", "force_build", "generic", "input", "interaction", "interactive", "invalid", "kattis", "lang", "latest_bt", "legacy", "memory", "more", "move_to", "no_bar", "no_generate", "no_solution", "no_solutions", "no_testcase_sanity_checks", "no_time_limit", "no_validators", "no_visualizer", "number", "open", "order", "order_from_ccs", "overview", "password", "post_freeze", "problem", "problemname", "remove", "reorder", "samples", "sanitizer", "skel", "skip", "sort", "submissions", "table", "testcases", "time_limit", "timeout", "token", "tree", "type", "username", "valid_output", "watch", "web", "write"]
# fmt: on


Expand Down
50 changes: 28 additions & 22 deletions bin/constraints.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import re
from collections import defaultdict
from typing import Optional

import latex
import validate
from colorama import Fore, Style
from problem import Problem

# Local imports
from util import *
Expand All @@ -15,21 +18,23 @@
"""


def check_validators(problem):
def check_validators(
problem: Problem,
) -> tuple[set[int | float], list[str | tuple[int | float, str, int | float]]]:
in_constraints: validate.ConstraintsDict = {}
ans_constraints: validate.ConstraintsDict = {}
problem.validate_data(validate.Mode.INPUT, constraints=in_constraints)
if not in_constraints:
warn("No constraint validation of input values found in input validators.")
problem.validate_data(validate.Mode.ANSWER, constraints=ans_constraints)
if not problem.interactive and not problem.multi_pass and not ans_constraints:
if not problem.settings.ans_is_output and not ans_constraints:
log("No constraint validation of answer values found in answer or output validators.")
print()

validator_values = set()
validator_values: set[int | float] = set()
validator_defs: list[str | tuple[int | float, str, int | float]] = []

def f(cs):
def f(cs: validate.ConstraintsDict) -> None:
for loc, value in sorted(cs.items()):
name, has_low, has_high, vmin, vmax, low, high = value
validator_defs.append((low, name, high))
Expand All @@ -44,12 +49,12 @@ def f(cs):
return validator_values, validator_defs


def check_statement(problem, language):
statement_file = problem.path / f"problem_statement/problem.{language}.tex"
def check_statement(problem: Problem, language: str) -> tuple[set[int | float], list[str]]:
statement_file = problem.path / latex.PdfType.PROBLEM.path(language)
statement = statement_file.read_text()

statement_values = set()
statement_defs = []
statement_values: set[int | float] = set()
statement_defs: list[str] = []

defines = ["\\def", "\\newcommand"]
sections = ["Input", "Output", "Interaction"]
Expand All @@ -66,15 +71,16 @@ def check_statement(problem, language):
}
relations = re.compile(r"(<=|!=|>=|<|=|>)")

def math_eval(text):
def math_eval(text: str) -> Optional[int | float]:
try:
# eval is dangerous, but on the other hand we run submission code so this is fine
text = text.replace("^", "**")
return eval(text, {"__builtin__": None})
value = eval(text, {"__builtin__": None})
return value if isinstance(value, (int, float)) else None
except (SyntaxError, NameError, TypeError, ZeroDivisionError):
return None

def constraint(text):
def constraint(text: str) -> None:
# handles $$math$$
if len(text) == 0:
return
Expand Down Expand Up @@ -131,13 +137,13 @@ def constraint(text):
in_io = False
end = None

def matches(text):
def matches(text: str) -> bool:
nonlocal pos
if pos + len(text) > len(statement):
return False
return statement[pos : pos + len(text)] == text

def parse_group():
def parse_group() -> str:
nonlocal pos
assert statement[pos] == "{"
next = pos + 1
Expand All @@ -154,7 +160,7 @@ def parse_group():
pos = next
return name

def parse_command():
def parse_command() -> str:
nonlocal pos
assert statement[pos] == "\\"
next = pos + 1
Expand All @@ -170,7 +176,7 @@ def parse_command():
# 3) if a section starts parse that (and ensure that no environment is active)
# 4) if an environment begins parse that (and ensure that no other environment is active)
# 5) if a new define starts parse that
# 6) if inline math starts in an input/ouput part parse it as constraint
# 6) if inline math starts in an input/output part parse it as constraint
while pos < len(statement):
if statement[pos] == "%":
next = statement.find("\n", pos)
Expand Down Expand Up @@ -250,16 +256,16 @@ def parse_command():
return statement_values, statement_defs


def check_constraints(problem):
def check_constraints(problem: Problem) -> bool:
validator_values, validator_defs = check_validators(problem)
statement_values = defaultdict(set)
statement_defs = defaultdict(set)
statement_values: dict[int | float, set[str]] = defaultdict(set)
statement_defs: dict[str, set[str]] = defaultdict(set)
for lang in problem.statement_languages:
values, defs = check_statement(problem, lang)
for entry in values:
statement_values[entry].add(lang)
for entry in defs:
statement_defs[entry].add(lang)
for value_entry in values:
statement_values[value_entry].add(lang)
for def_entry in defs:
statement_defs[def_entry].add(lang)

# print all the definitions.
value_len = 12
Expand Down
28 changes: 14 additions & 14 deletions bin/contest.py
Original file line number Diff line number Diff line change
@@ -1,47 +1,47 @@
import config

from pathlib import Path
from typing import cast, Any, Optional

from util import *

# Read the contest.yaml, if available
_contest_yaml = None
_contest_yaml: Optional[dict[str, Any]] = None


def contest_yaml():
def contest_yaml() -> dict[str, Any]:
global _contest_yaml
if _contest_yaml is not None:
return _contest_yaml

# TODO: Do we need both here?
for p in [Path("contest.yaml"), Path("../contest.yaml")]:
if p.is_file():
_contest_yaml = read_yaml_settings(p)
return _contest_yaml
contest_yaml_path = Path("contest.yaml")
if contest_yaml_path.is_file():
_contest_yaml = read_yaml_settings(contest_yaml_path)
return _contest_yaml
_contest_yaml = {}
return _contest_yaml


_problems_yaml = None


def problems_yaml():
def problems_yaml() -> Optional[list[dict[str, Any]]]:
global _problems_yaml
if _problems_yaml:
return _problems_yaml
if _problems_yaml is False:
return None
if _problems_yaml:
return _problems_yaml

problemsyaml_path = Path("problems.yaml")
if not problemsyaml_path.is_file():
_problems_yaml = False
return None
_problems_yaml = read_yaml(problemsyaml_path)
return _problems_yaml
return cast(list[dict[str, Any]], _problems_yaml)


def get_api():
api = config.args.api or contest_yaml().get("api")
def get_api() -> str:
api = config.args.api or cast(str, contest_yaml().get("api"))
if not api:
fatal(
"Could not find key `api` in contest.yaml and it was not specified on the command line."
Expand Down Expand Up @@ -105,7 +105,7 @@ def call_api(method, endpoint, **kwargs):
return r


def call_api_get_json(url):
def call_api_get_json(url: str):
r = call_api("GET", url)
r.raise_for_status()
try:
Expand Down
Loading
Loading