Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 1 addition & 1 deletion admin/docker/Dockerfile.full
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions problemtools/ProblemPlasTeX/ProblemsetMacros.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
11 changes: 6 additions & 5 deletions problemtools/md2html.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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+$'
Expand Down Expand Up @@ -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(
Expand All @@ -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:
Expand All @@ -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
27 changes: 19 additions & 8 deletions problemtools/verifyproblem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -1210,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'

Expand All @@ -1234,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[:]:
Expand Down Expand Up @@ -1332,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.
Expand Down Expand Up @@ -1401,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:
Expand Down Expand Up @@ -1800,6 +1810,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}-')
Expand Down
10 changes: 10 additions & 0 deletions tests/test_verify_hello.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()