Skip to content

Draft #433

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

Draft
wants to merge 24 commits into
base: master
Choose a base branch
from
Draft

Draft #433

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
35 changes: 27 additions & 8 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,7 +86,6 @@
"invalid_input",
"invalid_answer",
"invalid_output",
"bad",
]


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