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
rbx/annotations.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import importlib.resources
|
2
2
|
import pathlib
|
3
3
|
import re
|
4
|
-
from typing import List, Optional
|
4
|
+
from typing import Any, Dict, List, Optional
|
5
5
|
|
6
6
|
import typer
|
7
7
|
import typer.core
|
@@ -112,6 +112,26 @@ Checker = Annotated[
|
|
112
112
|
]
|
113
113
|
|
114
114
|
|
115
|
+
def parse_dictionary(value: Optional[str]) -> Dict[str, Any]:
|
116
|
+
if value is None:
|
117
|
+
return {}
|
118
|
+
res = {}
|
119
|
+
for item in value.split(','):
|
120
|
+
key, value = item.split('=', 1)
|
121
|
+
res[key] = value
|
122
|
+
return res
|
123
|
+
|
124
|
+
|
125
|
+
def parse_dictionary_items(items: Optional[List[str]]) -> Dict[str, Any]:
|
126
|
+
if items is None:
|
127
|
+
return {}
|
128
|
+
res = {}
|
129
|
+
for item in items:
|
130
|
+
key, value = item.split('=', 1)
|
131
|
+
res[key] = value
|
132
|
+
return res
|
133
|
+
|
134
|
+
|
115
135
|
class AliasGroup(typer.core.TyperGroup):
|
116
136
|
_CMD_SPLIT_P = re.compile(r', ?')
|
117
137
|
|
rbx/box/cli.py
CHANGED
@@ -42,6 +42,7 @@ from rbx.box.solutions import (
|
|
42
42
|
from rbx.box.statements import build_statements
|
43
43
|
from rbx.box.testcase_utils import TestcaseEntry
|
44
44
|
from rbx.box.testcases import main as testcases
|
45
|
+
from rbx.box.tooling import main as tooling
|
45
46
|
|
46
47
|
app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
|
47
48
|
app.add_typer(
|
@@ -60,7 +61,7 @@ app.add_typer(
|
|
60
61
|
)
|
61
62
|
app.add_typer(
|
62
63
|
download.app,
|
63
|
-
name='download',
|
64
|
+
name='download, down',
|
64
65
|
cls=annotations.AliasGroup,
|
65
66
|
help='Download an asset from supported repositories (sub-command).',
|
66
67
|
rich_help_panel='Management',
|
@@ -93,6 +94,13 @@ app.add_typer(
|
|
93
94
|
help='Manage testcases (sub-command).',
|
94
95
|
rich_help_panel='Management',
|
95
96
|
)
|
97
|
+
app.add_typer(
|
98
|
+
tooling.app,
|
99
|
+
name='tool, tooling',
|
100
|
+
cls=annotations.AliasGroup,
|
101
|
+
help='Manage tooling (sub-command).',
|
102
|
+
rich_help_panel='Misc',
|
103
|
+
)
|
96
104
|
|
97
105
|
|
98
106
|
@app.callback()
|
@@ -104,12 +112,11 @@ def main(
|
|
104
112
|
help='Whether to compile and run testlib components with sanitizers enabled. '
|
105
113
|
'If you want to run the solutions with sanitizers enabled, use the "-s" flag in the corresponding run command.',
|
106
114
|
),
|
107
|
-
|
108
|
-
|
109
|
-
'--
|
110
|
-
|
111
|
-
'
|
112
|
-
help='Whether to save extra debug logs along with the evaluation results.',
|
115
|
+
capture: bool = typer.Option(
|
116
|
+
True,
|
117
|
+
'--nocapture',
|
118
|
+
flag_value=False,
|
119
|
+
help='Whether to save extra logs and outputs from interactive solutions.',
|
113
120
|
),
|
114
121
|
):
|
115
122
|
if cd.is_problem_package() and not package.is_cache_valid():
|
@@ -125,7 +132,7 @@ def main(
|
|
125
132
|
'[warning]Sanitizers are running just for testlib components.\n'
|
126
133
|
'If you want to run the solutions with sanitizers enabled, use the [item]-s[/item] flag in the corresponding run command.[/warning]'
|
127
134
|
)
|
128
|
-
state.STATE.debug_logs =
|
135
|
+
state.STATE.debug_logs = capture
|
129
136
|
|
130
137
|
|
131
138
|
@app.command('ui', hidden=True)
|
@@ -136,6 +143,15 @@ def ui():
|
|
136
143
|
ui_pkg.start()
|
137
144
|
|
138
145
|
|
146
|
+
@app.command(
|
147
|
+
'on',
|
148
|
+
help='Run a command in the context of a problem (or a set of problems) of a contest.',
|
149
|
+
context_settings={'allow_extra_args': True, 'ignore_unknown_options': True},
|
150
|
+
)
|
151
|
+
def on(ctx: typer.Context, problems: str) -> None:
|
152
|
+
contest.on(ctx, problems)
|
153
|
+
|
154
|
+
|
139
155
|
@app.command('diff', hidden=True)
|
140
156
|
def diff(path1: pathlib.Path, path2: pathlib.Path):
|
141
157
|
from rbx.box.ui import main as ui_pkg
|
rbx/box/code.py
CHANGED
@@ -14,6 +14,7 @@ import typer
|
|
14
14
|
from rbx import console
|
15
15
|
from rbx.box import download, package, setter_config, state
|
16
16
|
from rbx.box.environment import (
|
17
|
+
CompilationConfig,
|
17
18
|
ExecutionConfig,
|
18
19
|
FileMapping,
|
19
20
|
get_compilation_config,
|
@@ -29,7 +30,7 @@ from rbx.box.formatting import get_formatted_memory
|
|
29
30
|
from rbx.box.sanitizers import warning_stack
|
30
31
|
from rbx.box.schema import CodeItem
|
31
32
|
from rbx.grading import steps, steps_with_caching
|
32
|
-
from rbx.grading.judge.sandbox import SandboxParams
|
33
|
+
from rbx.grading.judge.sandbox import SandboxBase, SandboxParams
|
33
34
|
from rbx.grading.steps import (
|
34
35
|
DigestHolder,
|
35
36
|
DigestOrDest,
|
@@ -39,7 +40,10 @@ from rbx.grading.steps import (
|
|
39
40
|
GradingFileOutput,
|
40
41
|
RunLog,
|
41
42
|
RunLogMetadata,
|
43
|
+
get_exe_from_command,
|
44
|
+
is_cpp_command,
|
42
45
|
is_cxx_command,
|
46
|
+
maybe_get_bits_stdcpp_for_commands,
|
43
47
|
)
|
44
48
|
|
45
49
|
|
@@ -391,12 +395,109 @@ def _prepare_run(
|
|
391
395
|
)
|
392
396
|
|
393
397
|
|
398
|
+
def _should_precompile(commands: List[str]) -> bool:
|
399
|
+
return any(is_cpp_command(command) for command in commands)
|
400
|
+
|
401
|
+
|
402
|
+
def _precompile_header(
|
403
|
+
compilation_options: CompilationConfig,
|
404
|
+
sanitized: SanitizationLevel,
|
405
|
+
sandbox: SandboxBase,
|
406
|
+
sandbox_params: SandboxParams,
|
407
|
+
artifacts: GradingArtifacts,
|
408
|
+
input_artifact: GradingFileInput,
|
409
|
+
force_warnings: bool = False,
|
410
|
+
verbose: bool = False,
|
411
|
+
) -> GradingFileInput:
|
412
|
+
"""
|
413
|
+
Precompile a header file (.h).
|
414
|
+
|
415
|
+
Assumes input artifact is a header file (.h) and compilation commands are C++.
|
416
|
+
"""
|
417
|
+
assert compilation_options.commands is not None
|
418
|
+
|
419
|
+
dependency_cache = package.get_dependency_cache()
|
420
|
+
|
421
|
+
# TODO: deduplicate code with compile_item.
|
422
|
+
commands = get_mapped_commands(
|
423
|
+
compilation_options.commands,
|
424
|
+
FileMapping(
|
425
|
+
compilable='precompilable.h',
|
426
|
+
executable='precompilable.h.gch',
|
427
|
+
),
|
428
|
+
)
|
429
|
+
commands = add_warning_flags(commands, force_warnings)
|
430
|
+
commands = substitute_commands(commands, sanitized=sanitized.should_sanitize())
|
431
|
+
|
432
|
+
if sanitized.should_sanitize():
|
433
|
+
commands = add_sanitizer_flags(commands)
|
434
|
+
|
435
|
+
precompilation_artifacts = GradingArtifacts()
|
436
|
+
|
437
|
+
# Keep only header files.
|
438
|
+
precompilation_artifacts.inputs = [
|
439
|
+
input
|
440
|
+
for input in artifacts.inputs
|
441
|
+
if input.src is not None and input.src.suffix == '.h'
|
442
|
+
]
|
443
|
+
precompilation_artifacts.inputs.append(
|
444
|
+
GradingFileInput(
|
445
|
+
src=input_artifact.src,
|
446
|
+
dest=PosixPath('precompilable.h'),
|
447
|
+
)
|
448
|
+
)
|
449
|
+
|
450
|
+
# Pull only the precompiled header file.
|
451
|
+
precompiled_digest = DigestHolder()
|
452
|
+
precompilation_artifacts.outputs.append(
|
453
|
+
GradingFileOutput(
|
454
|
+
src=PosixPath('precompilable.h.gch'),
|
455
|
+
digest=precompiled_digest,
|
456
|
+
executable=True,
|
457
|
+
)
|
458
|
+
)
|
459
|
+
|
460
|
+
if not steps_with_caching.compile(
|
461
|
+
commands,
|
462
|
+
params=sandbox_params,
|
463
|
+
artifacts=precompilation_artifacts,
|
464
|
+
sandbox=sandbox,
|
465
|
+
dependency_cache=dependency_cache,
|
466
|
+
):
|
467
|
+
console.console.print(
|
468
|
+
f'[error]Failed to precompile header file: [item]{input_artifact.src}[/item][/error]'
|
469
|
+
)
|
470
|
+
raise typer.Exit(1)
|
471
|
+
|
472
|
+
if verbose:
|
473
|
+
console.console.print(
|
474
|
+
f'[status]Precompiled header file: [item]{input_artifact.src}[/item]'
|
475
|
+
)
|
476
|
+
|
477
|
+
if (
|
478
|
+
precompilation_artifacts.logs is not None
|
479
|
+
and precompilation_artifacts.logs.preprocess is not None
|
480
|
+
):
|
481
|
+
for log in precompilation_artifacts.logs.preprocess:
|
482
|
+
console.console.print(f'[status]Command:[/status] {log.get_command()}')
|
483
|
+
console.console.print(f'[status]Summary:[/status] {log.get_summary()}')
|
484
|
+
|
485
|
+
assert precompiled_digest.value is not None
|
486
|
+
|
487
|
+
return GradingFileInput(
|
488
|
+
digest=precompiled_digest,
|
489
|
+
dest=input_artifact.dest.with_suffix('.h.gch'),
|
490
|
+
executable=True,
|
491
|
+
)
|
492
|
+
|
493
|
+
|
394
494
|
# Compile code item and return its digest in the storage.
|
395
495
|
def compile_item(
|
396
496
|
code: CodeItem,
|
397
497
|
sanitized: SanitizationLevel = SanitizationLevel.PREFER,
|
398
498
|
force_warnings: bool = False,
|
399
499
|
verbose: bool = False,
|
500
|
+
precompile: bool = True,
|
400
501
|
) -> str:
|
401
502
|
_check_stack_limit()
|
402
503
|
|
@@ -461,6 +562,41 @@ def compile_item(
|
|
461
562
|
for input in artifacts.inputs:
|
462
563
|
_ignore_warning_in_cxx_input(input)
|
463
564
|
|
565
|
+
# Add system bits/stdc++.h to the compilation.
|
566
|
+
bits_artifact = maybe_get_bits_stdcpp_for_commands(commands)
|
567
|
+
if bits_artifact is not None:
|
568
|
+
artifacts.inputs.append(bits_artifact)
|
569
|
+
commands = [
|
570
|
+
command + ' -I.'
|
571
|
+
for command in commands
|
572
|
+
if is_cxx_command(get_exe_from_command(command))
|
573
|
+
]
|
574
|
+
|
575
|
+
# Precompile C++ header files.
|
576
|
+
if precompile and _should_precompile(commands):
|
577
|
+
precompilation_inputs = []
|
578
|
+
for input in artifacts.inputs:
|
579
|
+
if (
|
580
|
+
input.src is not None
|
581
|
+
and input.src.suffix == '.h'
|
582
|
+
and input.dest.suffix == '.h'
|
583
|
+
):
|
584
|
+
precompilation_inputs.append(
|
585
|
+
_precompile_header(
|
586
|
+
compilation_options,
|
587
|
+
sanitized,
|
588
|
+
sandbox,
|
589
|
+
sandbox_params,
|
590
|
+
artifacts,
|
591
|
+
input,
|
592
|
+
force_warnings,
|
593
|
+
verbose=False,
|
594
|
+
)
|
595
|
+
)
|
596
|
+
if precompilation_inputs:
|
597
|
+
artifacts.inputs.extend(precompilation_inputs)
|
598
|
+
|
599
|
+
# Compile the code.
|
464
600
|
if not steps_with_caching.compile(
|
465
601
|
commands,
|
466
602
|
params=sandbox_params,
|
@@ -473,6 +609,7 @@ def compile_item(
|
|
473
609
|
assert compiled_digest.value is not None
|
474
610
|
|
475
611
|
if verbose and artifacts.logs is not None and artifacts.logs.preprocess is not None:
|
612
|
+
console.console.print(f'[status]Compiled item: [item]{code.path}[/item]')
|
476
613
|
for log in artifacts.logs.preprocess:
|
477
614
|
console.console.print(f'[status]Command:[/status] {log.get_command()}')
|
478
615
|
console.console.print(f'[status]Summary:[/status] {log.get_summary()}')
|
@@ -589,8 +726,8 @@ async def run_communication(
|
|
589
726
|
interactor_prepared.metadata.retryIndex = retry_index
|
590
727
|
solution_prepared.metadata.retryIndex = retry_index
|
591
728
|
|
592
|
-
interactor_prefix = '
|
593
|
-
solution_prefix = '
|
729
|
+
interactor_prefix = '<'
|
730
|
+
solution_prefix = '>'
|
594
731
|
|
595
732
|
if merged_capture is not None:
|
596
733
|
package.get_merged_capture_path().write_text(
|
@@ -2,7 +2,7 @@ import dataclasses
|
|
2
2
|
import pathlib
|
3
3
|
import tempfile
|
4
4
|
import typing
|
5
|
-
from typing import List, Optional, Tuple
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple
|
6
6
|
|
7
7
|
import typer
|
8
8
|
|
@@ -85,36 +85,41 @@ def get_statement_builder_contest(
|
|
85
85
|
|
86
86
|
def get_problems_for_statement(
|
87
87
|
contest: Contest,
|
88
|
-
|
88
|
+
contest_statement: ContestStatement,
|
89
89
|
requires_matching_statement: bool = True,
|
90
90
|
) -> List[ExtractedProblem]:
|
91
91
|
pkgs = get_problems(contest)
|
92
|
-
if not pkgs:
|
92
|
+
if not pkgs and not requires_matching_statement:
|
93
93
|
console.console.print(
|
94
94
|
'[error]No problems found in the contest, cannot infer statement type.[/error]'
|
95
95
|
)
|
96
96
|
raise typer.Exit(1)
|
97
97
|
|
98
|
+
def matches(statement: Statement) -> bool:
|
99
|
+
if not requires_matching_statement:
|
100
|
+
return True
|
101
|
+
if contest_statement.match is None:
|
102
|
+
return statement.language == contest_statement.language
|
103
|
+
return statement.name == contest_statement.match
|
104
|
+
|
98
105
|
res = []
|
99
106
|
for pkg, problem in zip(pkgs, contest.problems):
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
res.append(
|
105
|
-
ExtractedProblem(
|
106
|
-
package=pkg,
|
107
|
-
statement=statement,
|
108
|
-
problem=problem,
|
109
|
-
samples=_get_samples(problem),
|
110
|
-
)
|
111
|
-
)
|
112
|
-
break
|
113
|
-
if not found:
|
107
|
+
matching_statements = [
|
108
|
+
statement for statement in pkg.expanded_statements if matches(statement)
|
109
|
+
]
|
110
|
+
if not matching_statements:
|
114
111
|
console.console.print(
|
115
|
-
f'[error]No statement found for language {language} in problem {problem.short_name}[/error]'
|
112
|
+
f'[error]No statement found for language {contest_statement.language} in problem {problem.short_name}[/error]'
|
116
113
|
)
|
117
114
|
raise typer.Exit(1)
|
115
|
+
res.append(
|
116
|
+
ExtractedProblem(
|
117
|
+
package=pkg,
|
118
|
+
statement=matching_statements[0],
|
119
|
+
problem=problem,
|
120
|
+
samples=_get_samples(problem),
|
121
|
+
)
|
122
|
+
)
|
118
123
|
|
119
124
|
return res
|
120
125
|
|
@@ -146,14 +151,17 @@ def _build_problem_statements(
|
|
146
151
|
root: pathlib.Path,
|
147
152
|
output_type: StatementType,
|
148
153
|
use_samples: bool = True,
|
149
|
-
|
154
|
+
custom_vars: Optional[Dict[str, Any]] = None,
|
150
155
|
) -> List[ExtractedProblem]:
|
151
156
|
console.console.print('Building problem-level statements...')
|
152
|
-
extracted_problems = get_problems_for_statement(contest, statement
|
157
|
+
extracted_problems = get_problems_for_statement(contest, statement)
|
153
158
|
res = []
|
154
159
|
contest_cwd_absolute = pathlib.Path().resolve()
|
155
160
|
contest_assets = get_relative_assets(statement.path, statement.assets)
|
156
161
|
|
162
|
+
extra_vars = dict(statement.override.vars if statement.override is not None else {})
|
163
|
+
extra_vars.update(custom_vars or {})
|
164
|
+
|
157
165
|
for extracted_problem in extracted_problems:
|
158
166
|
console.console.print(
|
159
167
|
f'Building statement for problem {extracted_problem.problem.short_name}...'
|
@@ -174,7 +182,8 @@ def _build_problem_statements(
|
|
174
182
|
overridden_assets=contest_assets, # overridden assets
|
175
183
|
overridden_params_root=contest_cwd_absolute,
|
176
184
|
use_samples=use_samples,
|
177
|
-
|
185
|
+
# Use custom var overriding and problem-level overriding.
|
186
|
+
custom_vars=extra_vars,
|
178
187
|
)
|
179
188
|
dest_dir = root / '.problems' / extracted_problem.problem.short_name
|
180
189
|
dest_path = dest_dir / f'statement{output_type.get_file_suffix()}'
|
@@ -201,7 +210,7 @@ def build_contest_only(
|
|
201
210
|
input: bytes,
|
202
211
|
input_type: StatementType,
|
203
212
|
output_type: Optional[StatementType] = None,
|
204
|
-
|
213
|
+
custom_vars: Optional[Dict[str, Any]] = None,
|
205
214
|
) -> Tuple[bytes, StatementType]:
|
206
215
|
console.console.print('Building contest-level statement.')
|
207
216
|
bdrs = get_builders(
|
@@ -224,10 +233,11 @@ def build_contest_only(
|
|
224
233
|
output = bdr.build(
|
225
234
|
input=last_content,
|
226
235
|
context=StatementBuilderContext(
|
236
|
+
lang=statement.language,
|
227
237
|
languages=get_environment_languages_for_statement(),
|
228
238
|
params=params,
|
229
239
|
root=pathlib.Path(td),
|
230
|
-
|
240
|
+
custom_vars=custom_vars,
|
231
241
|
vars={**contest.expanded_vars, **statement.expanded_vars},
|
232
242
|
),
|
233
243
|
item=get_statement_builder_contest(statement, extracted_problems),
|
@@ -245,7 +255,7 @@ def build_statement_rooted(
|
|
245
255
|
root: pathlib.Path,
|
246
256
|
output_type: Optional[StatementType] = None,
|
247
257
|
use_samples: bool = True,
|
248
|
-
|
258
|
+
custom_vars: Optional[Dict[str, Any]] = None,
|
249
259
|
) -> Tuple[bytes, StatementType]:
|
250
260
|
# Validate.
|
251
261
|
if not statement.path.is_file():
|
@@ -257,7 +267,7 @@ def build_statement_rooted(
|
|
257
267
|
if statement.joiner is None:
|
258
268
|
joiner = None
|
259
269
|
extracted_problems = get_problems_for_statement(
|
260
|
-
contest, statement
|
270
|
+
contest, statement, requires_matching_statement=False
|
261
271
|
)
|
262
272
|
else:
|
263
273
|
# Build problem-level statements.
|
@@ -268,7 +278,7 @@ def build_statement_rooted(
|
|
268
278
|
root,
|
269
279
|
output_type=joiner.joined_type(),
|
270
280
|
use_samples=use_samples,
|
271
|
-
|
281
|
+
custom_vars=custom_vars,
|
272
282
|
)
|
273
283
|
|
274
284
|
# Build contest-level statement into joiner input type.
|
@@ -279,7 +289,7 @@ def build_statement_rooted(
|
|
279
289
|
statement.path.read_bytes(),
|
280
290
|
statement.type,
|
281
291
|
output_type=joiner.joined_type() if joiner is not None else output_type,
|
282
|
-
|
292
|
+
custom_vars=custom_vars,
|
283
293
|
)
|
284
294
|
|
285
295
|
if joiner is None:
|
@@ -313,7 +323,7 @@ def build_statement_rooted(
|
|
313
323
|
last_content,
|
314
324
|
last_output,
|
315
325
|
output_type=output_type,
|
316
|
-
|
326
|
+
custom_vars=custom_vars,
|
317
327
|
)
|
318
328
|
|
319
329
|
return last_content, last_output
|
@@ -324,7 +334,7 @@ def build_statement(
|
|
324
334
|
contest: Contest,
|
325
335
|
output_type: Optional[StatementType] = None,
|
326
336
|
use_samples: bool = True,
|
327
|
-
|
337
|
+
custom_vars: Optional[Dict[str, Any]] = None,
|
328
338
|
) -> pathlib.Path:
|
329
339
|
with tempfile.TemporaryDirectory() as td:
|
330
340
|
root = pathlib.Path(td)
|
@@ -334,17 +344,17 @@ def build_statement(
|
|
334
344
|
root,
|
335
345
|
output_type=output_type,
|
336
346
|
use_samples=use_samples,
|
337
|
-
|
347
|
+
custom_vars=custom_vars,
|
338
348
|
)
|
339
349
|
|
340
|
-
statement_path = pathlib.Path(
|
341
|
-
|
350
|
+
statement_path = (pathlib.Path('build') / statement.name).with_suffix(
|
351
|
+
last_output.get_file_suffix()
|
342
352
|
)
|
343
353
|
statement_path.parent.mkdir(parents=True, exist_ok=True)
|
344
354
|
statement_path.write_bytes(typing.cast(bytes, last_content))
|
345
355
|
console.console.print(
|
346
|
-
f'Statement built successfully for language '
|
356
|
+
f'[success]Statement [item]{statement.name}[/item] built successfully for language '
|
347
357
|
f'[item]{statement.language}[/item] at '
|
348
|
-
f'{href(statement_path)}'
|
358
|
+
f'{href(statement_path)}[/success]'
|
349
359
|
)
|
350
360
|
return statement_path
|
rbx/box/contest/contest_utils.py
CHANGED
@@ -1,5 +1,30 @@
|
|
1
|
+
from typing import List
|
2
|
+
|
1
3
|
from rbx.box import environment, package
|
2
4
|
from rbx.box.contest import contest_package
|
5
|
+
from rbx.box.contest.schema import ContestProblem
|
6
|
+
|
7
|
+
|
8
|
+
def match_problem(problems: str, contest_problem: ContestProblem) -> bool:
|
9
|
+
short_name = contest_problem.short_name.lower()
|
10
|
+
problems = problems.lower()
|
11
|
+
if problems == '*':
|
12
|
+
return True
|
13
|
+
if '-' in problems:
|
14
|
+
start, end = problems.split('-')
|
15
|
+
return start <= short_name <= end
|
16
|
+
problem_set = set(p.strip().lower() for p in problems.split(','))
|
17
|
+
return short_name in problem_set
|
18
|
+
|
19
|
+
|
20
|
+
def get_problems_of_interest(problems: str) -> List[ContestProblem]:
|
21
|
+
contest = contest_package.find_contest_package_or_die()
|
22
|
+
problems_of_interest = []
|
23
|
+
|
24
|
+
for p in contest.problems:
|
25
|
+
if match_problem(problems, p):
|
26
|
+
problems_of_interest.append(p)
|
27
|
+
return problems_of_interest
|
3
28
|
|
4
29
|
|
5
30
|
def clear_all_caches():
|
rbx/box/contest/main.py
CHANGED
@@ -232,3 +232,27 @@ def each(ctx: typer.Context) -> None:
|
|
232
232
|
console.console.print(
|
233
233
|
'[error]One of the commands above failed. Check the output![/error]'
|
234
234
|
)
|
235
|
+
|
236
|
+
|
237
|
+
@app.command(
|
238
|
+
'on',
|
239
|
+
help='Run a command in the problem (or in a set of problems) of a context.',
|
240
|
+
context_settings={'allow_extra_args': True, 'ignore_unknown_options': True},
|
241
|
+
)
|
242
|
+
@within_contest
|
243
|
+
def on(ctx: typer.Context, problems: str) -> None:
|
244
|
+
command = ' '.join(['rbx'] + ctx.args)
|
245
|
+
problems_of_interest = contest_utils.get_problems_of_interest(problems)
|
246
|
+
|
247
|
+
if not problems_of_interest:
|
248
|
+
console.console.print(
|
249
|
+
f'[error]No problems found in contest matching [item]{problems}[/item].[/error]'
|
250
|
+
)
|
251
|
+
raise typer.Exit(1)
|
252
|
+
|
253
|
+
for p in problems_of_interest:
|
254
|
+
console.console.print(
|
255
|
+
f'[status]Running [item]{command}[/item] for [item]{p.short_name}[/item]...[/status]'
|
256
|
+
)
|
257
|
+
subprocess.call(command, cwd=p.get_path(), shell=True)
|
258
|
+
console.console.print()
|
rbx/box/contest/schema.py
CHANGED
@@ -1,12 +1,15 @@
|
|
1
1
|
import pathlib
|
2
|
-
from typing import Dict, List, Optional
|
2
|
+
from typing import Annotated, Dict, List, Optional
|
3
3
|
|
4
|
-
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
4
|
+
from pydantic import AfterValidator, BaseModel, ConfigDict, Field, model_validator
|
5
5
|
|
6
|
-
from rbx.box.
|
6
|
+
from rbx.box.fields import FNameField, NameField
|
7
|
+
from rbx.box.schema import Primitive, expand_var
|
8
|
+
from rbx.box.statements.expander import expand_statements
|
7
9
|
from rbx.box.statements.schema import (
|
8
10
|
ConversionStep,
|
9
11
|
Joiner,
|
12
|
+
StatementLanguage,
|
10
13
|
StatementType,
|
11
14
|
)
|
12
15
|
|
@@ -15,6 +18,13 @@ def ShortNameField(**kwargs):
|
|
15
18
|
return Field(pattern=r'^[A-Z]+[0-9]*$', min_length=1, max_length=4, **kwargs)
|
16
19
|
|
17
20
|
|
21
|
+
def is_unique_by_name(statements: List['ContestStatement']) -> List['ContestStatement']:
|
22
|
+
names = {st.name for st in statements}
|
23
|
+
if len(names) != len(statements):
|
24
|
+
raise ValueError('Statement names must be unique.')
|
25
|
+
return statements
|
26
|
+
|
27
|
+
|
18
28
|
class ProblemStatementOverride(BaseModel):
|
19
29
|
model_config = ConfigDict(extra='forbid')
|
20
30
|
|
@@ -29,13 +39,26 @@ configure them in case they are applied.
|
|
29
39
|
""",
|
30
40
|
)
|
31
41
|
|
42
|
+
vars: Dict[str, Primitive] = Field(
|
43
|
+
default={},
|
44
|
+
description='Variables to be merged into the problem statement vars.',
|
45
|
+
)
|
46
|
+
|
32
47
|
|
33
48
|
class ContestStatement(BaseModel):
|
34
49
|
model_config = ConfigDict(extra='forbid')
|
35
50
|
|
36
|
-
|
51
|
+
name: str = FNameField(description='Name of this statement.')
|
52
|
+
|
53
|
+
extends: Optional[str] = FNameField(
|
54
|
+
default=None, description='Name of the statement to inherit from.'
|
55
|
+
)
|
56
|
+
|
57
|
+
language: StatementLanguage = Field(
|
58
|
+
default='en', description='Language code for this statement (ISO 639-1).'
|
59
|
+
)
|
37
60
|
|
38
|
-
title: str = Field(description='Title of the contest in this language.')
|
61
|
+
title: str = Field(default='', description='Title of the contest in this language.')
|
39
62
|
|
40
63
|
location: Optional[str] = Field(
|
41
64
|
default=None, description='Location of the contest in this language.'
|
@@ -45,9 +68,14 @@ class ContestStatement(BaseModel):
|
|
45
68
|
default=None, description='Date of the contest in this language.'
|
46
69
|
)
|
47
70
|
|
48
|
-
path: pathlib.Path = Field(
|
71
|
+
path: pathlib.Path = Field(
|
72
|
+
default_factory=pathlib.Path,
|
73
|
+
description='Path to the input statement file.',
|
74
|
+
)
|
49
75
|
|
50
|
-
type: StatementType = Field(
|
76
|
+
type: StatementType = Field(
|
77
|
+
default=StatementType.rbxTeX, description='Type of the input statement file.'
|
78
|
+
)
|
51
79
|
|
52
80
|
joiner: Optional[Joiner] = Field(
|
53
81
|
default=None,
|
@@ -95,6 +123,15 @@ Can be glob pattern as well, such as `imgs/*.png`.
|
|
95
123
|
default=None, description='Override configuration for problem statements.'
|
96
124
|
)
|
97
125
|
|
126
|
+
match: Optional[str] = FNameField(
|
127
|
+
default=None,
|
128
|
+
description="""
|
129
|
+
Name of the problem-level statement to match this statement against.
|
130
|
+
|
131
|
+
If not specified, will match against the first statement of the same language.
|
132
|
+
""",
|
133
|
+
)
|
134
|
+
|
98
135
|
# Vars to be re-used in the statement.
|
99
136
|
# - It will be available as \VAR{vars} variable in the contest-level box statement.
|
100
137
|
vars: Dict[str, Primitive] = Field(
|
@@ -188,7 +225,10 @@ class Contest(BaseModel):
|
|
188
225
|
default=[], description='List of problems in this contest.'
|
189
226
|
)
|
190
227
|
|
191
|
-
statements:
|
228
|
+
statements: Annotated[
|
229
|
+
List[ContestStatement],
|
230
|
+
AfterValidator(is_unique_by_name),
|
231
|
+
] = Field(
|
192
232
|
default=None,
|
193
233
|
description='Configure statements in this contest, per language.',
|
194
234
|
)
|
@@ -199,6 +239,10 @@ class Contest(BaseModel):
|
|
199
239
|
default={}, description='Variables to be re-used across the package.'
|
200
240
|
)
|
201
241
|
|
242
|
+
@property
|
243
|
+
def expanded_statements(self) -> List[ContestStatement]:
|
244
|
+
return expand_statements(self.statements)
|
245
|
+
|
202
246
|
@property
|
203
247
|
def expanded_vars(self) -> Dict[str, Primitive]:
|
204
248
|
return {key: expand_var(value) for key, value in self.vars.items()}
|