rbx.cp 0.5.72__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 +24 -8
- rbx/box/code.py +140 -3
- rbx/box/contest/build_contest_statements.py +44 -34
- rbx/box/contest/contest_utils.py +25 -0
- rbx/box/contest/main.py +24 -0
- rbx/box/contest/schema.py +52 -8
- rbx/box/contest/statements.py +53 -25
- rbx/box/download.py +19 -1
- 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/screens/run_explorer.py +1 -1
- 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 -62
- rbx/resources/packagers/boca/interactive/cc +15 -62
- rbx/resources/packagers/boca/interactive/cpp +15 -61
- rbx/resources/packagers/boca/interactive/java +15 -67
- rbx/resources/packagers/boca/interactive/kt +15 -67
- rbx/resources/packagers/boca/interactive/py2 +15 -67
- rbx/resources/packagers/boca/interactive/py3 +15 -65
- 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.72.dist-info → rbx_cp-0.6.0.dist-info}/METADATA +2 -1
- {rbx_cp-0.5.72.dist-info → rbx_cp-0.6.0.dist-info}/RECORD +70 -59
- rbx/resources/packagers/boca/compile/pas +0 -172
- {rbx_cp-0.5.72.dist-info → rbx_cp-0.6.0.dist-info}/LICENSE +0 -0
- {rbx_cp-0.5.72.dist-info → rbx_cp-0.6.0.dist-info}/WHEEL +0 -0
- {rbx_cp-0.5.72.dist-info → rbx_cp-0.6.0.dist-info}/entry_points.txt +0 -0
@@ -10,8 +10,8 @@ import typer
|
|
10
10
|
from rbx import console
|
11
11
|
from rbx.box import header, package
|
12
12
|
from rbx.box.generators import get_all_built_testcases
|
13
|
+
from rbx.box.lang import code_to_langs, is_valid_lang_code
|
13
14
|
from rbx.box.packaging.polygon import polygon_api as api
|
14
|
-
from rbx.box.packaging.polygon.packager import code_to_langs, is_valid_lang_code
|
15
15
|
from rbx.box.schema import CodeItem, ExpectedOutcome, Solution, TaskType, Testcase
|
16
16
|
from rbx.box.statements.build_statements import get_relative_assets
|
17
17
|
from rbx.box.statements.builders import (
|
@@ -20,7 +20,11 @@ from rbx.box.statements.builders import (
|
|
20
20
|
render_jinja_blocks,
|
21
21
|
)
|
22
22
|
from rbx.box.statements.schema import Statement, StatementType
|
23
|
-
from rbx.box.testcase_utils import
|
23
|
+
from rbx.box.testcase_utils import (
|
24
|
+
TestcaseInteractionParsingError,
|
25
|
+
get_alternate_interaction_texts,
|
26
|
+
parse_interaction,
|
27
|
+
)
|
24
28
|
|
25
29
|
_API_URL = 'https://polygon.codeforces.com/api'
|
26
30
|
|
@@ -163,17 +167,23 @@ def _get_test_params_for_statement(
|
|
163
167
|
|
164
168
|
pio_path = testcase.outputPath.with_suffix('.pio')
|
165
169
|
if pio_path.is_file():
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
170
|
+
try:
|
171
|
+
interaction = parse_interaction(pio_path)
|
172
|
+
except TestcaseInteractionParsingError:
|
173
|
+
pass
|
174
|
+
else:
|
175
|
+
res['test_input_for_statements'], res['test_output_for_statements'] = (
|
176
|
+
get_alternate_interaction_texts(interaction)
|
177
|
+
)
|
178
|
+
return res
|
179
|
+
|
180
|
+
# .pio does not exist or is not parseable, fallback to .pin and .pout.
|
181
|
+
pin_path = testcase.outputPath.with_suffix('.pin')
|
182
|
+
if pin_path.is_file():
|
183
|
+
res['test_input_for_statements'] = pin_path.read_text()
|
184
|
+
pout_path = testcase.outputPath.with_suffix('.pout')
|
185
|
+
if pout_path.is_file():
|
186
|
+
res['test_output_for_statements'] = pout_path.read_text()
|
177
187
|
return res
|
178
188
|
|
179
189
|
|
@@ -229,7 +239,7 @@ def _upload_solutions(problem: api.Problem):
|
|
229
239
|
|
230
240
|
def _get_statement_for_language(language: str) -> Optional[Statement]:
|
231
241
|
pkg = package.find_problem_package_or_die()
|
232
|
-
for statement in pkg.
|
242
|
+
for statement in pkg.expanded_statements:
|
233
243
|
if statement.language == language:
|
234
244
|
return statement
|
235
245
|
return None
|
@@ -290,11 +300,15 @@ def _upload_statement(problem: api.Problem, preserve_language: bool = False):
|
|
290
300
|
pkg = package.find_problem_package_or_die()
|
291
301
|
|
292
302
|
languages = set()
|
293
|
-
for statement in pkg.
|
303
|
+
for statement in pkg.expanded_statements:
|
294
304
|
if not is_valid_lang_code(statement.language):
|
295
305
|
continue
|
296
306
|
languages.add(statement.language)
|
297
|
-
|
307
|
+
|
308
|
+
uploaded_languages = set()
|
309
|
+
|
310
|
+
# Prioritize English statements.
|
311
|
+
for language in ['en'] + list(languages):
|
298
312
|
statement = _get_statement_for_language(language)
|
299
313
|
if statement is None:
|
300
314
|
continue
|
@@ -304,6 +318,10 @@ def _upload_statement(problem: api.Problem, preserve_language: bool = False):
|
|
304
318
|
console.console.print(
|
305
319
|
f'Uploading statement for language [item]{language}[/item] (polygon language: [item]{statement_lang}[/item])...'
|
306
320
|
)
|
321
|
+
uploaded_language = statement_lang if preserve_language else 'english'
|
322
|
+
if uploaded_language in uploaded_languages:
|
323
|
+
continue
|
324
|
+
uploaded_languages.add(uploaded_language)
|
307
325
|
if not preserve_language and statement_lang != 'english':
|
308
326
|
console.console.print(
|
309
327
|
'[warning]By default, Polygon statements are uploaded in English.\n'
|
@@ -322,7 +340,7 @@ def _upload_statement(problem: api.Problem, preserve_language: bool = False):
|
|
322
340
|
notes=_get_notes_with_explanations(blocks) or '',
|
323
341
|
)
|
324
342
|
problem.save_statement(
|
325
|
-
lang=
|
343
|
+
lang=uploaded_language,
|
326
344
|
problem_statement=polygon_statement,
|
327
345
|
)
|
328
346
|
|
rbx/box/remote.py
CHANGED
@@ -52,14 +52,14 @@ class BocaExpander(Expander):
|
|
52
52
|
return [str(self.get_boca_path(run_number, site_number)) + '.*']
|
53
53
|
|
54
54
|
def expand(self, path: pathlib.Path) -> Optional[pathlib.Path]:
|
55
|
-
from rbx.box.
|
55
|
+
from rbx.box.tooling.boca import scraper as boca_upload
|
56
56
|
|
57
57
|
match = self.get_match(str(path))
|
58
58
|
if match is None:
|
59
59
|
return None
|
60
60
|
run_number, site_number = match
|
61
61
|
|
62
|
-
boca_uploader = boca_upload.
|
62
|
+
boca_uploader = boca_upload.get_boca_scraper()
|
63
63
|
boca_uploader.login()
|
64
64
|
sol_path = boca_uploader.download_run(
|
65
65
|
run_number, site_number, self.get_boca_folder()
|
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:
|