rbx.cp 0.5.73__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rbx/annotations.py +21 -1
- rbx/box/cli.py +10 -1
- rbx/box/code.py +140 -3
- rbx/box/contest/build_contest_statements.py +44 -34
- rbx/box/contest/schema.py +52 -8
- rbx/box/contest/statements.py +53 -25
- rbx/box/fields.py +35 -0
- rbx/box/lang.py +27 -0
- rbx/box/package.py +1 -1
- rbx/box/packaging/boca/packager.py +48 -5
- rbx/box/packaging/contest_main.py +13 -0
- rbx/box/packaging/main.py +13 -2
- rbx/box/packaging/packager.py +4 -4
- rbx/box/packaging/pkg/packager.py +142 -0
- rbx/box/packaging/polygon/packager.py +2 -24
- rbx/box/packaging/polygon/upload.py +35 -17
- rbx/box/remote.py +2 -2
- rbx/box/schema.py +68 -18
- rbx/box/solutions.py +6 -1
- rbx/box/statements/build_statements.py +44 -27
- rbx/box/statements/builders.py +18 -10
- rbx/box/statements/expander.py +49 -0
- rbx/box/statements/latex_jinja.py +61 -4
- rbx/box/statements/schema.py +33 -9
- rbx/box/testcase_utils.py +19 -47
- rbx/box/tooling/__init__.py +0 -0
- rbx/box/tooling/boca/__init__.py +0 -0
- rbx/box/tooling/boca/main.py +13 -0
- rbx/box/tooling/boca/scrape.py +34 -0
- rbx/box/{packaging/boca/upload.py → tooling/boca/scraper.py} +77 -8
- rbx/box/tooling/main.py +8 -0
- rbx/box/ui/widgets/interaction_box.py +19 -1
- rbx/grading/caching.py +18 -2
- rbx/grading/judge/sandbox.py +48 -5
- rbx/grading/judge/sandboxes/isolate.py +1 -0
- rbx/grading/judge/sandboxes/stupid_sandbox.py +11 -5
- rbx/grading/judge/sandboxes/timeit.py +36 -15
- rbx/grading/processing_context.py +62 -78
- rbx/grading/steps.py +91 -40
- rbx/resources/packagers/boca/checker.sh +4 -1
- rbx/resources/packagers/boca/compile/c +2 -6
- rbx/resources/packagers/boca/compile/cc +2 -6
- rbx/resources/packagers/boca/compile/cpp +2 -6
- rbx/resources/packagers/boca/compile/java +1 -6
- rbx/resources/packagers/boca/compile/kt +24 -28
- rbx/resources/packagers/boca/compile/py2 +2 -6
- rbx/resources/packagers/boca/compile/py3 +2 -6
- rbx/resources/packagers/boca/interactive/c +15 -83
- rbx/resources/packagers/boca/interactive/cc +15 -83
- rbx/resources/packagers/boca/interactive/cpp +15 -83
- rbx/resources/packagers/boca/interactive/java +15 -88
- rbx/resources/packagers/boca/interactive/kt +15 -88
- rbx/resources/packagers/boca/interactive/py2 +15 -88
- rbx/resources/packagers/boca/interactive/py3 +15 -88
- rbx/resources/packagers/boca/interactor_compile.sh +5 -2
- rbx/resources/packagers/boca/interactor_run.sh +174 -0
- rbx/resources/packagers/boca/safeexec.c +530 -0
- rbx/resources/packagers/boca/safeexec_compile.sh +49 -0
- rbx/resources/presets/default/contest/contest.rbx.yml +9 -8
- rbx/resources/presets/default/problem/problem.rbx.yml +27 -26
- rbx/resources/templates/rbx.h +2 -3
- {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.0.dist-info}/METADATA +2 -1
- {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.0.dist-info}/RECORD +66 -55
- rbx/resources/packagers/boca/compile/pas +0 -172
- {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.0.dist-info}/LICENSE +0 -0
- {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.0.dist-info}/WHEEL +0 -0
- {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.0.dist-info}/entry_points.txt +0 -0
rbx/box/schema.py
CHANGED
@@ -2,30 +2,21 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import os
|
4
4
|
import pathlib
|
5
|
-
|
5
|
+
import re
|
6
|
+
from typing import Annotated, Any, Dict, List, Optional, Union
|
6
7
|
|
7
|
-
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
8
|
+
from pydantic import AfterValidator, BaseModel, ConfigDict, Field, model_validator
|
8
9
|
from pydantic_core import PydanticCustomError
|
9
10
|
|
10
11
|
from rbx.autoenum import AutoEnum, alias
|
12
|
+
from rbx.box.fields import FNameField, NameField
|
13
|
+
from rbx.box.statements.expander import expand_statements
|
11
14
|
from rbx.box.statements.schema import Statement
|
12
15
|
from rbx.grading.steps import Outcome
|
13
16
|
|
14
17
|
Primitive = Union[str, int, float, bool]
|
15
18
|
|
16
19
|
|
17
|
-
def NameField(**kwargs):
|
18
|
-
return Field(
|
19
|
-
pattern=r'^[a-zA-Z0-9][a-zA-Z0-9\-_]*$', min_length=3, max_length=32, **kwargs
|
20
|
-
)
|
21
|
-
|
22
|
-
|
23
|
-
def FNameField(**kwargs):
|
24
|
-
return Field(
|
25
|
-
pattern=r'^[a-zA-Z0-9][a-zA-Z0-9\-_]*$', min_length=3, max_length=128, **kwargs
|
26
|
-
)
|
27
|
-
|
28
|
-
|
29
20
|
def _check_oneof(model_obj: BaseModel, fields: List[str]):
|
30
21
|
has = []
|
31
22
|
for field in fields:
|
@@ -56,6 +47,44 @@ def expand_var(value: Primitive) -> Primitive:
|
|
56
47
|
)
|
57
48
|
|
58
49
|
|
50
|
+
def expand_vars(vars: Dict[str, Primitive]) -> Dict[str, Primitive]:
|
51
|
+
return {key: expand_var(value) for key, value in vars.items()}
|
52
|
+
|
53
|
+
|
54
|
+
def _represents_int(s: str) -> bool:
|
55
|
+
return re.match(r'[-+]?\d+$', s.strip()) is not None
|
56
|
+
|
57
|
+
|
58
|
+
def _represents_float(s: str) -> bool:
|
59
|
+
return re.match(r'[-+]?\d+\.\d+$', s.strip()) is not None
|
60
|
+
|
61
|
+
|
62
|
+
def _represents_bool(s: str) -> bool:
|
63
|
+
return s.lower().strip() in ['true', 'false', 'True', 'False']
|
64
|
+
|
65
|
+
|
66
|
+
def convert_to_primitive(value: Any) -> Primitive:
|
67
|
+
if _represents_int(value):
|
68
|
+
return int(value)
|
69
|
+
if _represents_float(value):
|
70
|
+
return float(value)
|
71
|
+
if _represents_bool(value):
|
72
|
+
return value.lower().strip() == 'true'
|
73
|
+
return str(value)
|
74
|
+
|
75
|
+
|
76
|
+
def expand_any_vars(vars: Dict[str, Any]) -> Dict[str, Primitive]:
|
77
|
+
converted_vars = {key: convert_to_primitive(value) for key, value in vars.items()}
|
78
|
+
return expand_vars(converted_vars)
|
79
|
+
|
80
|
+
|
81
|
+
def is_unique_by_name(statements: List['Statement']) -> List['Statement']:
|
82
|
+
names = {st.name for st in statements}
|
83
|
+
if len(names) != len(statements):
|
84
|
+
raise ValueError('Statement names must be unique.')
|
85
|
+
return statements
|
86
|
+
|
87
|
+
|
59
88
|
class ExpectedOutcome(AutoEnum):
|
60
89
|
ANY = alias('any') # type: ignore
|
61
90
|
"""Expected outcome for any outcome."""
|
@@ -459,9 +488,10 @@ that is correct and used as reference -- and should have the `accepted` outcome.
|
|
459
488
|
default=[], description='Stress tests for the problem.'
|
460
489
|
)
|
461
490
|
|
462
|
-
statements:
|
463
|
-
|
464
|
-
|
491
|
+
statements: Annotated[
|
492
|
+
List[Statement],
|
493
|
+
AfterValidator(is_unique_by_name),
|
494
|
+
] = Field(default=[], description='Statements for the problem.')
|
465
495
|
|
466
496
|
# Vars to be re-used across the package.
|
467
497
|
# - It will be passed as --key=value arguments to the validator.
|
@@ -475,9 +505,13 @@ that is correct and used as reference -- and should have the `accepted` outcome.
|
|
475
505
|
description='Unit tests for components of this problem.',
|
476
506
|
)
|
477
507
|
|
508
|
+
@property
|
509
|
+
def expanded_statements(self) -> List[Statement]:
|
510
|
+
return expand_statements(self.statements)
|
511
|
+
|
478
512
|
@property
|
479
513
|
def expanded_vars(self) -> Dict[str, Primitive]:
|
480
|
-
return
|
514
|
+
return expand_vars(self.vars)
|
481
515
|
|
482
516
|
def timelimit_for_language(self, language: Optional[str]) -> int:
|
483
517
|
res = self.timeLimit
|
@@ -525,3 +559,19 @@ that is correct and used as reference -- and should have the `accepted` outcome.
|
|
525
559
|
{'i': i + 1},
|
526
560
|
)
|
527
561
|
return self
|
562
|
+
|
563
|
+
@model_validator(mode='after')
|
564
|
+
def check_checker_and_interactor_for_task_type(self):
|
565
|
+
if self.type == TaskType.BATCH:
|
566
|
+
if self.interactor is not None:
|
567
|
+
raise PydanticCustomError(
|
568
|
+
'INTERACTOR_NOT_ALLOWED',
|
569
|
+
'Interactor is not allowed for batch problems. Change the task type to COMMUNICATION.',
|
570
|
+
)
|
571
|
+
if self.type == TaskType.COMMUNICATION:
|
572
|
+
if self.checker is not None:
|
573
|
+
raise PydanticCustomError(
|
574
|
+
'CHECKER_NOT_ALLOWED',
|
575
|
+
'Checkers should not be specified for communication problems.',
|
576
|
+
)
|
577
|
+
return self
|
rbx/box/solutions.py
CHANGED
@@ -48,6 +48,7 @@ from rbx.box.tasks import (
|
|
48
48
|
from rbx.box.testcase_extractors import extract_generation_testcases
|
49
49
|
from rbx.box.testcase_utils import (
|
50
50
|
TestcaseEntry,
|
51
|
+
TestcaseInteractionParsingError,
|
51
52
|
find_built_testcases,
|
52
53
|
parse_interaction,
|
53
54
|
print_interaction,
|
@@ -374,7 +375,11 @@ def print_best_output(output_files: List[pathlib.Path], empty_warning: bool = Fa
|
|
374
375
|
if not output_file.is_file():
|
375
376
|
continue
|
376
377
|
if output_file.suffix == '.pio':
|
377
|
-
|
378
|
+
try:
|
379
|
+
print_interaction(parse_interaction(output_file))
|
380
|
+
except TestcaseInteractionParsingError:
|
381
|
+
# Ignore parsing errors and proceed to next file.
|
382
|
+
continue
|
378
383
|
else:
|
379
384
|
console.console.print(output_file.read_text())
|
380
385
|
return
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import pathlib
|
2
2
|
import tempfile
|
3
3
|
import typing
|
4
|
-
from typing import Annotated, Dict, List, Optional, Tuple
|
4
|
+
from typing import Annotated, Any, Dict, List, Optional, Tuple
|
5
5
|
|
6
6
|
import syncer
|
7
7
|
import typer
|
@@ -9,7 +9,7 @@ import typer
|
|
9
9
|
from rbx import annotations, console
|
10
10
|
from rbx.box import environment, naming, package
|
11
11
|
from rbx.box.formatting import href
|
12
|
-
from rbx.box.schema import Package
|
12
|
+
from rbx.box.schema import Package, expand_any_vars
|
13
13
|
from rbx.box.statements.builders import (
|
14
14
|
BUILDER_LIST,
|
15
15
|
PROBLEM_BUILDER_LIST,
|
@@ -217,7 +217,7 @@ def build_statement_bytes(
|
|
217
217
|
overridden_params: Optional[Dict[ConversionType, ConversionStep]] = None,
|
218
218
|
overridden_assets: Optional[List[Tuple[pathlib.Path, pathlib.Path]]] = None,
|
219
219
|
use_samples: bool = True,
|
220
|
-
|
220
|
+
custom_vars: Optional[Dict[str, Any]] = None,
|
221
221
|
) -> Tuple[bytes, StatementType]:
|
222
222
|
overridden_params = overridden_params or {}
|
223
223
|
overridden_assets = overridden_assets or []
|
@@ -258,10 +258,11 @@ def build_statement_bytes(
|
|
258
258
|
output = bdr.build(
|
259
259
|
input=last_content,
|
260
260
|
context=StatementBuilderContext(
|
261
|
+
lang=statement.language,
|
261
262
|
languages=get_environment_languages_for_statement(),
|
262
263
|
params=params,
|
263
264
|
root=pathlib.Path(td),
|
264
|
-
|
265
|
+
custom_vars=custom_vars,
|
265
266
|
),
|
266
267
|
item=StatementBuilderProblem(
|
267
268
|
package=pkg,
|
@@ -284,24 +285,23 @@ def build_statement(
|
|
284
285
|
pkg: Package,
|
285
286
|
output_type: Optional[StatementType] = None,
|
286
287
|
use_samples: bool = True,
|
287
|
-
|
288
|
+
custom_vars: Optional[Dict[str, Any]] = None,
|
288
289
|
) -> pathlib.Path:
|
289
290
|
last_content, last_output = build_statement_bytes(
|
290
291
|
statement,
|
291
292
|
pkg,
|
292
293
|
output_type=output_type,
|
293
294
|
use_samples=use_samples,
|
294
|
-
|
295
|
+
custom_vars=custom_vars,
|
295
296
|
short_name=naming.get_problem_shortname(),
|
296
297
|
)
|
297
|
-
statement_path = (
|
298
|
-
|
299
|
-
/ f'{statement.path.stem}{last_output.get_file_suffix()}'
|
298
|
+
statement_path = (package.get_build_path() / statement.name).with_suffix(
|
299
|
+
last_output.get_file_suffix()
|
300
300
|
)
|
301
301
|
statement_path.parent.mkdir(parents=True, exist_ok=True)
|
302
302
|
statement_path.write_bytes(last_content)
|
303
303
|
console.console.print(
|
304
|
-
f'Statement built successfully for language '
|
304
|
+
f'Statement [item]{statement.name}[/item] built successfully for language '
|
305
305
|
f'[item]{statement.language}[/item] at '
|
306
306
|
f'{href(statement_path)}'
|
307
307
|
)
|
@@ -313,13 +313,18 @@ def build_statement(
|
|
313
313
|
@syncer.sync
|
314
314
|
async def build(
|
315
315
|
verification: environment.VerificationParam,
|
316
|
+
names: Annotated[
|
317
|
+
Optional[List[str]],
|
318
|
+
typer.Argument(
|
319
|
+
help='Names of statements to build.',
|
320
|
+
),
|
321
|
+
] = None,
|
316
322
|
languages: Annotated[
|
317
323
|
Optional[List[str]],
|
318
324
|
typer.Option(
|
319
|
-
default_factory=list,
|
320
325
|
help='Languages to build statements for. If not specified, build statements for all available languages.',
|
321
326
|
),
|
322
|
-
],
|
327
|
+
] = None,
|
323
328
|
output: Annotated[
|
324
329
|
Optional[StatementType],
|
325
330
|
typer.Option(
|
@@ -331,9 +336,14 @@ async def build(
|
|
331
336
|
bool,
|
332
337
|
typer.Option(help='Whether to build the statement with samples or not.'),
|
333
338
|
] = True,
|
334
|
-
|
335
|
-
|
336
|
-
|
339
|
+
vars: Annotated[
|
340
|
+
Optional[List[str]],
|
341
|
+
typer.Option(
|
342
|
+
'-v',
|
343
|
+
'--vars',
|
344
|
+
help='Variables to be used in the statements.',
|
345
|
+
),
|
346
|
+
] = None,
|
337
347
|
):
|
338
348
|
# At most run the validators, only in samples.
|
339
349
|
if samples:
|
@@ -350,24 +360,31 @@ async def build(
|
|
350
360
|
raise typer.Exit(1)
|
351
361
|
|
352
362
|
pkg = package.find_problem_package_or_die()
|
353
|
-
candidate_languages = languages
|
354
|
-
|
355
|
-
candidate_languages = sorted(set([st.language for st in pkg.statements]))
|
363
|
+
candidate_languages = set(languages or [])
|
364
|
+
candidate_names = set(names or [])
|
356
365
|
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
366
|
+
def should_process(st: Statement) -> bool:
|
367
|
+
if candidate_languages and st.language not in candidate_languages:
|
368
|
+
return False
|
369
|
+
if candidate_names and st.name not in candidate_names:
|
370
|
+
return False
|
371
|
+
return True
|
372
|
+
|
373
|
+
valid_statements = [st for st in pkg.expanded_statements if should_process(st)]
|
374
|
+
|
375
|
+
if not valid_statements:
|
376
|
+
console.console.print(
|
377
|
+
'[error]No statement found according to the specified criteria.[/error]',
|
378
|
+
)
|
379
|
+
raise typer.Exit(1)
|
364
380
|
|
381
|
+
for statement in valid_statements:
|
365
382
|
build_statement(
|
366
|
-
|
383
|
+
statement,
|
367
384
|
pkg,
|
368
385
|
output_type=output,
|
369
386
|
use_samples=samples,
|
370
|
-
|
387
|
+
custom_vars=expand_any_vars(annotations.parse_dictionary_items(vars)),
|
371
388
|
)
|
372
389
|
|
373
390
|
|
rbx/box/statements/builders.py
CHANGED
@@ -25,7 +25,11 @@ from rbx.box.statements.schema import (
|
|
25
25
|
TexToPDF,
|
26
26
|
rbxToTeX,
|
27
27
|
)
|
28
|
-
from rbx.box.testcase_utils import
|
28
|
+
from rbx.box.testcase_utils import (
|
29
|
+
TestcaseInteraction,
|
30
|
+
TestcaseInteractionParsingError,
|
31
|
+
parse_interaction,
|
32
|
+
)
|
29
33
|
|
30
34
|
|
31
35
|
@dataclasses.dataclass
|
@@ -37,20 +41,22 @@ class StatementCodeLanguage:
|
|
37
41
|
|
38
42
|
@dataclasses.dataclass
|
39
43
|
class StatementBuilderContext:
|
44
|
+
lang: str
|
40
45
|
languages: List[StatementCodeLanguage]
|
41
46
|
params: ConversionStep
|
42
47
|
root: pathlib.Path
|
43
|
-
|
48
|
+
custom_vars: Optional[Dict[str, Any]] = None
|
44
49
|
vars: Optional[Dict[str, Primitive]] = None
|
45
50
|
|
46
51
|
def build_jinja_kwargs(self) -> Dict[str, Any]:
|
47
52
|
res = {
|
53
|
+
'lang': self.lang,
|
48
54
|
'languages': self.languages,
|
49
55
|
'keyed_languages': {lang.id: lang for lang in self.languages},
|
50
|
-
'is_editorial': self.editorial,
|
51
56
|
}
|
52
|
-
if self.vars is not None:
|
53
|
-
res['vars'] = self.vars
|
57
|
+
if self.vars is not None or self.custom_vars is not None:
|
58
|
+
res['vars'] = self.vars or {}
|
59
|
+
res['vars'].update(self.custom_vars or {})
|
54
60
|
return res
|
55
61
|
|
56
62
|
|
@@ -82,7 +88,13 @@ class StatementSample(BaseModel):
|
|
82
88
|
|
83
89
|
interaction = None
|
84
90
|
if pio_path.is_file():
|
85
|
-
|
91
|
+
try:
|
92
|
+
interaction = parse_interaction(pio_path)
|
93
|
+
except TestcaseInteractionParsingError as e:
|
94
|
+
console.console.print(
|
95
|
+
f'Error parsing interactive sample: [error]{e}[/error]'
|
96
|
+
)
|
97
|
+
raise typer.Exit(1) from e
|
86
98
|
|
87
99
|
return StatementSample(
|
88
100
|
inputPath=input_path,
|
@@ -325,10 +337,6 @@ class rbxTeXBuilder(StatementBuilder):
|
|
325
337
|
)
|
326
338
|
blocks = statement_blocks.blocks
|
327
339
|
|
328
|
-
# Remove editorial block when not editorial.
|
329
|
-
if not context.editorial and 'editorial' in blocks:
|
330
|
-
del blocks['editorial']
|
331
|
-
|
332
340
|
problem_kwargs = problem.build_jinja_kwargs()
|
333
341
|
problem_kwargs['problem']['blocks'] = blocks
|
334
342
|
if statement_blocks.explanations is not None:
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import collections
|
2
|
+
from typing import Any, List, TypeVar
|
3
|
+
|
4
|
+
from rbx.box.fields import merge_pydantic_models
|
5
|
+
|
6
|
+
TypeVarT = TypeVar('TypeVarT', bound=Any)
|
7
|
+
|
8
|
+
|
9
|
+
def expand_statements(statements: List[TypeVarT]) -> List[TypeVarT]:
|
10
|
+
deg = collections.defaultdict(int)
|
11
|
+
dependencies = collections.defaultdict(list)
|
12
|
+
for statement in statements:
|
13
|
+
if statement.extends is not None:
|
14
|
+
deg[statement.name] += 1
|
15
|
+
dependencies[statement.extends].append(statement.name)
|
16
|
+
|
17
|
+
# Topological sort.
|
18
|
+
# - We need to expand statements in the order of dependencies.
|
19
|
+
# - This is a simple topological sort.
|
20
|
+
# - If there are multiple statements with indegree 0, we choose the first one.
|
21
|
+
st_per_name = {}
|
22
|
+
expanded = {}
|
23
|
+
st = []
|
24
|
+
for statement in statements:
|
25
|
+
st_per_name[statement.name] = statement
|
26
|
+
if deg[statement.name] == 0:
|
27
|
+
st.append(statement)
|
28
|
+
|
29
|
+
while st:
|
30
|
+
statement = st.pop()
|
31
|
+
expanded_statement = statement.model_copy()
|
32
|
+
if statement.extends is not None:
|
33
|
+
expanded_statement = merge_pydantic_models(
|
34
|
+
expanded[statement.extends], statement
|
35
|
+
)
|
36
|
+
|
37
|
+
expanded[statement.name] = expanded_statement
|
38
|
+
|
39
|
+
for dep_name in dependencies[statement.name]:
|
40
|
+
deg[dep_name] -= 1
|
41
|
+
if deg[dep_name] == 0:
|
42
|
+
st.append(st_per_name[dep_name])
|
43
|
+
|
44
|
+
if len(expanded) != len(statements):
|
45
|
+
raise ValueError(
|
46
|
+
f'Failed to expand statements: only {len(expanded)} out of {len(statements)} were expanded. This means there is a cycle introduced by the `extends` field.'
|
47
|
+
)
|
48
|
+
|
49
|
+
return [expanded[statement.name] for statement in statements]
|
@@ -6,9 +6,10 @@ with Latex.
|
|
6
6
|
import pathlib
|
7
7
|
import re
|
8
8
|
import typing
|
9
|
-
from typing import Dict, Tuple, Union
|
9
|
+
from typing import Any, Dict, Tuple, Union
|
10
10
|
|
11
11
|
import jinja2
|
12
|
+
import jinja2.runtime
|
12
13
|
import typer
|
13
14
|
|
14
15
|
from rbx import console
|
@@ -131,11 +132,56 @@ def path_stem(path: pathlib.Path) -> str:
|
|
131
132
|
return path.stem
|
132
133
|
|
133
134
|
|
135
|
+
@jinja2.pass_context
|
136
|
+
def test_var_truthy(ctx: jinja2.runtime.Context, value: Any):
|
137
|
+
if isinstance(value, jinja2.Undefined):
|
138
|
+
return False
|
139
|
+
if value is None:
|
140
|
+
return False
|
141
|
+
return bool(value)
|
142
|
+
|
143
|
+
|
144
|
+
@jinja2.pass_context
|
145
|
+
def test_var_falsy(ctx: jinja2.runtime.Context, value: Any):
|
146
|
+
return not test_var_truthy(ctx, value)
|
147
|
+
|
148
|
+
|
149
|
+
@jinja2.pass_context
|
150
|
+
def test_var_null(ctx: jinja2.runtime.Context, value: Any):
|
151
|
+
if isinstance(value, jinja2.Undefined):
|
152
|
+
return True
|
153
|
+
if value is None:
|
154
|
+
return True
|
155
|
+
return False
|
156
|
+
|
157
|
+
|
158
|
+
@jinja2.pass_context
|
159
|
+
def test_var_nonnull(ctx: jinja2.runtime.Context, value: Any):
|
160
|
+
return not test_var_null(ctx, value)
|
161
|
+
|
162
|
+
|
134
163
|
######################################################################
|
135
164
|
# Declare module functions
|
136
165
|
######################################################################
|
137
166
|
|
138
167
|
|
168
|
+
class StrictChainableUndefined(jinja2.StrictUndefined):
|
169
|
+
def __getattr__(self, name: str) -> 'StrictChainableUndefined':
|
170
|
+
# Raise AttributeError on requests for names that appear to be unimplemented
|
171
|
+
# dunder methods to avoid confusing Python with truthy non-method objects that
|
172
|
+
# do not implement the protocol being probed for. e.g., copy.copy(Undefined())
|
173
|
+
# fails spectacularly if getattr(Undefined(), '__setstate__') returns an
|
174
|
+
# Undefined object instead of raising AttributeError to signal that it does not
|
175
|
+
# support that style of object initialization.
|
176
|
+
if name[:2] == '__' and name[-2:] == '__':
|
177
|
+
raise AttributeError(name)
|
178
|
+
|
179
|
+
return self
|
180
|
+
|
181
|
+
def __getitem__(self, _name: str) -> 'StrictChainableUndefined': # type: ignore[override]
|
182
|
+
return self
|
183
|
+
|
184
|
+
|
139
185
|
class JinjaDictWrapper(dict):
|
140
186
|
def __init__(self, *args, key='dict object', **kwargs):
|
141
187
|
super().__init__(*args, **kwargs)
|
@@ -145,7 +191,9 @@ class JinjaDictWrapper(dict):
|
|
145
191
|
try:
|
146
192
|
return super().__getitem__(key)
|
147
193
|
except KeyError:
|
148
|
-
return
|
194
|
+
return StrictChainableUndefined(
|
195
|
+
hint=f'"{key}" was not found in "{self.key}"'
|
196
|
+
)
|
149
197
|
|
150
198
|
|
151
199
|
def add_builtin_filters(j2_env: jinja2.Environment):
|
@@ -155,6 +203,13 @@ def add_builtin_filters(j2_env: jinja2.Environment):
|
|
155
203
|
j2_env.filters['stem'] = path_stem
|
156
204
|
|
157
205
|
|
206
|
+
def add_builtin_tests(j2_env: jinja2.Environment):
|
207
|
+
j2_env.tests['truthy'] = test_var_truthy
|
208
|
+
j2_env.tests['falsy'] = test_var_falsy
|
209
|
+
j2_env.tests['null'] = test_var_null
|
210
|
+
j2_env.tests['nonnull'] = test_var_nonnull
|
211
|
+
|
212
|
+
|
158
213
|
def render_latex_template(path_templates, template_filename, template_vars=None) -> str:
|
159
214
|
"""Render a latex template, filling in its template variables
|
160
215
|
|
@@ -168,9 +223,10 @@ def render_latex_template(path_templates, template_filename, template_vars=None)
|
|
168
223
|
j2_env = jinja2.Environment(
|
169
224
|
loader=jinja2.FileSystemLoader(path_templates),
|
170
225
|
**J2_ARGS,
|
171
|
-
undefined=
|
226
|
+
undefined=StrictChainableUndefined,
|
172
227
|
)
|
173
228
|
add_builtin_filters(j2_env)
|
229
|
+
add_builtin_tests(j2_env)
|
174
230
|
template = j2_env.get_template(template_filename)
|
175
231
|
try:
|
176
232
|
return template.render(**var_dict) # type: ignore
|
@@ -198,9 +254,10 @@ def render_latex_template_blocks(
|
|
198
254
|
j2_env = jinja2.Environment(
|
199
255
|
loader=jinja2.FileSystemLoader(path_templates),
|
200
256
|
**J2_ARGS,
|
201
|
-
undefined=
|
257
|
+
undefined=StrictChainableUndefined,
|
202
258
|
)
|
203
259
|
add_builtin_filters(j2_env)
|
260
|
+
add_builtin_tests(j2_env)
|
204
261
|
template = j2_env.get_template(template_filename)
|
205
262
|
ctx = template.new_context(var_dict) # type: ignore
|
206
263
|
try:
|
rbx/box/statements/schema.py
CHANGED
@@ -2,11 +2,24 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import pathlib
|
4
4
|
from enum import Enum
|
5
|
-
from typing import List, Literal, Union
|
5
|
+
from typing import Annotated, List, Literal, Optional, Union
|
6
6
|
|
7
|
-
from pydantic import BaseModel, ConfigDict, Field
|
7
|
+
from pydantic import AfterValidator, BaseModel, ConfigDict, Field
|
8
8
|
|
9
9
|
from rbx.autoenum import AutoEnum, alias
|
10
|
+
from rbx.box.fields import FNameField
|
11
|
+
from rbx.box.lang import is_valid_lang_code
|
12
|
+
|
13
|
+
|
14
|
+
def validate_statement_language(lang: str):
|
15
|
+
if not is_valid_lang_code(lang) or not lang.islower():
|
16
|
+
raise ValueError(
|
17
|
+
f'Invalid statement language: {lang}. Language must be a valid lowercase ISO 639-1 code.'
|
18
|
+
)
|
19
|
+
return lang
|
20
|
+
|
21
|
+
|
22
|
+
StatementLanguage = Annotated[str, AfterValidator(validate_statement_language)]
|
10
23
|
|
11
24
|
|
12
25
|
### Conversion types
|
@@ -95,13 +108,28 @@ class StatementType(AutoEnum):
|
|
95
108
|
class Statement(BaseModel):
|
96
109
|
model_config = ConfigDict(extra='forbid')
|
97
110
|
|
111
|
+
name: str = FNameField(description='Name of this statement.')
|
112
|
+
|
113
|
+
extends: Optional[str] = FNameField(
|
114
|
+
default=None,
|
115
|
+
description='Name of the statement that this statement extends.',
|
116
|
+
)
|
117
|
+
|
118
|
+
language: StatementLanguage = Field(
|
119
|
+
default='en', description='Language code of this statement (ISO 639-1).'
|
120
|
+
)
|
121
|
+
|
98
122
|
title: str = Field(
|
99
|
-
description='Name of the problem, as it appears in the statement.'
|
123
|
+
default='', description='Name of the problem, as it appears in the statement.'
|
100
124
|
)
|
101
125
|
|
102
|
-
path: pathlib.Path = Field(
|
126
|
+
path: pathlib.Path = Field(
|
127
|
+
default_factory=pathlib.Path, description='Path to the input statement file.'
|
128
|
+
)
|
103
129
|
|
104
|
-
type: StatementType = Field(
|
130
|
+
type: StatementType = Field(
|
131
|
+
default=StatementType.rbxTeX, description='Type of the input statement file.'
|
132
|
+
)
|
105
133
|
|
106
134
|
steps: List[ConversionStep] = Field(
|
107
135
|
default=[],
|
@@ -134,7 +162,3 @@ the statement. Files will be included in the same folder as the statement file,
|
|
134
162
|
their relativeness. Can be glob pattern as well, such as `imgs/*.png`.
|
135
163
|
""",
|
136
164
|
)
|
137
|
-
|
138
|
-
language: str = Field(
|
139
|
-
default='en', description='Language this is statement is written in.'
|
140
|
-
)
|
rbx/box/testcase_utils.py
CHANGED
@@ -160,6 +160,10 @@ def fill_output_for_defined_testcase(testcase: Testcase) -> Testcase:
|
|
160
160
|
return res
|
161
161
|
|
162
162
|
|
163
|
+
class TestcaseInteractionParsingError(Exception):
|
164
|
+
pass
|
165
|
+
|
166
|
+
|
163
167
|
def parse_interaction(file: pathlib.Path) -> TestcaseInteraction:
|
164
168
|
entries = []
|
165
169
|
with file.open('r') as f:
|
@@ -167,53 +171,21 @@ def parse_interaction(file: pathlib.Path) -> TestcaseInteraction:
|
|
167
171
|
interactor_prefix = f.readline().strip()
|
168
172
|
solution_prefix = f.readline().strip()
|
169
173
|
except Exception:
|
170
|
-
|
171
|
-
f'
|
172
|
-
)
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
return (solution_idx, solution_idx + len(solution_prefix))
|
186
|
-
if solution_idx == -1:
|
187
|
-
return (interactor_idx, interactor_idx + len(interactor_prefix))
|
188
|
-
if interactor_idx < solution_idx:
|
189
|
-
return (interactor_idx, interactor_idx + len(interactor_prefix))
|
190
|
-
return (solution_idx, solution_idx + len(solution_prefix))
|
191
|
-
|
192
|
-
def _find_next_block() -> Optional[Tuple[int, Tuple[int, int]]]:
|
193
|
-
prefix = _find_next_prefix(start)
|
194
|
-
if prefix is None:
|
195
|
-
return None
|
196
|
-
prefix_start, prefix_end = prefix
|
197
|
-
prefix = rest[prefix_start:prefix_end]
|
198
|
-
pipe = 1 if prefix == solution_prefix else 0
|
199
|
-
|
200
|
-
nxt = _find_next_prefix(prefix_end)
|
201
|
-
if nxt is None:
|
202
|
-
return (pipe, (prefix_end, len(rest)))
|
203
|
-
nxt_start, _ = nxt
|
204
|
-
return (pipe, (prefix_end, nxt_start))
|
205
|
-
|
206
|
-
# TODO: optimize
|
207
|
-
blocks = 0
|
208
|
-
MAX_BLOCKS = 1024
|
209
|
-
while blocks < MAX_BLOCKS:
|
210
|
-
block = _find_next_block()
|
211
|
-
if block is None:
|
212
|
-
break
|
213
|
-
pipe, (st, nd) = block
|
214
|
-
entries.append(TestcaseInteractionEntry(data=rest[st:nd], pipe=pipe))
|
215
|
-
start = nd
|
216
|
-
blocks += 1
|
174
|
+
raise TestcaseInteractionParsingError(
|
175
|
+
f'Failed to read interaction file {file}. Expected the first two lines to be the interactor and solution prefixes.'
|
176
|
+
) from None
|
177
|
+
|
178
|
+
while line := f.readline().strip():
|
179
|
+
if line.startswith(interactor_prefix):
|
180
|
+
stripped = line[len(interactor_prefix) :].strip()
|
181
|
+
entries.append(TestcaseInteractionEntry(data=stripped, pipe=0))
|
182
|
+
elif line.startswith(solution_prefix):
|
183
|
+
stripped = line[len(solution_prefix) :].strip()
|
184
|
+
entries.append(TestcaseInteractionEntry(data=stripped, pipe=1))
|
185
|
+
else:
|
186
|
+
raise TestcaseInteractionParsingError(
|
187
|
+
f'Invalid line in interaction file {file}. Expected the line to start with the interactor or solution prefix ({interactor_prefix} or {solution_prefix}).'
|
188
|
+
) from None
|
217
189
|
|
218
190
|
return TestcaseInteraction(
|
219
191
|
prefixes=(interactor_prefix, solution_prefix),
|
File without changes
|
File without changes
|