Skip to content

Commit 58e692e

Browse files
authored
Merge pull request #431 from RagnarGrootKoerkamp/constants-final
Implement constants
2 parents 16dd811 + 18bdc6c commit 58e692e

36 files changed

+429
-139
lines changed

bin/config.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,20 @@
3434
# When --table is set, this threshold determines the number of identical profiles needed to get flagged.
3535
TABLE_THRESHOLD: Final[int] = 4
3636

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

40+
CONSTANT_NAME_REGEX = "[a-zA-Z_][a-zA-Z0-9_]*"
41+
COMPILED_CONSTANT_NAME_REGEX: Final[re.Pattern[str]] = re.compile(CONSTANT_NAME_REGEX)
42+
CONSTANT_SUBSTITUTE_REGEX: Final[re.Pattern[str]] = re.compile(
43+
f"\\{{\\{{({CONSTANT_NAME_REGEX})\\}}\\}}"
44+
)
45+
46+
BAPCTOOLS_SUBSTITUTE_REGEX: Final[re.Pattern[str]] = re.compile(
47+
f"\\{{%({CONSTANT_NAME_REGEX})%\\}}"
48+
)
49+
50+
4051
KNOWN_TESTCASE_EXTENSIONS: Final[Sequence[str]] = [
4152
".in",
4253
".ans",

bin/export.py

+98-101
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,6 @@
1212
from problem import Problem
1313

1414

15-
# Replace \problemname{...} by the value of `name:` in problems.yaml in all .tex files.
16-
# This is needed because Kattis is currently still running the legacy version of the problem spec,
17-
# rather than 2023-07-draft.
18-
def fix_problem_name_cmd(problem):
19-
reverts = []
20-
for f in (problem.path / "problem_statement").iterdir():
21-
if f.is_file() and f.suffix == ".tex" and len(f.suffixes) >= 2:
22-
lang = f.suffixes[-2][1:]
23-
t = f.read_text()
24-
match = re.search(r"\\problemname\{\s*(\\problemyamlname)?\s*\}", t)
25-
if match:
26-
if lang in problem.settings.name:
27-
reverts.append((f, t))
28-
t = t.replace(match[0], r"\problemname{" + problem.settings.name[lang] + "}")
29-
f.write_text(t)
30-
else:
31-
util.error(f"{f}: no name set for language {lang}.")
32-
33-
def revert():
34-
for f, t in reverts:
35-
f.write_text(t)
36-
37-
return revert
38-
39-
4015
def force_single_language(problems):
4116
if config.args.languages and len(config.args.languages) == 1:
4217
statement_language = config.args.languages[0]
@@ -115,18 +90,13 @@ def build_samples_zip(problems, output, statement_language):
11590

11691

11792
def build_problem_zip(problem: Problem, output: Path):
118-
"""Make DOMjudge ZIP file for specified problem."""
93+
"""Make DOMjudge/Kattis ZIP file for specified problem."""
11994

12095
# Add problem PDF for only one language to the zip file (note that Kattis export does not include PDF)
12196
statement_language = None if config.args.kattis else force_single_language([problem])
12297

123-
deprecated = [ # may be removed at some point.
124-
"domjudge-problem.ini",
125-
]
126-
127-
write_file_strs: list[tuple[str, str]] = []
128-
12998
files = [
99+
("problem.yaml", True),
130100
("problem_statement/*", True),
131101
("submissions/accepted/**/*", True),
132102
("submissions/*/**/*", False),
@@ -156,56 +126,21 @@ def build_problem_zip(problem: Problem, output: Path):
156126
if config.args.kattis:
157127
files.append(("input_validators/**/*", True))
158128

159-
print("Preparing to make ZIP file for problem dir %s" % problem.path, file=sys.stderr)
160-
161-
# DOMjudge does not support 'type' in problem.yaml nor 'output_validator_args' in testdata.yaml yet.
162-
# TODO: Remove this once it does.
163-
problem_yaml_str = (problem.path / "problem.yaml").read_text()
164-
if not config.args.kattis:
165-
validator_flags = " ".join(
166-
problem.get_testdata_yaml(
167-
problem.path / "data",
168-
"output_validator_args",
169-
PrintBar("Getting validator_flags for legacy DOMjudge export"),
170-
)
171-
)
172-
if validator_flags:
173-
validator_flags = "validator_flags: " + validator_flags + "\n"
174-
write_file_strs.append(
175-
(
176-
"problem.yaml",
177-
f"""{problem_yaml_str}\nvalidation: {
178-
"custom interactive"
179-
if problem.interactive
180-
else "custom multi-pass"
181-
if problem.multi_pass
182-
else "custom"
183-
if problem.custom_output
184-
else "default"
185-
}\n{validator_flags}""",
186-
)
187-
)
188-
else:
189-
write_file_strs.append(("problem.yaml", problem_yaml_str))
190-
191-
# DOMjudge does not support 'limits.time_limit' in problem.yaml yet.
192-
# TODO: Remove this once it does.
193-
if not config.args.kattis:
194-
write_file_strs.append((".timelimit", str(problem.limits.time_limit)))
129+
message("preparing zip file content", "Zip", problem.path, color_type=MessageType.LOG)
195130

196-
# Warn for all deprecated files but still add them to the files list
197-
for pattern in deprecated:
198-
files.append((pattern, False))
199-
# Only include hidden files if the pattern starts with a '.'.
200-
paths = list(util.glob(problem.path, pattern, include_hidden=pattern[0] == "."))
201-
if len(paths) > 0:
202-
addition = ""
203-
if len(paths) > 1:
204-
addition = f" and {len(paths) - 1} more"
205-
util.warn(f'Found deprecated file "{paths[0]}"{addition}.')
131+
# prepare files inside dir
132+
export_dir = problem.tmpdir / "export"
133+
if export_dir.exists():
134+
shutil.rmtree(export_dir)
135+
# For Kattis, prepend the problem shortname to all files.
136+
if config.args.kattis:
137+
export_dir /= problem.name
138+
export_dir.mkdir(parents=True, exist_ok=True)
206139

207-
# Build list of files to store in ZIP file.
208-
copyfiles = set()
140+
def add_file(path, source):
141+
path = export_dir / path
142+
path.parent.mkdir(parents=True, exist_ok=True)
143+
ensure_symlink(path, source)
209144

210145
# Include all files beside testcases
211146
for pattern, required in files:
@@ -214,22 +149,17 @@ def build_problem_zip(problem: Problem, output: Path):
214149
if required and len(paths) == 0:
215150
util.error(f"No matches for required path {pattern}.")
216151
for f in paths:
217-
# NOTE: Directories are skipped because ZIP only supports files.
218152
if f.is_file():
219153
out = f.relative_to(problem.path)
220154
out = remove_language_suffix(out, statement_language)
221-
# For Kattis, prepend the problem shortname to all files.
222-
if config.args.kattis:
223-
out = problem.name / out
224-
copyfiles.add((f, out))
155+
add_file(out, f)
225156

226157
# Include all testcases (specified by a .in file) and copy all related files
227158
for pattern, required in testcases:
228159
paths = list(util.glob(problem.path, pattern))
229160
if required and len(paths) == 0:
230161
util.error(f"No matches for required path {pattern}.")
231162
for f in paths:
232-
# NOTE: Directories are skipped because ZIP only supports files.
233163
if f.is_file():
234164
if not f.with_suffix(".ans").is_file():
235165
util.warn(f"No answer file found for {f}, skipping.")
@@ -238,31 +168,98 @@ def build_problem_zip(problem: Problem, output: Path):
238168
f2 = f.with_suffix(ext)
239169
if f2.is_file():
240170
out = f2.relative_to(problem.path)
241-
# For Kattis, prepend the problem shortname to all files.
242-
if config.args.kattis:
243-
out = problem.name / out
244-
copyfiles.add((f2, out))
171+
add_file(out, f2)
245172

246-
# Build .ZIP file.
247-
print("writing ZIP file:", output, file=sys.stderr)
173+
# DOMjudge does not support 'type' in problem.yaml nor 'output_validator_args' in testdata.yaml yet.
174+
# TODO: Remove this once it does.
175+
if not config.args.kattis:
176+
yaml_path = export_dir / "problem.yaml"
177+
yaml_data = [yaml_path.read_text(), "\nvalidation:"]
178+
if problem.custom_output:
179+
yaml_data.append(" custom")
180+
if problem.interactive:
181+
yaml_data.append(" interactive")
182+
if problem.multi_pass:
183+
yaml_data.append(" multi-pass")
184+
else:
185+
yaml_data.append(" default")
186+
yaml_data.append("\n")
187+
188+
validator_flags = " ".join(
189+
problem.get_testdata_yaml(
190+
problem.path / "data",
191+
"output_validator_args",
192+
PrintBar("Getting validator_flags for legacy DOMjudge export"),
193+
)
194+
)
195+
if validator_flags:
196+
yaml_data.append(f"validator_flags: {validator_flags}\n")
197+
198+
yaml_path.unlink()
199+
yaml_path.write_text("".join(yaml_data))
200+
201+
# DOMjudge does not support 'limits.time_limit' in problem.yaml yet.
202+
# TODO: Remove this once it does.
203+
if not config.args.kattis:
204+
(export_dir / ".timelimit").write_text(str(problem.limits.time_limit))
248205

249-
revert_problem_name_cmd = fix_problem_name_cmd(problem)
206+
# Replace \problemname{...} by the value of `name:` in problems.yaml in all .tex files.
207+
# This is needed because Kattis is currently still running the legacy version of the problem spec,
208+
# rather than 2023-07-draft.
209+
for f in (export_dir / "problem_statement").iterdir():
210+
if f.is_file() and f.suffix == ".tex" and len(f.suffixes) >= 2:
211+
lang = f.suffixes[-2][1:]
212+
t = f.read_text()
213+
match = re.search(r"\\problemname\{\s*(\\problemyamlname)?\s*\}", t)
214+
if match:
215+
if lang in problem.settings.name:
216+
t = t.replace(match[0], r"\problemname{" + problem.settings.name[lang] + "}")
217+
f.unlink()
218+
f.write_text(t)
219+
else:
220+
util.error(f"{f}: no name set for language {lang}.")
221+
222+
# DOMjudge does not support constants.
223+
# TODO: Remove this if it ever does.
224+
if problem.settings.constants:
225+
constants_supported = [
226+
"data/**/testdata.yaml",
227+
"output_validators/**/*",
228+
"input_validators/**/*",
229+
# "problem_statement/*", uses \constants
230+
# "submissions/*/**/*", removed support?
231+
]
232+
for pattern in constants_supported:
233+
for f in export_dir.glob(pattern):
234+
if f.is_file() and util.has_substitute(f, config.CONSTANT_SUBSTITUTE_REGEX):
235+
text = f.read_text()
236+
text = util.substitute(
237+
text,
238+
problem.settings.constants,
239+
pattern=config.CONSTANT_SUBSTITUTE_REGEX,
240+
bar=util.PrintBar("Zip"),
241+
)
242+
f.unlink()
243+
f.write_text(text)
250244

245+
# Build .ZIP file.
246+
message("writing zip file", "Zip", output, color_type=MessageType.LOG)
251247
try:
252248
zf = zipfile.ZipFile(output, mode="w", compression=zipfile.ZIP_DEFLATED, allowZip64=False)
253249

254-
for source, target in sorted(copyfiles):
255-
zf.write(source, target, compress_type=zipfile.ZIP_DEFLATED)
256-
for target_file, content in sorted(write_file_strs):
257-
zf.writestr(target_file, content, compress_type=zipfile.ZIP_DEFLATED)
250+
export_dir = problem.tmpdir / "export"
251+
for f in sorted(export_dir.rglob("*")):
252+
# NOTE: Directories are skipped because ZIP only supports files.
253+
if f.is_file():
254+
name = f.relative_to(export_dir)
255+
zf.write(f, name, compress_type=zipfile.ZIP_DEFLATED)
258256

259257
# Done.
260258
zf.close()
261-
print("done", file=sys.stderr)
259+
message("done", "Zip", color_type=MessageType.LOG)
262260
print(file=sys.stderr)
263-
264-
finally:
265-
revert_problem_name_cmd()
261+
except Exception:
262+
return False
266263

267264
return True
268265

bin/generate.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ def default_solution_path(generator_config):
268268
if config.args.default_solution:
269269
if generator_config.has_yaml:
270270
message(
271-
f"""--default-solution Ignored. Set the default solution in the generator.yaml!
271+
f"""--default-solution Ignored. Set the default solution in the generators.yaml!
272272
solution: /{config.args.default_solution}""",
273273
"generators.yaml",
274274
color_type=MessageType.WARN,
@@ -297,7 +297,7 @@ def default_solution_path(generator_config):
297297
raw = f"solution: /{solution.relative_to(problem.path)}\n" + raw
298298
yaml_path.write_text(raw)
299299
message(
300-
f"No solution specified. {solution_short_path} added as default solution in the generator.yaml",
300+
f"No solution specified. {solution_short_path} added as default solution in the generators.yaml",
301301
"generators.yaml",
302302
color_type=MessageType.LOG,
303303
)
@@ -522,8 +522,15 @@ def __init__(self, problem, generator_config, key, name: str, yaml, parent, coun
522522
if len(yaml["generate"]) == 0:
523523
raise ParseException("`generate` must not be empty.")
524524

525-
# replace count
525+
# first replace {{constants}}
526526
command_string = yaml["generate"]
527+
command_string = substitute(
528+
command_string,
529+
problem.settings.constants,
530+
pattern=config.CONSTANT_SUBSTITUTE_REGEX,
531+
)
532+
533+
# then replace {count} and {seed}
527534
if "{count}" in command_string:
528535
if "count" in yaml:
529536
command_string = command_string.replace(
@@ -907,6 +914,7 @@ def generate_from_rule():
907914
bar.error(f"Hardcoded {ext} data must not be empty!")
908915
return False
909916
else:
917+
# substitute in contents? -> No!
910918
infile.with_suffix(ext).write_text(contents)
911919

912920
# Step 4: Error if infile was not generated.

bin/latex.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,20 @@ def flush():
164164
samples_file_path.write_text("".join(samples_data))
165165

166166

167+
def create_constants_file(problem: "problem.Problem", language: str) -> None:
168+
constant_data: list[str] = []
169+
for key, item in problem.settings.constants.items():
170+
constant_data.append(f"\\expandafter\\def\\csname constants_{key}\\endcsname{{{item}}}\n")
171+
172+
builddir = latex_builddir(problem, language)
173+
constants_file_path = builddir / "constants.tex"
174+
constants_file_path.write_text("".join(constant_data))
175+
176+
167177
# Steps needed for both problem and contest compilation.
168178
def prepare_problem(problem: "problem.Problem", language: str):
169179
create_samples_file(problem, language)
180+
create_constants_file(problem, language)
170181

171182

172183
def get_tl(problem: "problem.Problem"):
@@ -350,7 +361,7 @@ def run_latexmk(stdout, stderr):
350361

351362
# 1. Copy the latex/problem.tex file to tmpdir/<problem>/latex/<language>/problem.tex,
352363
# substituting variables.
353-
# 2. Create tmpdir/<problem>/latex/<language>/samples.tex.
364+
# 2. Create tmpdir/<problem>/latex/<language>/{samples,constants}.tex.
354365
# 3. Run latexmk and link the resulting <build_type>.<language>.pdf into the problem directory.
355366
def build_problem_pdf(
356367
problem: "problem.Problem", language: str, build_type=PdfType.PROBLEM, web=False
@@ -374,6 +385,7 @@ def build_problem_pdf(
374385
local_data if local_data.is_file() else config.TOOLS_ROOT / "latex" / main_file,
375386
builddir / main_file,
376387
problem_data(problem, language),
388+
bar=bar,
377389
)
378390

379391
return build_latex_pdf(builddir, builddir / main_file, language, bar, problem.path)
@@ -465,6 +477,7 @@ def build_contest_pdf(
465477
),
466478
builddir / "contest_data.tex",
467479
config_data,
480+
bar=bar,
468481
)
469482

470483
problems_data = ""
@@ -489,6 +502,7 @@ def build_contest_pdf(
489502
if build_type == PdfType.PROBLEM:
490503
prepare_problem(prob, language)
491504
else: # i.e. for SOLUTION and PROBLEM_SLIDE
505+
create_constants_file(prob, language)
492506
tex_no_lang = prob.path / "problem_statement" / f"{build_type.value}.tex"
493507
tex_with_lang = prob.path / "problem_statement" / f"{build_type.value}.{language}.tex"
494508
if tex_with_lang.is_file():
@@ -507,6 +521,7 @@ def build_contest_pdf(
507521
problems_data += substitute(
508522
per_problem_data_tex,
509523
problem_data(prob, language),
524+
bar=bar,
510525
)
511526

512527
if solutions:

0 commit comments

Comments
 (0)