Skip to content

Commit 3a3b480

Browse files
mzuennimpsijm
andcommitted
Split up problem_statement/ into statement/, solution/, and problem_slide/ (#434)
* use new paths * update latex template files * update skel * ran bt upgrade * remove stem call * implemented suggestions * export more stuff * Rewrite problem_statement to statement/solution/problem_slide in documentation * [export] Move files in solution/ or problem_slide/ to problem_statement/, not statement/ * use pdfType everywhere * dont create empty keys * return empty list on error * update glob patterns --------- Co-authored-by: Maarten Sijm <[email protected]>
1 parent e5d9634 commit 3a3b480

File tree

49 files changed

+141
-108
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+141
-108
lines changed

bin/constraints.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import re
22
from collections import defaultdict
33

4+
import latex
45
import validate
56
from colorama import Fore, Style
67

@@ -45,7 +46,7 @@ def f(cs):
4546

4647

4748
def check_statement(problem, language):
48-
statement_file = problem.path / f"problem_statement/problem.{language}.tex"
49+
statement_file = problem.path / latex.PdfType.PROBLEM.path(language)
4950
statement = statement_file.read_text()
5051

5152
statement_values = set()
@@ -170,7 +171,7 @@ def parse_command():
170171
# 3) if a section starts parse that (and ensure that no environment is active)
171172
# 4) if an environment begins parse that (and ensure that no other environment is active)
172173
# 5) if a new define starts parse that
173-
# 6) if inline math starts in an input/ouput part parse it as constraint
174+
# 6) if inline math starts in an input/output part parse it as constraint
174175
while pos < len(statement):
175176
if statement[pos] == "%":
176177
next = statement.find("\n", pos)

bin/export.py

+20-3
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def build_problem_zip(problem: Problem, output: Path):
9797

9898
files = [
9999
("problem.yaml", True),
100-
("problem_statement/*", True),
100+
("statement/*", True),
101101
("submissions/accepted/**/*", True),
102102
("submissions/*/**/*", False),
103103
("attachments/**/*", problem.interactive or problem.multi_pass),
@@ -206,7 +206,7 @@ def add_file(path, source):
206206
# Replace \problemname{...} by the value of `name:` in problems.yaml in all .tex files.
207207
# This is needed because Kattis is currently still running the legacy version of the problem spec,
208208
# rather than 2023-07-draft.
209-
for f in (export_dir / "problem_statement").iterdir():
209+
for f in (export_dir / "statement").iterdir():
210210
if f.is_file() and f.suffix == ".tex" and len(f.suffixes) >= 2:
211211
lang = f.suffixes[-2][1:]
212212
t = f.read_text()
@@ -226,7 +226,7 @@ def add_file(path, source):
226226
"data/**/testdata.yaml",
227227
"output_validators/**/*",
228228
"input_validators/**/*",
229-
# "problem_statement/*", uses \constants
229+
# "statement/*", uses \constants
230230
# "submissions/*/**/*", removed support?
231231
]
232232
for pattern in constants_supported:
@@ -242,6 +242,23 @@ def add_file(path, source):
242242
f.unlink()
243243
f.write_text(text)
244244

245+
# TODO: Remove this if we know others import the statement folder
246+
if (export_dir / "statement").exists():
247+
(export_dir / "statement").rename(export_dir / "problem_statement")
248+
for d in ["solution", "problem_slide"]:
249+
for f in list(util.glob(problem.path, f"{d}/*")):
250+
if f.is_file():
251+
out = Path("problem_statement") / f.relative_to(problem.path / d)
252+
if out.exists():
253+
message(
254+
f"Can not export {f.relative_to(problem.path)} as {out}",
255+
"Zip",
256+
output,
257+
color_type=MessageType.WARN,
258+
)
259+
else:
260+
add_file(out, f)
261+
245262
# Build .ZIP file.
246263
message("writing zip file", "Zip", output, color_type=MessageType.LOG)
247264
try:

bin/latex.py

+32-29
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@
66
import sys
77
from enum import Enum
88
from pathlib import Path
9-
from typing import Optional
9+
from typing import Optional, TYPE_CHECKING
1010

1111
from colorama import Fore, Style
1212

1313
import config
1414
from contest import contest_yaml, problems_yaml
15-
import problem
1615
from util import (
1716
copy_and_substitute,
1817
ensure_symlink,
@@ -26,20 +25,27 @@
2625
warn,
2726
)
2827

28+
if TYPE_CHECKING: # Prevent circular import: https://stackoverflow.com/a/39757388
29+
from problem import Problem
2930

30-
class PdfType(str, Enum):
31-
PROBLEM = "problem"
32-
PROBLEM_SLIDE = "problem-slide"
33-
SOLUTION = "solution"
3431

32+
class PdfType(Enum):
33+
PROBLEM = Path("statement") / "problem"
34+
PROBLEM_SLIDE = Path("problem_slide") / "problem-slide"
35+
SOLUTION = Path("solution") / "solution"
3536

36-
def latex_builddir(problem: "problem.Problem", language: str) -> Path:
37+
def path(self, lang: Optional[str] = None, ext: str = ".tex") -> Path:
38+
lang = f".{lang}" if lang is not None else ""
39+
return self.value.with_name(f"{self.value.name}{lang}{ext}")
40+
41+
42+
def latex_builddir(problem: "Problem", language: str) -> Path:
3743
builddir = problem.tmpdir / "latex" / language
3844
builddir.mkdir(parents=True, exist_ok=True)
3945
return builddir
4046

4147

42-
def create_samples_file(problem: "problem.Problem", language: str) -> None:
48+
def create_samples_file(problem: "Problem", language: str) -> None:
4349
builddir = latex_builddir(problem, language)
4450

4551
# create the samples.tex file
@@ -164,7 +170,7 @@ def flush():
164170
samples_file_path.write_text("".join(samples_data))
165171

166172

167-
def create_constants_file(problem: "problem.Problem", language: str) -> None:
173+
def create_constants_file(problem: "Problem", language: str) -> None:
168174
constant_data: list[str] = []
169175
for key, item in problem.settings.constants.items():
170176
constant_data.append(f"\\expandafter\\def\\csname constants_{key}\\endcsname{{{item}}}\n")
@@ -175,12 +181,12 @@ def create_constants_file(problem: "problem.Problem", language: str) -> None:
175181

176182

177183
# Steps needed for both problem and contest compilation.
178-
def prepare_problem(problem: "problem.Problem", language: str):
184+
def prepare_problem(problem: "Problem", language: str):
179185
create_samples_file(problem, language)
180186
create_constants_file(problem, language)
181187

182188

183-
def get_tl(problem: "problem.Problem"):
189+
def get_tl(problem: "Problem"):
184190
tl = problem.limits.time_limit
185191
tl = int(tl) if abs(tl - int(tl)) < 0.0001 else tl
186192

@@ -194,7 +200,7 @@ def get_tl(problem: "problem.Problem"):
194200
return tl if print_tl else ""
195201

196202

197-
def problem_data(problem: "problem.Problem", language: str):
203+
def problem_data(problem: "Problem", language: str):
198204
background = next(
199205
(
200206
p["rgb"][1:]
@@ -363,17 +369,14 @@ def run_latexmk(stdout, stderr):
363369
# substituting variables.
364370
# 2. Create tmpdir/<problem>/latex/<language>/{samples,constants}.tex.
365371
# 3. Run latexmk and link the resulting <build_type>.<language>.pdf into the problem directory.
366-
def build_problem_pdf(
367-
problem: "problem.Problem", language: str, build_type=PdfType.PROBLEM, web=False
368-
):
372+
def build_problem_pdf(problem: "Problem", language: str, build_type=PdfType.PROBLEM, web=False):
369373
"""
370374
Arguments:
371375
-- language: str, the two-letter language code appearing the file name, such as problem.en.tex
372376
"""
373-
main_file = build_type.value
374-
main_file += "-web.tex" if web else ".tex"
377+
main_file = build_type.path(ext="-web.tex" if web else ".tex").name
375378

376-
bar = PrintBar(f"{main_file[:-3]}{language}.pdf")
379+
bar = PrintBar(f"{main_file[:-4]}.{language}.pdf")
377380
bar.log(f"Building PDF for language {language}")
378381

379382
prepare_problem(problem, language)
@@ -391,7 +394,7 @@ def build_problem_pdf(
391394
return build_latex_pdf(builddir, builddir / main_file, language, bar, problem.path)
392395

393396

394-
def build_problem_pdfs(problem: "problem.Problem", build_type=PdfType.PROBLEM, web=False):
397+
def build_problem_pdfs(problem: "Problem", build_type=PdfType.PROBLEM, web=False):
395398
"""Build PDFs for various languages. If list of languages is specified,
396399
(either via config files or --language arguments), build those. Otherwise
397400
build all languages for which there is a statement latex source.
@@ -411,11 +414,11 @@ def build_problem_pdfs(problem: "problem.Problem", build_type=PdfType.PROBLEM, w
411414
if build_type != PdfType.PROBLEM:
412415
filtered_languages = []
413416
for lang in languages:
414-
if (problem.path / "problem_statement" / f"{build_type.value}.{lang}.tex").exists():
417+
if (problem.path / build_type.path(lang)).exists():
415418
filtered_languages.append(lang)
416419
else:
417420
message(
418-
f"{build_type.value}.{lang}.tex not found",
421+
f"{build_type.path(lang)} not found",
419422
problem.name,
420423
color_type=MessageType.WARN,
421424
)
@@ -436,7 +439,7 @@ def find_logo() -> Path:
436439

437440
def build_contest_pdf(
438441
contest: str,
439-
problems: list["problem.Problem"],
442+
problems: list["Problem"],
440443
tmpdir: Path,
441444
language: str,
442445
build_type=PdfType.PROBLEM,
@@ -491,31 +494,31 @@ def build_contest_pdf(
491494
elif headertex.exists():
492495
problems_data += f"\\input{{{headertex}}}\n"
493496

494-
local_per_problem_data = Path(f"contest-{build_type.value}.tex")
497+
local_per_problem_data = Path(f"contest-{build_type.path().name}")
495498
per_problem_data_tex = (
496499
local_per_problem_data
497500
if local_per_problem_data.is_file()
498-
else config.TOOLS_ROOT / "latex" / f"contest-{build_type.value}.tex"
501+
else config.TOOLS_ROOT / "latex" / local_per_problem_data.name
499502
).read_text()
500503

501504
for prob in problems:
502505
if build_type == PdfType.PROBLEM:
503506
prepare_problem(prob, language)
504507
else: # i.e. for SOLUTION and PROBLEM_SLIDE
505508
create_constants_file(prob, language)
506-
tex_no_lang = prob.path / "problem_statement" / f"{build_type.value}.tex"
507-
tex_with_lang = prob.path / "problem_statement" / f"{build_type.value}.{language}.tex"
509+
tex_no_lang = prob.path / build_type.path()
510+
tex_with_lang = prob.path / build_type.path(language)
508511
if tex_with_lang.is_file():
509512
# All is good
510513
pass
511514
elif tex_no_lang.is_file():
512515
bar.warn(
513-
f"Rename {build_type.value}.tex to {build_type.value}.{language}.tex",
516+
f"Rename {tex_no_lang.name} to {tex_with_lang.name}",
514517
prob.name,
515518
)
516519
continue
517520
else:
518-
bar.warn(f"{build_type.value}.{language}.tex not found", prob.name)
521+
bar.warn(f"{tex_with_lang.name} not found", prob.name)
519522
continue
520523

521524
problems_data += substitute(
@@ -533,7 +536,7 @@ def build_contest_pdf(
533536
elif footertex.exists():
534537
problems_data += f"\\input{{{footertex}}}\n"
535538

536-
(builddir / f"contest-{build_type.value}s.tex").write_text(problems_data)
539+
(builddir / f"contest-{build_type.path(ext='s.tex').name}").write_text(problems_data)
537540

538541
return build_latex_pdf(builddir, Path(main_file), language, bar)
539542

bin/problem.py

+11-8
Original file line numberDiff line numberDiff line change
@@ -334,30 +334,31 @@ def _determine_statement_languages(self):
334334
"""
335335
yamllangs = set(self.settings.name)
336336
texlangs = set(
337-
path.suffixes[0][1:] for path in glob(self.path, "problem_statement/problem.*.tex")
337+
path.suffixes[0][1:] for path in glob(self.path, str(latex.PdfType.PROBLEM.path("*")))
338338
)
339339
for lang in texlangs - yamllangs:
340340
error(
341-
f"{self.name}: Found problem.{lang}.tex, but no corresponding name in problem.yaml."
341+
f"{self.name}: Found {latex.PdfType.PROBLEM.path(lang).name}, but no corresponding name in problem.yaml."
342342
)
343343
for lang in yamllangs - texlangs:
344344
error(
345-
f"{self.name}: Found name for language {lang} in problem.yaml, but not problem_statement/problem.{lang}.tex."
345+
f"{self.name}: Found name for language {lang} in problem.yaml, but not {latex.PdfType.PROBLEM.path(lang)}."
346346
)
347347
# Check that names in problem.yaml and \problemname{} in problem.*.tex agree:
348348
for lang in texlangs & yamllangs:
349349
unnormalised_yamlname = self.settings.name[lang]
350350
yamlname = " ".join(unnormalised_yamlname.split())
351-
with open(self.path / "problem_statement" / f"problem.{lang}.tex") as texfile:
351+
texpath = self.path / latex.PdfType.PROBLEM.path(lang)
352+
with open(texpath) as texfile:
352353
match texname := latex.get_argument_for_command(texfile, "problemname"):
353354
case None:
354-
error(rf"No \problemname found in problem.{lang}.tex")
355+
error(rf"No \problemname found in {texpath.name}")
355356
continue
356357
case "":
357358
continue
358359
case r"\problemyamlname":
359360
warn(
360-
rf"Prefer using \problemname{{}} instead of \problemname{{\problemyamlname}} in problem.{lang}.tex"
361+
rf"Prefer using \problemname{{}} instead of \problemname{{\problemyamlname}} in {texpath.name}"
361362
)
362363
continue
363364
case s if "\\" in s or "_" in s or "^" in s:
@@ -366,7 +367,7 @@ def _determine_statement_languages(self):
366367
continue
367368
case s if s != yamlname:
368369
warn(
369-
f"Problem titles in problem.{lang}.tex ({texname})"
370+
f"Problem titles in {texpath.name} ({texname})"
370371
+ f" and problem.yaml ({yamlname}) differ;"
371372
+ r" consider using \problemname{}."
372373
)
@@ -514,12 +515,14 @@ def get_testdata_yaml(
514515
if key in flags:
515516
if key == "output_validator_args":
516517
if not isinstance(flags[key], str):
517-
bar.error("ouput_validator_args must be string")
518+
bar.error("output_validator_args must be string")
519+
return []
518520
return flags[key].split()
519521

520522
if key == "input_validator_args":
521523
if not isinstance(flags[key], (str, dict)):
522524
bar.error("input_validator_args must be string or map")
525+
return []
523526
if isinstance(flags[key], str):
524527
return flags[key].split()
525528
elif name in flags[key]:

bin/skel.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
# Local imports
66
import config
7+
import latex
78
from export import force_single_language
89
from problem import Problem
910
from util import *
@@ -254,10 +255,11 @@ def new_problem():
254255

255256
# Warn about missing problem statement skeletons for non-en languages
256257
for lang in statement_languages:
257-
filename = f"problem.{lang}.tex"
258-
statement_path = target_dir / dirname / "problem_statement" / filename
258+
statement_path = target_dir / dirname / latex.PdfType.PROBLEM.path(lang)
259259
if not statement_path.is_file():
260-
warn(f"No skeleton for {filename} found. Create it manually or update skel/problem.")
260+
warn(
261+
f"No skeleton for {statement_path.name} found. Create it manually or update skel/problem."
262+
)
261263

262264

263265
def rename_problem(problem):
@@ -344,8 +346,9 @@ def problem_source_dir(problem: Problem):
344346
contest_yml = (config.TOOLS_ROOT / "skel/gitlab_ci/contest.yaml").read_text()
345347
contest_path = Path(".").resolve().relative_to(git_root_path)
346348
changes = "".join(
347-
" - " + str(problem_source_dir(problem)) + "/problem_statement/**/*\n"
349+
f" - {problem_source_dir(problem)}/{pdf_type.path().parent}/**/*\n"
348350
for problem in problems
351+
for pdf_type in latex.PdfType
349352
)
350353
print(
351354
substitute(

bin/stats.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import config
1313
import generate
14+
import latex
1415
import program
1516
from util import error, exec_command, glob, warn
1617

@@ -48,8 +49,8 @@ def problem_stats(problems):
4849
# Roughly in order of importance
4950
(" time", lambda p: p.limits.time_limit, 0),
5051
("yaml", "problem.yaml"),
51-
("tex", "problem_statement/problem*.tex", 1),
52-
("sol", "problem_statement/solution*.tex", 1),
52+
("tex", str(latex.PdfType.PROBLEM.path("*")), 1),
53+
("sol", str(latex.PdfType.SOLUTION.path("*")), 1),
5354
(" val: I", ["input_validators/*", "input_format_validators/*"]),
5455
("A", ["answer_validators/*"]),
5556
("O", ["output_validators/*"]),

bin/tools.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -1216,10 +1216,8 @@ def run_parsed_arguments(args):
12161216
web=True,
12171217
)
12181218
# Only build the problem slides if at least one problem has the TeX for it
1219-
if any(
1220-
glob(problem.path / "problem_statement", "problem-slide.*.tex")
1221-
for problem in problems
1222-
):
1219+
slideglob = latex.PdfType.PROBLEM_SLIDE.path("*")
1220+
if any(problem.path.glob(str(slideglob)) for problem in problems):
12231221
success &= latex.build_contest_pdf(
12241222
contest,
12251223
problems,
@@ -1228,7 +1226,7 @@ def run_parsed_arguments(args):
12281226
build_type=latex.PdfType.PROBLEM_SLIDE,
12291227
)
12301228
else:
1231-
log("No problem has problem-slide.*.tex, skipping problem slides")
1229+
log(f"No problem has {slideglob.name}, skipping problem slides")
12321230

12331231
outfile = contest + ".zip"
12341232
if config.args.kattis:

0 commit comments

Comments
 (0)