From bf24a9f5509fedfbf40c70d05b82285d52bfe86e Mon Sep 17 00:00:00 2001 From: Gunnar Kreitz Date: Tue, 3 Jun 2025 15:41:33 +0200 Subject: [PATCH 1/6] Fix bug where we crashed if we attempted to load/check twice --- problemtools/verifyproblem.py | 1 + tests/test_verify_hello.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/problemtools/verifyproblem.py b/problemtools/verifyproblem.py index 1b419785..912b1b3b 100644 --- a/problemtools/verifyproblem.py +++ b/problemtools/verifyproblem.py @@ -1800,6 +1800,7 @@ def load(self) -> None: self.graders = Graders(self) self.testdata = TestCaseGroup(self, os.path.join(self.probdir, 'data')) self.submissions = Submissions(self) + self.loaded = True def __enter__(self) -> Problem: self.tmpdir = tempfile.mkdtemp(prefix=f'verify-{self.shortname}-') diff --git a/tests/test_verify_hello.py b/tests/test_verify_hello.py index f51de740..6f5ac3c4 100644 --- a/tests/test_verify_hello.py +++ b/tests/test_verify_hello.py @@ -21,3 +21,13 @@ def test_load_hello(): assert not p.is_interactive() assert not p.is_multi_pass() assert not p.is_submit_answer() + + +def test_load_twice(): + directory = pathlib.Path(__file__).parent / 'hello' + string = str(directory.resolve()) + + args = verify.argparser().parse_args([string]) + with verify.Problem(string, args) as p: + p.load() + p.load() From 41ad547c61842c58a5ca0fa67209f69ec49f2571 Mon Sep 17 00:00:00 2001 From: Gunnar Kreitz Date: Tue, 3 Jun 2025 15:44:28 +0200 Subject: [PATCH 2/6] Error if problem name exits in a language without a statement --- problemtools/verifyproblem.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/problemtools/verifyproblem.py b/problemtools/verifyproblem.py index 912b1b3b..65fc3646 100644 --- a/problemtools/verifyproblem.py +++ b/problemtools/verifyproblem.py @@ -867,6 +867,10 @@ def check(self, context: Context) -> bool: if self._metadata.uuid is None: self.error_in_2023_07(f'Missing uuid from problem.yaml. Add "uuid: {uuid.uuid4()}" to problem.yaml.') + names_with_no_statement = [lang for lang in self._metadata.name if lang not in self.problem.statement.statements] + if names_with_no_statement: + self.error(f'Names exist for languages without problem statements: {", ".join(names_with_no_statement)}') + if self._metadata.legacy_grading.show_test_data_groups and self.problem.is_pass_fail(): self.error('Showing test data groups is only supported for scoring problems, this is a pass-fail problem') if ( From c571d56b96f0509c11f04a51a08bf58c90f9b645 Mon Sep 17 00:00:00 2001 From: Gunnar Kreitz Date: Tue, 3 Jun 2025 15:59:08 +0200 Subject: [PATCH 3/6] Add utility function uses_default_validator for output validation. Warn/error on multiple validators --- problemtools/verifyproblem.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/problemtools/verifyproblem.py b/problemtools/verifyproblem.py index 65fc3646..eed76615 100644 --- a/problemtools/verifyproblem.py +++ b/problemtools/verifyproblem.py @@ -1214,6 +1214,11 @@ def setup(self): ) self._has_precompiled = False + def uses_default_validator(self) -> bool: + if self.problem.format is FormatVersion.LEGACY: + return self.problem.metadata.legacy_validation == 'default' + return not self._validators + def __str__(self) -> str: return 'output validators' @@ -1238,12 +1243,15 @@ def check(self, context: Context) -> bool: f'Output validator in {v.language.name}. Only {safe_output_validator_languages} are standardized. Check carefully if your CCS supports more (Kattis does not).' ) - if self.problem.metadata.legacy_validation == 'default' and self._validators: + if len(self._validators) > 1: + self.error_in_2023_07('Found more than one output validator. This was allowed in legacy (but not on Kattis)') + + if self.uses_default_validator() and self._validators: self.error('There are validator programs but problem.yaml has validation = "default"') - elif self.problem.metadata.legacy_validation.startswith('custom') and not self._validators: + elif not self.uses_default_validator() and not self._validators: self.fatal('problem.yaml specifies custom validator but no validator programs found') - if self.problem.metadata.legacy_validation == 'default' and self._default_validator is None: + if self.uses_default_validator() and self._default_validator is None: self.fatal('Unable to locate default validator') for val in self._validators[:]: @@ -1336,10 +1344,9 @@ def _parse_validator_results(self, val, status: int, feedbackdir, testcase: Test return SubmissionResult('AC', score=score) def _actual_validators(self) -> list: - vals = self._validators - if self.problem.metadata.legacy_validation == 'default' or (self.problem.format is FormatVersion.V_2023_07 and not vals): - vals = [self._default_validator] - return [val for val in vals if val is not None] + if self.uses_default_validator(): + return [self._default_validator] + return self._validators def validate_interactive(self, testcase: TestCase, submission, timelim: int, errorhandler: Submissions) -> SubmissionResult: # This may be called off-main thread. @@ -1405,7 +1412,6 @@ def validate_interactive(self, testcase: TestCase, submission, timelim: int, err shutil.rmtree(feedbackdir) if res.verdict != 'AC': return res - # TODO: check that all output validators give same result return res def validate(self, testcase: TestCase, submission_output: str) -> SubmissionResult: From ab1a2daf86b71c6cb609f276c1082a43d74dcadf Mon Sep 17 00:00:00 2001 From: Gunnar Kreitz Date: Tue, 3 Jun 2025 18:52:28 +0200 Subject: [PATCH 4/6] Fix new mypy error in mypy 1.16 --- problemtools/ProblemPlasTeX/ProblemsetMacros.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/problemtools/ProblemPlasTeX/ProblemsetMacros.py b/problemtools/ProblemPlasTeX/ProblemsetMacros.py index 16a2b39d..b5bd4f41 100644 --- a/problemtools/ProblemPlasTeX/ProblemsetMacros.py +++ b/problemtools/ProblemPlasTeX/ProblemsetMacros.py @@ -114,7 +114,7 @@ def invoke(self, tex): f = self.attributes['file'] ext = self.ownerDocument.userdata.getPath('packages/graphicx/extensions', ['.png', '.jpg', '.jpeg', '.gif', '.pdf']) paths = self.ownerDocument.userdata.getPath('packages/graphicx/paths', [os.path.dirname(basetex.filename)]) - img = None + img: str | None = None # Check for file using graphicspath for p in paths: for e in [''] + ext: @@ -134,7 +134,7 @@ def invoke(self, tex): except (OSError, IOError): pass - if not os.path.isfile(img): + if img is None or not os.path.isfile(img): log.warning('Could not identify image "%s"' % f) self.imageoverride = img From 42ec59dcd3d5a82ad4bcf3e6980f607a840a6cc5 Mon Sep 17 00:00:00 2001 From: Gunnar Kreitz Date: Wed, 4 Jun 2025 12:17:04 +0200 Subject: [PATCH 5/6] Fix missing support for imgbasedir in md2html --- problemtools/md2html.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/problemtools/md2html.py b/problemtools/md2html.py index fa373bc9..445eca0f 100644 --- a/problemtools/md2html.py +++ b/problemtools/md2html.py @@ -23,11 +23,12 @@ def convert(problem_root: Path, options: argparse.Namespace, statement_file: Pat options: command-line arguments. See problem2html.py """ destfile = string.Template(options.destfile).safe_substitute(problem=problem_root.name) + imgbasedir = string.Template(options.imgbasedir).safe_substitute(problem=problem_root.name) command = ['pandoc', str(statement_file), '-t', 'html', '--mathjax'] statement_html = subprocess.run(command, capture_output=True, text=True, shell=False, check=True).stdout - statement_html = sanitize_html(statement_file.parent, statement_html) + statement_html = sanitize_html(statement_file.parent, statement_html, imgbasedir) templatepaths = [ os.path.join(os.path.dirname(__file__), 'templates/markdown_html'), @@ -76,7 +77,7 @@ def convert(problem_root: Path, options: argparse.Namespace, statement_file: Pat return True -def sanitize_html(statement_dir: Path, statement_html: str) -> str: +def sanitize_html(statement_dir: Path, statement_html: str, imgbasedir: str) -> str: # Allow footnote ids (the anchor points you jump to) def is_fn_id(s): pattern_id_top = r'^fn\d+$' @@ -106,7 +107,7 @@ def attribute_filter(tag, attribute, value): nonlocal image_fail_reason image_fail_reason.append(e) return None - return copy_image(statement_dir, value) + return copy_image(statement_dir, value, imgbasedir) return None statement_html = nh3.clean( @@ -132,7 +133,7 @@ def attribute_filter(tag, attribute, value): return statement_html -def copy_image(statement_dir: Path, img_src: str) -> str: +def copy_image(statement_dir: Path, img_src: str, imgbasedir: str) -> str: """Copy image to working directory (with new filename) and returns the new filename Args: @@ -147,4 +148,4 @@ def copy_image(statement_dir: Path, img_src: str) -> str: if not os.path.isfile(filename): # check if already copied shutil.copyfile(statement_dir / img_src, filename) - return filename + return imgbasedir + filename From a8e80b1f96999fa41c9c2658be36912afd6a7d2a Mon Sep 17 00:00:00 2001 From: Gunnar Kreitz Date: Wed, 4 Jun 2025 15:55:12 +0200 Subject: [PATCH 6/6] Fix typo in Dockerfile.full causing it to lack a lot of languages --- admin/docker/Dockerfile.full | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/docker/Dockerfile.full b/admin/docker/Dockerfile.full index 7d982b65..75ec8745 100644 --- a/admin/docker/Dockerfile.full +++ b/admin/docker/Dockerfile.full @@ -10,7 +10,7 @@ # PROBLEMTOOLS_VERSION but this is not checked.) ARG PROBLEMTOOLS_VERSION=develop -FROM problemtools/runreqs:${PROBLEMTOOLS_VERSION} +FROM problemtools/fulllangs:${PROBLEMTOOLS_VERSION} LABEL maintainer="contact@kattis.com" ENV DEBIAN_FRONTEND=noninteractive