Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4176b2d
Add more types
mzuenni Oct 20, 2025
0e404e1
rework default solution
mzuenni Oct 21, 2025
4dc7f70
more typing
mzuenni Oct 21, 2025
9ab565a
Introduce config.Args class
mzuenni Oct 22, 2025
0a1ae16
fix use of bool variable and moved deprecation warning
mzuenni Oct 22, 2025
95699b1
enable strict
mzuenni Oct 22, 2025
ea1b02b
ignore code not used for bt
mzuenni Oct 22, 2025
95846fd
made parsing more lenient
mzuenni Oct 22, 2025
bf8e291
Update handling of default solution
mzuenni Oct 23, 2025
3a465df
fix
mzuenni Oct 23, 2025
a4fe580
use generator for counting testcases
mzuenni Oct 23, 2025
6b496d9
use ProgressBar
mzuenni Oct 23, 2025
974b0d7
cleanup
mzuenni Oct 23, 2025
228dfa8
empty keys should be None
mzuenni Oct 23, 2025
98507d9
Cleanup & fix tests
mzuenni Oct 23, 2025
e7b50ef
disallow star imports
mzuenni Oct 24, 2025
08563a2
Add doc/workflow.md
RagnarGrootKoerkamp Oct 29, 2025
17f26d4
remove * import
mzuenni Oct 24, 2025
3611cc7
Improve typing
mzuenni Oct 24, 2025
6ccf053
print => eprint to get rid of all file=sys.stderr
mzuenni Oct 24, 2025
7355dd8
removed unused file argument
mzuenni Oct 24, 2025
3a9752c
Code cleanup to reduce line wrapping
mzuenni Oct 24, 2025
5dbe71c
less flickering in verdicts :)
mzuenni Oct 24, 2025
e48d03a
use error instead of difference in wording
mzuenni Oct 24, 2025
be0ea54
Use PrintBar instead of 'message()' fn
mzuenni Oct 26, 2025
62cd22c
remove unreachable code
mzuenni Oct 27, 2025
fb453d0
fix interactive multipass logic
mzuenni Oct 27, 2025
0fe1ab4
Ruff sorts imports
mzuenni Oct 27, 2025
0f0eafa
automatically organize imports
mzuenni Oct 27, 2025
c805e4d
Warn if output validator ever crashes
mzuenni Oct 28, 2025
d5550fb
Review fixes/comments
RagnarGrootKoerkamp Oct 29, 2025
c6bfc97
Update testcase.py
mzuenni Oct 29, 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
7 changes: 5 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ repos:
hooks:
- id: ruff
args: [ --fix ]
- id: ruff
files: ^bin/.*\.py$
args: ["--select=I", "--fix"]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.15.0
Expand All @@ -30,5 +33,5 @@ repos:
- --no-incremental # Fixes ruamel.yaml, see https://stackoverflow.com/a/65223004
- --python-version=3.10
- --scripts-are-modules
#- --strict # TODO #102: Enable flag once everything has type annotations
exclude: ^test/
- --strict
exclude: ^(test|skel|scripts|bin/misc)/
19 changes: 14 additions & 5 deletions bin/check_testing_tool.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import shutil
import sys
from collections.abc import Sequence
from pathlib import Path
from typing import Optional, Sequence
from typing import Optional, TYPE_CHECKING

import config
import parallel
from program import Program
from run import Submission
from util import *
from util import (
command_supports_memory_limit,
default_exec_code_map,
ensure_symlink,
error,
ExecResult,
ExecStatus,
ProgressBar,
)

if TYPE_CHECKING: # Prevent circular import: https://stackoverflow.com/a/39757388
from problem import Problem
Expand All @@ -28,7 +37,7 @@


class TestInput:
def __init__(self, problem: "Problem", in_path: Path, short_path: Path):
def __init__(self, problem: "Problem", in_path: Path, short_path: Path) -> None:
assert in_path.suffix in [".in", ".download", ".statement"]
self.problem = problem
self.in_path = in_path
Expand All @@ -43,7 +52,7 @@ def __init__(self, problem: "Problem", in_path: Path, short_path: Path):


class WrappedSubmission:
def __init__(self, problem: "Problem", submission: Submission):
def __init__(self, problem: "Problem", submission: Submission) -> None:
self.problem = problem
self.submission = submission
self.name = submission.name
Expand Down Expand Up @@ -156,7 +165,7 @@ def run(self, bar: ProgressBar, testing_tool: "TestingTool", testinput: TestInpu


class TestingTool(Program):
def __init__(self, problem: "Problem", path: Path):
def __init__(self, problem: "Problem", path: Path) -> None:
super().__init__(
problem,
path,
Expand Down
216 changes: 174 additions & 42 deletions bin/config.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
# Global variables that are constant after the programs arguments have been parsed.

import argparse
import copy
import os
import re
import sys
from collections.abc import Sequence
from colorama import Fore, Style
from pathlib import Path
from collections.abc import Mapping, Sequence
from typing import Any, Final, Literal, Optional
from typing import Any, Final, Literal, Optional, TypeVar

# Randomly generated uuid4 for BAPCtools
BAPC_UUID: Final[str] = "8ee7605a-d1ce-47b3-be37-15de5acd757e"
BAPC_UUID_PREFIX: Final[int] = 8

SPEC_VERSION: Final[str] = "2025-09"

Expand Down Expand Up @@ -103,42 +109,6 @@

# Below here is some global state that will be filled in main().

args = argparse.Namespace()

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


# The list of arguments below is generated using the following command:
"""
for cmd in $(bt --help | grep '^ {' | sed 's/ {//;s/}//;s/,/ /g') ; do bt $cmd --help ; done |& \
grep '^ [^ ]' | sed 's/^ //' | cut -d ' ' -f 1 | sed -E 's/,//;s/^-?-?//;s/-/_/g' | sort -u | \
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", "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


def set_default_args() -> list[str]:
# Set default argument values.
missing = []
for arg in ARGS_LIST:
if not hasattr(args, arg):
setattr(args, arg, None)
missing.append(arg)
for arg, value in DEFAULT_ARGS.items():
if not hasattr(args, arg):
setattr(args, arg, value)
missing.append(arg)
return missing


level: Optional[Literal["problem", "problemset"]] = None

# The number of warnings and errors encountered.
Expand All @@ -152,6 +122,168 @@ def set_default_args() -> list[str]:
TEST_TLE_SUBMISSIONS: bool = False


# Randomly generated uuid4 for BAPCtools
BAPC_UUID: Final[str] = "8ee7605a-d1ce-47b3-be37-15de5acd757e"
BAPC_UUID_PREFIX: Final[int] = 8
class ARGS:
def __init__(self, source: str | Path, **kwargs: Any) -> None:
self._set = set[str]()
self._source = source

def warn(msg: Any) -> None:
global n_warn
print(f"{Fore.YELLOW}WARNING: {msg}{Style.RESET_ALL}", file=sys.stderr)
n_warn += 1

T = TypeVar("T")

def normalize_arg(value: Any, t: type[Any]) -> Any:
if isinstance(value, str) and t is Path:
value = Path(value)
if isinstance(value, int) and t is float:
value = float(value)
if isinstance(value, bool) and t is int:
value = bool(value)
return value

def get_optional_arg(key: str, t: type[T], constraint: Optional[str] = None) -> Optional[T]:
if key in kwargs:
value = normalize_arg(kwargs.pop(key), t)
if constraint:
assert isinstance(value, (float, int))
if not eval(f"{value} {constraint}"):
warn(
f"value for '{key}' in {source} should be {constraint} but is {value}. SKIPPED."
)
return None
if isinstance(value, t):
self._set.add(key)
return value
warn(f"incompatible value for key '{key}' in {source}. SKIPPED.")
return None

def get_list_arg(
key: str, t: type[T], constraint: Optional[str] = None
) -> Optional[list[T]]:
values = get_optional_arg(key, list)
if values is None:
return None
checked = []
for value in values:
value = normalize_arg(value, t)
if constraint:
assert isinstance(value, (float, int))
if not eval(f"{value} {constraint}"):
warn(
f"value for '{key}' in {source} should be {constraint} but is {value}. SKIPPED."
)
continue
if not isinstance(value, t):
warn(f"incompatible value for key '{key}' in {source}. SKIPPED.")
continue
checked.append(value)
return checked

def get_arg(key: str, default: T, constraint: Optional[str] = None) -> T:
value = get_optional_arg(key, type(default), constraint)
result = default if value is None else value
return result

setattr(self, "1", get_arg("1", False))
self.action: Optional[str] = get_optional_arg("action", str)
self.add: Optional[list[Path]] = get_list_arg("add", Path)
self.all: int = get_arg("all", 0)
self.answer: bool = get_arg("answer", False)
self.api: Optional[str] = get_optional_arg("api", str)
self.author: Optional[str] = get_optional_arg("author", str)
self.check_deterministic: bool = get_arg("check_deterministic", False)
self.clean: bool = get_arg("clean", False)
self.colors: Optional[str] = get_optional_arg("colors", str)
self.contest: Optional[Path] = get_optional_arg("contest", Path)
self.contest_id: Optional[str] = get_optional_arg("contest_id", str)
self.contestname: Optional[str] = get_optional_arg("contestname", str)
self.cp: bool = get_arg("cp", False)
self.defaults: bool = get_arg("defaults", False)
self.default_solution: Optional[Path] = get_optional_arg("default_solution", Path)
self.depth: Optional[int] = get_optional_arg("depth", int, ">= 0")
self.directory: list[Path] = get_list_arg("directory", Path) or []
self.error: bool = get_arg("error", False)
self.force: bool = get_arg("force", False)
self.force_build: bool = get_arg("force_build", False)
self.generic: Optional[list[str]] = get_list_arg("generic", str)
self.input: bool = get_arg("input", False)
self.interaction: bool = get_arg("interaction", False)
self.interactive: bool = get_arg("interactive", False)
self.jobs: int = get_arg("jobs", (os.cpu_count() or 1) // 2, ">= 0")
self.invalid: bool = get_arg("invalid", False)
self.kattis: bool = get_arg("kattis", False)
self.lang: Optional[list[str]] = get_list_arg("lang", str)
self.latest_bt: bool = get_arg("latest_bt", False)
self.legacy: bool = get_arg("legacy", False)
self.memory: Optional[int] = get_optional_arg("legacy", int, "> 0")

more: Optional[bool] = get_optional_arg("more", bool)
if more is not None:
self.all = int(more)
self._set.add("all")
self._set.discard("more")
warn("--more is deprecated, use --all instead!\n")

self.move_to: Optional[str] = get_optional_arg("colors", str)
self.no_bar: bool = get_arg("no_bar", False)
self.no_generate: bool = get_arg("no_generate", False)
self.no_solution: bool = get_arg("no_solution", False)
self.no_solutions: bool = get_arg("no_solutions", False)
self.no_testcase_sanity_checks: bool = get_arg("no_testcase_sanity_checks", False)
self.no_time_limit: bool = get_arg("no_time_limit", False)
self.no_validators: bool = get_arg("no_validators", False)
self.no_visualizer: bool = get_arg("no_visualizer", True, ">= 0")
self.number: Optional[str] = get_optional_arg("number", str)
self.open: Optional[Literal[True] | Path] = get_optional_arg("open", Path)
self.order: Optional[str] = get_optional_arg("order", str)
self.order_from_ccs: Optional[str] = get_optional_arg("order_from_ccs", str)
self.overview: bool = get_arg("overview", False)
self.password: Optional[str] = get_optional_arg("password", str)
self.post_freeze: bool = get_arg("post_freeze", False)
self.problem: Optional[Path] = get_optional_arg("problem", Path)
self.problemname: Optional[str] = get_optional_arg("problemname", str)
self.remove: bool = get_arg("remove", False)
self.reorder: bool = get_arg("reorder", False)
self.samples: bool = get_arg("samples", False)
self.sanitizer: bool = get_arg("sanitizer", False)
self.skel: Optional[str] = get_optional_arg("skel", str)
self.skip: bool = get_arg("skip", False)
self.sort: bool = get_arg("sort", False)
self.submissions: Optional[list[Path]] = get_list_arg("submissions", Path)
self.table: bool = get_arg("table", False)
self.testcases: Optional[list[Path]] = get_list_arg("testcases", Path)
self.time: int = get_arg("time", 600, "> 0")
self.time_limit: Optional[float] = get_optional_arg("time_limit", float, "> 0")
self.timeout: Optional[int] = get_optional_arg("timeout", int, "> 0")
self.token: Optional[str] = get_optional_arg("token", str)
self.tree: bool = get_arg("tree", False)
self.type: Optional[str] = get_optional_arg("type", str)
self.username: Optional[str] = get_optional_arg("username", str)
self.valid_output: bool = get_arg("valid_output", False)
self.verbose: int = get_arg("verbose", 0, ">= 0")
self.watch: bool = get_arg("watch", False)
self.web: bool = get_arg("web", False)
self.write: bool = get_arg("write", False)

for key in kwargs:
print(key, type(kwargs[key]))
warn(f"unknown key in {source}: '{key}'")

def update(self, args: "ARGS", replace: bool = False) -> None:
for key in args._set:
if key not in self._set or replace:
setattr(self, key, getattr(args, key))
self._set.add(key)

def mark_set(self, *keys: str) -> None:
self._set.update(list(keys))

def copy(self) -> "ARGS":
res = copy.copy(self)
res._set = copy.copy(res._set)
return res


args = ARGS("config.py")
Loading
Loading