Skip to content

Absolute2 #442

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 25 commits into
base: draft
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8c77822
Implement constants (#431)
mzuenni Feb 26, 2025
15968b9
Add bt upgrade (#432)
mzuenni Mar 3, 2025
824fc5b
Split up `problem_statement/` into `statement/`, `solution/`, and `pr…
mzuenni Mar 6, 2025
3198d61
Output validator (#435)
mzuenni Mar 9, 2025
d0c3d80
[problem] Update parsing of problem.yaml based on Kattis/problem-pack…
mpsijm Mar 16, 2025
027ff31
Add .download samples and ans_is_output flag (#436)
mzuenni Mar 23, 2025
75d5264
fix yaml handling
mzuenni Mar 24, 2025
9f3bd66
readd empty test
mzuenni Mar 25, 2025
825c8cd
[validate] Remove {input,answer}_format_validators (#443)
mzuenni Mar 30, 2025
b721537
Legacy export (#441)
mzuenni Mar 30, 2025
a0736af
Use Validator.source_dir at all places (#444)
mzuenni Mar 30, 2025
5030bc0
use more absolute
mzuenni Mar 29, 2025
f5ba217
use more absolute
mzuenni Mar 29, 2025
208e7f4
change order
mzuenni Mar 30, 2025
a560f79
Drop support for `data/bad` (#445)
mzuenni Mar 31, 2025
1044c30
fix a lot of typing issues
mzuenni Mar 31, 2025
6da3bc5
more typing
mzuenni Mar 31, 2025
2a05dc7
Merge branch 'draft' into absolute2
mzuenni Mar 31, 2025
467e2bd
use config.SPEC_VERSION
mzuenni Mar 31, 2025
a6eff1b
no need to select language
mzuenni Mar 31, 2025
f4e23e2
Order in contest yaml (#446)
mzuenni Apr 2, 2025
9091a8d
change typing
mzuenni Apr 2, 2025
dd7cb2d
Add secondary sort-keys to `--order-from-ccs` (#447)
mzuenni Apr 5, 2025
baae8d8
Draft visualizer (#448)
mzuenni Apr 24, 2025
98db142
Merge branch 'draft' into absolute2
mzuenni Apr 24, 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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,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: 29 additions & 10 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,13 +61,19 @@
".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',
]

Expand All @@ -67,17 +86,16 @@
"invalid_input",
"invalid_answer",
"invalid_output",
"bad",
]


SEED_DEPENDENCY_RETRIES: Final[int] = 10

# The root directory of the BAPCtools repository.
TOOLS_ROOT: Final[Path] = Path(__file__).resolve().parent.parent
TOOLS_ROOT: Final[Path] = Path(__file__).absolute().parent.parent

# The directory from which BAPCtools is invoked.
current_working_directory: Final[Path] = Path.cwd().resolve()
current_working_directory: Final[Path] = Path.cwd().absolute()

# Add third_party/ to the $PATH for checktestdata.
os.environ["PATH"] += os.pathsep + str(TOOLS_ROOT / "third_party")
Expand All @@ -86,11 +104,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 +120,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", "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