rbx.cp 0.13.8__py3-none-any.whl → 0.14.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.
Files changed (71) hide show
  1. rbx/box/cli.py +74 -70
  2. rbx/box/code.py +3 -0
  3. rbx/box/contest/build_contest_statements.py +65 -23
  4. rbx/box/contest/contest_package.py +8 -1
  5. rbx/box/contest/main.py +9 -3
  6. rbx/box/contest/schema.py +17 -13
  7. rbx/box/contest/statements.py +12 -8
  8. rbx/box/dump_schemas.py +2 -1
  9. rbx/box/environment.py +1 -1
  10. rbx/box/fields.py +22 -4
  11. rbx/box/generators.py +32 -13
  12. rbx/box/limits_info.py +161 -0
  13. rbx/box/package.py +18 -1
  14. rbx/box/packaging/boca/boca_language_utils.py +26 -0
  15. rbx/box/packaging/boca/boca_outcome_utils.py +10 -0
  16. rbx/box/packaging/boca/packager.py +7 -5
  17. rbx/box/packaging/contest_main.py +20 -12
  18. rbx/box/packaging/packager.py +24 -14
  19. rbx/box/packaging/polygon/packager.py +7 -3
  20. rbx/box/packaging/polygon/upload.py +2 -1
  21. rbx/box/presets/__init__.py +64 -64
  22. rbx/box/remote.py +3 -3
  23. rbx/box/sanitizers/issue_stack.py +124 -0
  24. rbx/box/schema.py +87 -27
  25. rbx/box/solutions.py +74 -117
  26. rbx/box/statements/build_statements.py +12 -1
  27. rbx/box/statements/builders.py +5 -3
  28. rbx/box/statements/latex_jinja.py +73 -23
  29. rbx/box/statements/schema.py +7 -9
  30. rbx/box/stressing/generator_parser.py +3 -1
  31. rbx/box/tasks.py +10 -10
  32. rbx/box/testcase_extractors.py +8 -0
  33. rbx/box/testing/testing_preset.py +129 -2
  34. rbx/box/testing/testing_shared.py +3 -1
  35. rbx/box/timing.py +305 -0
  36. rbx/box/tooling/boca/debug_utils.py +88 -0
  37. rbx/box/tooling/boca/manual_scrape.py +20 -0
  38. rbx/box/tooling/boca/scraper.py +660 -57
  39. rbx/box/unit.py +0 -2
  40. rbx/box/validators.py +0 -4
  41. rbx/grading/judge/cacher.py +36 -0
  42. rbx/grading/judge/program.py +12 -2
  43. rbx/grading/judge/sandbox.py +1 -1
  44. rbx/grading/judge/sandboxes/stupid_sandbox.py +2 -1
  45. rbx/grading/judge/storage.py +36 -3
  46. rbx/grading/limits.py +4 -0
  47. rbx/grading/steps.py +3 -2
  48. rbx/resources/presets/default/contest/contest.rbx.yml +7 -1
  49. rbx/resources/presets/default/contest/statement/info.rbx.tex +54 -0
  50. rbx/resources/presets/default/problem/.gitignore +1 -0
  51. rbx/resources/presets/default/problem/problem.rbx.yml +19 -3
  52. rbx/resources/presets/default/problem/rbx.h +52 -5
  53. rbx/resources/presets/default/problem/statement/statement.rbx.tex +6 -2
  54. rbx/resources/presets/default/problem/testlib.h +6299 -0
  55. rbx/resources/presets/default/problem/validator.cpp +4 -3
  56. rbx/resources/presets/default/shared/contest_template.rbx.tex +8 -4
  57. rbx/resources/presets/default/shared/icpc.sty +16 -1
  58. rbx/resources/presets/default/shared/problem_template.rbx.tex +4 -1
  59. rbx/testing_utils.py +17 -1
  60. {rbx_cp-0.13.8.dist-info → rbx_cp-0.14.0.dist-info}/METADATA +4 -2
  61. {rbx_cp-0.13.8.dist-info → rbx_cp-0.14.0.dist-info}/RECORD +65 -62
  62. {rbx_cp-0.13.8.dist-info → rbx_cp-0.14.0.dist-info}/WHEEL +1 -1
  63. {rbx_cp-0.13.8.dist-info → rbx_cp-0.14.0.dist-info}/entry_points.txt +0 -1
  64. rbx/providers/__init__.py +0 -43
  65. rbx/providers/codeforces.py +0 -73
  66. rbx/providers/provider.py +0 -26
  67. rbx/submitors/__init__.py +0 -18
  68. rbx/submitors/codeforces.py +0 -121
  69. rbx/submitors/submitor.py +0 -25
  70. /rbx/resources/presets/default/problem/sols/{wa.cpp → wa-overflow.cpp} +0 -0
  71. {rbx_cp-0.13.8.dist-info → rbx_cp-0.14.0.dist-info}/LICENSE +0 -0
rbx/box/cli.py CHANGED
@@ -25,6 +25,7 @@ from rbx.box import (
25
25
  presets,
26
26
  setter_config,
27
27
  state,
28
+ timing,
28
29
  validators,
29
30
  )
30
31
  from rbx.box.contest import main as contest
@@ -34,7 +35,6 @@ from rbx.box.header import generate_header
34
35
  from rbx.box.packaging import main as packaging
35
36
  from rbx.box.schema import CodeItem, ExpectedOutcome, TestcaseGroup
36
37
  from rbx.box.solutions import (
37
- estimate_time_limit,
38
38
  get_exact_matching_solutions,
39
39
  get_matching_solutions,
40
40
  pick_solutions,
@@ -107,6 +107,16 @@ app.add_typer(
107
107
  )
108
108
 
109
109
 
110
+ def version_callback(value: bool) -> None:
111
+ if value:
112
+ import importlib.metadata
113
+
114
+ version = importlib.metadata.version('rbx.cp')
115
+
116
+ console.console.print(f'rbx version {version}')
117
+ raise typer.Exit()
118
+
119
+
110
120
  @app.callback()
111
121
  def main(
112
122
  cache: Annotated[
@@ -136,6 +146,9 @@ def main(
136
146
  '-p',
137
147
  help='Whether to profile the execution.',
138
148
  ),
149
+ version: Annotated[
150
+ bool, typer.Option('--version', '-v', callback=version_callback, is_eager=True)
151
+ ] = False,
139
152
  ):
140
153
  if cd.is_problem_package() and not package.is_cache_valid():
141
154
  console.console.print(
@@ -266,12 +279,6 @@ async def run(
266
279
  '-d',
267
280
  help='Whether to print a detailed view of the tests using tables.',
268
281
  ),
269
- timeit: bool = typer.Option(
270
- False,
271
- '--time',
272
- '-t',
273
- help='Whether to use estimate a time limit based on accepted solutions.',
274
- ),
275
282
  sanitized: bool = typer.Option(
276
283
  False,
277
284
  '--sanitized',
@@ -324,20 +331,6 @@ async def run(
324
331
  )
325
332
  return
326
333
 
327
- override_tl = None
328
- if timeit:
329
- if sanitized:
330
- console.console.print(
331
- '[error]Sanitizers are known to be time-hungry, so they cannot be used for time estimation.\n'
332
- 'Remove either the [item]-s[/item] flag or the [item]-t[/item] flag to run solutions without sanitizers.[/error]'
333
- )
334
- raise typer.Exit(1)
335
-
336
- # Never use sanitizers for time estimation.
337
- override_tl = await _time_impl(check=check, detailed=False)
338
- if override_tl is None:
339
- raise typer.Exit(1)
340
-
341
334
  if sanitized:
342
335
  console.console.print(
343
336
  '[warning]Sanitizers are running, so the time limit for the problem will be dropped, '
@@ -359,7 +352,6 @@ async def run(
359
352
  tracked_solutions=tracked_solutions,
360
353
  check=check,
361
354
  verification=VerificationLevel(verification),
362
- timelimit_override=override_tl,
363
355
  sanitized=sanitized,
364
356
  )
365
357
 
@@ -374,51 +366,6 @@ async def run(
374
366
  )
375
367
 
376
368
 
377
- async def _time_impl(check: bool, detailed: bool, runs: int = 0) -> Optional[int]:
378
- if package.get_main_solution() is None:
379
- console.console.print(
380
- '[warning]No main solution found, so cannot estimate a time limit.[/warning]'
381
- )
382
- return None
383
-
384
- verification = VerificationLevel.ALL_SOLUTIONS.value
385
-
386
- with utils.StatusProgress('Running ACCEPTED solutions...') as s:
387
- tracked_solutions = OrderedSet(
388
- str(solution.path)
389
- for solution in get_exact_matching_solutions(ExpectedOutcome.ACCEPTED)
390
- )
391
- solution_result = run_solutions(
392
- progress=s,
393
- tracked_solutions=tracked_solutions,
394
- check=check,
395
- verification=VerificationLevel(verification),
396
- timelimit_override=-1, # Unlimited for time limit estimation
397
- nruns=runs,
398
- )
399
-
400
- console.console.print()
401
- console.console.rule(
402
- '[status]Run report (for time estimation)[/status]', style='status'
403
- )
404
- ok = await print_run_report(
405
- solution_result,
406
- console.console,
407
- VerificationLevel(verification),
408
- detailed=detailed,
409
- skip_printing_limits=True,
410
- )
411
-
412
- if not ok:
413
- console.console.print(
414
- '[error]Failed to run ACCEPTED solutions, so cannot estimate a reliable time limit.[/error]'
415
- )
416
- return None
417
-
418
- console.console.print()
419
- return await estimate_time_limit(console.console, solution_result)
420
-
421
-
422
369
  @app.command(
423
370
  'time, t',
424
371
  rich_help_panel='Testing',
@@ -444,7 +391,61 @@ async def time(
444
391
  '-r',
445
392
  help='Number of runs to perform for each solution. Zero means the config default.',
446
393
  ),
394
+ profile: str = typer.Option(
395
+ 'local',
396
+ '--profile',
397
+ '-p',
398
+ help='Profile to use for time limit estimation.',
399
+ ),
400
+ integrate: bool = typer.Option(
401
+ False,
402
+ '--integrate',
403
+ '-i',
404
+ help='Integrate the given limits profile into the package.',
405
+ ),
447
406
  ):
407
+ if integrate:
408
+ timing.integrate(profile)
409
+ return
410
+
411
+ import questionary
412
+
413
+ formula = environment.get_environment().timing.formula
414
+ timing_choices = [
415
+ questionary.Choice(
416
+ f'Estimate time limits based on the formula {formula} (recommended)',
417
+ value='estimate',
418
+ ),
419
+ questionary.Choice('Inherit from the package.', value='inherit'),
420
+ questionary.Choice(
421
+ 'Estimate time limits based on a custom formula.', value='estimate_custom'
422
+ ),
423
+ questionary.Choice('Provide a custom time limit.', value='custom'),
424
+ ]
425
+
426
+ choice = await questionary.select(
427
+ 'Select how you want to define the time limits for the problem.',
428
+ choices=timing_choices,
429
+ ).ask_async()
430
+
431
+ formula = environment.get_environment().timing.formula
432
+
433
+ if choice == 'inherit':
434
+ timing.inherit_time_limits(profile=profile)
435
+ return
436
+ elif choice == 'custom':
437
+ timelimit = await questionary.text(
438
+ 'Enter a custom time limit for the problem (ms).',
439
+ validate=lambda x: x.isdigit() and int(x) > 0,
440
+ ).ask_async()
441
+ timing.set_time_limit(int(timelimit), profile=profile)
442
+ return
443
+
444
+ if choice == 'estimate_custom':
445
+ formula = await questionary.text(
446
+ 'Enter a custom formula for time limit estimation.'
447
+ ).ask_async()
448
+
448
449
  main_solution = package.get_main_solution()
449
450
  if check and main_solution is None:
450
451
  console.console.print(
@@ -458,7 +459,9 @@ async def time(
458
459
  if not await builder.build(verification=verification, output=check):
459
460
  return None
460
461
 
461
- await _time_impl(check, detailed, runs)
462
+ await timing.compute_time_limits(
463
+ check, detailed, runs, formula=formula, profile=profile
464
+ )
462
465
 
463
466
 
464
467
  @app.command(
@@ -850,11 +853,12 @@ async def validate(
850
853
  help='Run unit tests for the validator and checker.',
851
854
  )
852
855
  @package.within_problem
853
- def unit_tests():
856
+ @syncer.sync
857
+ async def unit_tests():
854
858
  from rbx.box import unit
855
859
 
856
860
  with utils.StatusProgress('Running unit tests...') as s:
857
- unit.run_unit_tests(s)
861
+ await unit.run_unit_tests(s)
858
862
 
859
863
 
860
864
  @app.command(
rbx/box/code.py CHANGED
@@ -3,6 +3,7 @@ import pathlib
3
3
  import re
4
4
  import resource
5
5
  import shlex
6
+ import sys
6
7
  from enum import Enum
7
8
  from pathlib import PosixPath
8
9
  from typing import List, Optional
@@ -214,6 +215,8 @@ def _check_stack_limit():
214
215
  return
215
216
  if not state.STATE.run_through_cli:
216
217
  return
218
+ if sys.platform != 'darwin':
219
+ return
217
220
  soft, hard = resource.RLIM_INFINITY, resource.RLIM_INFINITY
218
221
 
219
222
  TARGET = 256 * 1024 * 1024 # 256 MiB
@@ -7,12 +7,14 @@ from typing import Any, Dict, List, Optional, Tuple
7
7
  import typer
8
8
 
9
9
  from rbx import console, testing_utils, utils
10
- from rbx.box import cd, package
10
+ from rbx.box import cd, limits_info, package
11
11
  from rbx.box.contest.contest_package import get_problems
12
12
  from rbx.box.contest.schema import Contest, ContestProblem, ContestStatement
13
13
  from rbx.box.fields import Primitive
14
14
  from rbx.box.formatting import href
15
- from rbx.box.schema import Package, Testcase
15
+ from rbx.box.sanitizers import issue_stack
16
+ from rbx.box.sanitizers.issue_stack import Issue
17
+ from rbx.box.schema import LimitsProfile, Package, Testcase
16
18
  from rbx.box.statements import build_statements, latex
17
19
  from rbx.box.statements.build_statements import (
18
20
  get_builders,
@@ -41,6 +43,7 @@ class ExtractedProblem:
41
43
  package: Package
42
44
  statement: Statement
43
45
  problem: ContestProblem
46
+ limits: LimitsProfile
44
47
  samples: List[Testcase]
45
48
  built_statement: Optional[pathlib.Path] = None
46
49
 
@@ -52,6 +55,7 @@ class ExtractedProblem:
52
55
 
53
56
  def get_statement_builder_problem(self) -> StatementBuilderProblem:
54
57
  return StatementBuilderProblem(
58
+ limits=self.limits,
55
59
  package=self.package,
56
60
  statement=self.statement,
57
61
  samples=StatementSample.from_testcases(self.samples),
@@ -60,10 +64,28 @@ class ExtractedProblem:
60
64
  )
61
65
 
62
66
 
67
+ class StatementBuildIssue(Issue):
68
+ def __init__(self, problem: ContestProblem):
69
+ self.problem = problem
70
+
71
+ def get_overview_section(self) -> Optional[Tuple[str, ...]]:
72
+ return ('statement',)
73
+
74
+ def get_overview_message(self) -> str:
75
+ return f'Error building statement for problem [item]{self.problem.short_name}[/item].'
76
+
77
+
63
78
  def _get_samples(problem: ContestProblem) -> List[Testcase]:
64
79
  with cd.new_package_cd(problem.get_path()):
65
80
  package.clear_package_cache()
66
- return get_samples()
81
+ try:
82
+ return get_samples()
83
+ except Exception as e:
84
+ console.console.print(
85
+ f'[error]Error getting samples for problem {problem.short_name}: {e}[/error]'
86
+ )
87
+ issue_stack.add_issue(StatementBuildIssue(problem))
88
+ return []
67
89
 
68
90
 
69
91
  def get_statement_builder_problems(
@@ -94,12 +116,12 @@ def get_statement_builder_contest(
94
116
  def get_problems_for_statement(
95
117
  contest: Contest,
96
118
  contest_statement: ContestStatement,
97
- requires_matching_statement: bool = True,
119
+ requires_matching_statement: bool = False,
98
120
  ) -> List[ExtractedProblem]:
99
121
  pkgs = get_problems(contest)
100
- if not pkgs and not requires_matching_statement:
122
+ if not pkgs and requires_matching_statement:
101
123
  console.console.print(
102
- '[error]No problems found in the contest, cannot infer statement type.[/error]'
124
+ f'[error]No problems found in the contest, cannot infer statement type for statement [item]{contest_statement.name}[/item].[/error]'
103
125
  )
104
126
  raise typer.Exit(1)
105
127
 
@@ -122,6 +144,9 @@ def get_problems_for_statement(
122
144
  raise typer.Exit(1)
123
145
  res.append(
124
146
  ExtractedProblem(
147
+ limits=limits_info.get_limits_profile(
148
+ profile=limits_info.get_active_profile(), root=problem.get_path()
149
+ ),
125
150
  package=pkg,
126
151
  statement=matching_statements[0],
127
152
  problem=problem,
@@ -137,6 +162,7 @@ def get_builder_problems(
137
162
  ) -> List[StatementBuilderProblem]:
138
163
  return [
139
164
  StatementBuilderProblem(
165
+ limits=ex.limits,
140
166
  package=ex.package,
141
167
  statement=ex.statement,
142
168
  samples=StatementSample.from_testcases(ex.samples),
@@ -177,22 +203,29 @@ def _build_problem_statements(
177
203
  with cd.new_package_cd(extracted_problem.problem.get_path()):
178
204
  package.clear_package_cache()
179
205
  # TODO: respect steps override
180
- content, _ = build_statements.build_statement_bytes(
181
- extracted_problem.statement,
182
- extracted_problem.package,
183
- output_type=output_type,
184
- short_name=extracted_problem.problem.short_name,
185
- overridden_params={
186
- cfg.type: cfg for cfg in statement.override.configure
187
- }
188
- if statement.override is not None
189
- else {}, # overridden configure params
190
- overridden_assets=contest_assets, # overridden assets
191
- overridden_params_root=contest_cwd_absolute,
192
- use_samples=use_samples,
193
- # Use custom var overriding and problem-level overriding.
194
- custom_vars=extra_vars,
195
- )
206
+ try:
207
+ content, _ = build_statements.build_statement_bytes(
208
+ extracted_problem.statement,
209
+ extracted_problem.package,
210
+ output_type=output_type,
211
+ short_name=extracted_problem.problem.short_name,
212
+ overridden_params={
213
+ cfg.type: cfg for cfg in statement.override.configure
214
+ }
215
+ if statement.override is not None
216
+ else {}, # overridden configure params
217
+ overridden_assets=contest_assets, # overridden assets
218
+ overridden_params_root=contest_cwd_absolute,
219
+ use_samples=use_samples,
220
+ # Use custom var overriding and problem-level overriding.
221
+ custom_vars=extra_vars,
222
+ )
223
+ except Exception as e:
224
+ console.console.print(
225
+ f'[error]Error building statement for problem {extracted_problem.problem.short_name}: {e}[/error]'
226
+ )
227
+ issue_stack.add_issue(StatementBuildIssue(extracted_problem.problem))
228
+ continue
196
229
  dest_dir = root / '.problems' / extracted_problem.problem.short_name
197
230
  dest_path = dest_dir / f'statement{output_type.get_file_suffix()}'
198
231
  dest_dir.mkdir(parents=True, exist_ok=True)
@@ -292,7 +325,8 @@ def build_statement_rooted(
292
325
  if statement.joiner is None:
293
326
  joiner = None
294
327
  extracted_problems = get_problems_for_statement(
295
- contest, statement, requires_matching_statement=False
328
+ contest,
329
+ statement,
296
330
  )
297
331
  else:
298
332
  # Build problem-level statements.
@@ -380,6 +414,14 @@ def build_statement(
380
414
  statement_path = (pathlib.Path('build') / statement.name).with_suffix(
381
415
  last_output.get_file_suffix()
382
416
  )
417
+ active_profile = limits_info.get_active_profile()
418
+ if (
419
+ active_profile is not None
420
+ and limits_info.get_saved_limits_profile(active_profile) is not None
421
+ ):
422
+ statement_path = statement_path.with_stem(
423
+ f'{statement_path.stem}-{active_profile}'
424
+ )
383
425
  statement_path.parent.mkdir(parents=True, exist_ok=True)
384
426
  statement_path.write_bytes(typing.cast(bytes, last_content))
385
427
  console.console.print(
@@ -10,6 +10,7 @@ from rbx import console, utils
10
10
  from rbx.box import cd
11
11
  from rbx.box.contest.schema import Contest
12
12
  from rbx.box.package import find_problem_package_or_die
13
+ from rbx.box.sanitizers import issue_stack
13
14
  from rbx.box.schema import Package
14
15
 
15
16
  YAML_NAME = 'contest.rbx.yml'
@@ -60,7 +61,13 @@ def within_contest(func):
60
61
  @functools.wraps(func)
61
62
  def wrapper(*args, **kwargs):
62
63
  with cd.new_package_cd(find_contest()):
63
- return func(*args, **kwargs)
64
+ issue_level_token = issue_stack.issue_level_var.set(
65
+ issue_stack.IssueLevel.OVERVIEW
66
+ )
67
+ ret = func(*args, **kwargs)
68
+ issue_stack.print_current_report()
69
+ issue_stack.issue_level_var.reset(issue_level_token)
70
+ return ret
64
71
 
65
72
  return wrapper
66
73
 
rbx/box/contest/main.py CHANGED
@@ -148,7 +148,7 @@ def add(
148
148
 
149
149
  creation.create(name, preset=preset, path=pathlib.Path(path))
150
150
 
151
- contest = find_contest_package_or_die()
151
+ contest_pkg = find_contest_package_or_die()
152
152
 
153
153
  ru, contest = contest_package.get_ruyaml()
154
154
 
@@ -156,10 +156,16 @@ def add(
156
156
  'short_name': short_name,
157
157
  'path': path,
158
158
  }
159
- if 'problems' not in contest:
159
+ if 'problems' not in contest or not contest_pkg.problems:
160
160
  contest['problems'] = [item]
161
161
  else:
162
- contest['problems'].append(item)
162
+ idx = 0
163
+ while (
164
+ idx < len(contest_pkg.problems)
165
+ and contest_pkg.problems[idx].short_name <= short_name
166
+ ):
167
+ idx += 1
168
+ contest['problems'].insert(idx, item)
163
169
 
164
170
  dest = find_contest_yaml()
165
171
  assert dest is not None
rbx/box/contest/schema.py CHANGED
@@ -3,7 +3,14 @@ from typing import Annotated, Dict, List, Optional
3
3
 
4
4
  from pydantic import AfterValidator, BaseModel, ConfigDict, Field, model_validator
5
5
 
6
- from rbx.box.fields import FNameField, NameField, Primitive, expand_var
6
+ from rbx.box.fields import (
7
+ FNameField,
8
+ NameField,
9
+ Primitive,
10
+ RecVars,
11
+ Vars,
12
+ expand_vars,
13
+ )
7
14
  from rbx.box.statements.expander import expand_statements
8
15
  from rbx.box.statements.schema import (
9
16
  ConversionStep,
@@ -27,9 +34,8 @@ def is_unique_by_name(statements: List['ContestStatement']) -> List['ContestStat
27
34
  class ProblemStatementOverride(BaseModel):
28
35
  model_config = ConfigDict(extra='forbid')
29
36
 
30
- configure: List[ConversionStep] = Field(
37
+ configure: List[Annotated[ConversionStep, Field(discriminator='type')]] = Field(
31
38
  default=[],
32
- discriminator='type',
33
39
  description="""
34
40
  Configure how certain conversion steps should happen when applied to the statement file.
35
41
 
@@ -84,9 +90,8 @@ Joiner to be used to build the statement.
84
90
  This determines how problem statements will be joined into a single contest statement.""",
85
91
  )
86
92
 
87
- steps: List[ConversionStep] = Field(
93
+ steps: List[Annotated[ConversionStep, Field(discriminator='type')]] = Field(
88
94
  default=[],
89
- discriminator='type',
90
95
  description="""
91
96
  Describes a sequence of conversion steps that should be applied to the statement file
92
97
  of this contest.
@@ -97,9 +102,8 @@ certain conversion steps to happen.
97
102
  """,
98
103
  )
99
104
 
100
- configure: List[ConversionStep] = Field(
105
+ configure: List[Annotated[ConversionStep, Field(discriminator='type')]] = Field(
101
106
  default=[],
102
- discriminator='type',
103
107
  description="""
104
108
  Configure how certain conversion steps should happen when applied to the statement file of
105
109
  this contest.
@@ -133,13 +137,13 @@ Can be glob pattern as well, such as `imgs/*.png`.
133
137
 
134
138
  # Vars to be re-used in the statement.
135
139
  # - It will be available as \VAR{vars} variable in the contest-level box statement.
136
- vars: Dict[str, Primitive] = Field(
140
+ vars: RecVars = Field(
137
141
  default={}, description='Variables to be re-used across the package.'
138
142
  )
139
143
 
140
144
  @property
141
- def expanded_vars(self) -> Dict[str, Primitive]:
142
- return {key: expand_var(value) for key, value in self.vars.items()}
145
+ def expanded_vars(self) -> Vars:
146
+ return expand_vars(self.vars)
143
147
 
144
148
 
145
149
  class ContestProblem(BaseModel):
@@ -234,7 +238,7 @@ class Contest(BaseModel):
234
238
 
235
239
  # Vars to be re-used in the statements.
236
240
  # - It will be available as \VAR{vars} variable in the contest-level box statement.
237
- vars: Dict[str, Primitive] = Field(
241
+ vars: RecVars = Field(
238
242
  default={}, description='Variables to be re-used across the package.'
239
243
  )
240
244
 
@@ -243,5 +247,5 @@ class Contest(BaseModel):
243
247
  return expand_statements(self.statements)
244
248
 
245
249
  @property
246
- def expanded_vars(self) -> Dict[str, Primitive]:
247
- return {key: expand_var(value) for key, value in self.vars.items()}
250
+ def expanded_vars(self) -> Vars:
251
+ return expand_vars(self.vars)
@@ -5,13 +5,17 @@ import typer
5
5
 
6
6
  from rbx import annotations, console
7
7
  from rbx.box import cd, environment, package
8
- from rbx.box.contest.build_contest_statements import build_statement
8
+ from rbx.box.contest.build_contest_statements import (
9
+ StatementBuildIssue,
10
+ build_statement,
11
+ )
9
12
  from rbx.box.contest.contest_package import (
10
13
  find_contest_package_or_die,
11
14
  within_contest,
12
15
  )
13
16
  from rbx.box.contest.schema import ContestStatement
14
17
  from rbx.box.formatting import href
18
+ from rbx.box.sanitizers import issue_stack
15
19
  from rbx.box.schema import expand_any_vars
16
20
  from rbx.box.statements.schema import StatementType
17
21
 
@@ -70,13 +74,13 @@ async def build(
70
74
  with cd.new_package_cd(problem.get_path()):
71
75
  package.clear_package_cache()
72
76
 
73
- if not await builder.build(
74
- verification=verification, groups=set(['samples']), output=None
75
- ):
76
- console.console.print(
77
- '[error]Failed to build statements with samples, aborting.[/error]'
78
- )
79
- raise typer.Exit(1)
77
+ try:
78
+ if not await builder.build(
79
+ verification=verification, groups=set(['samples']), output=None
80
+ ):
81
+ issue_stack.add_issue(StatementBuildIssue(problem))
82
+ except Exception:
83
+ issue_stack.add_issue(StatementBuildIssue(problem))
80
84
 
81
85
  contest = find_contest_package_or_die()
82
86
 
rbx/box/dump_schemas.py CHANGED
@@ -7,10 +7,11 @@ from rbx.box.environment import Environment
7
7
  from rbx.box.package import Package
8
8
  from rbx.box.presets.lock_schema import PresetLock
9
9
  from rbx.box.presets.schema import Preset
10
+ from rbx.box.schema import LimitsProfile
10
11
  from rbx.box.statements.schema import Statement
11
12
  from rbx.utils import dump_schema_str
12
13
 
13
- models = [Package, Environment, Contest, Preset, PresetLock, Statement]
14
+ models = [Package, Environment, Contest, Preset, PresetLock, Statement, LimitsProfile]
14
15
 
15
16
  for model in models:
16
17
  path = pathlib.Path('schemas') / f'{model.__name__}.json'
rbx/box/environment.py CHANGED
@@ -192,7 +192,7 @@ for the environment will be used.""",
192
192
  def get_extension(self, name: str, _: Type[T]) -> Optional[T]:
193
193
  if self.extensions is None:
194
194
  return None
195
- if hasattr(self.extensions, name):
195
+ if not hasattr(self.extensions, name):
196
196
  return None
197
197
  return getattr(self.extensions, name)
198
198
 
rbx/box/fields.py CHANGED
@@ -1,7 +1,8 @@
1
- from typing import Dict, TypeVar, Union
1
+ from typing import TYPE_CHECKING, Dict, TypeVar, Union
2
2
 
3
3
  from deepmerge import always_merger
4
4
  from pydantic import BaseModel, Field
5
+ from typing_extensions import TypeAliasType
5
6
 
6
7
 
7
8
  def NameField(**kwargs):
@@ -35,7 +36,12 @@ def merge_pydantic_models(base: T, nxt: T) -> T:
35
36
  return base.model_validate(merged_dict)
36
37
 
37
38
 
38
- Primitive = Union[str, int, float, bool]
39
+ Primitive = Union[int, float, bool, str]
40
+ Vars = Dict[str, Primitive]
41
+ if TYPE_CHECKING:
42
+ RecVars = Dict[str, Union[Primitive, 'RecVars']]
43
+ else:
44
+ RecVars = TypeAliasType('RecVars', "Dict[str, Union[Primitive, 'RecVars']]")
39
45
 
40
46
 
41
47
  def expand_var(value: Primitive) -> Primitive:
@@ -55,5 +61,17 @@ def expand_var(value: Primitive) -> Primitive:
55
61
  )
56
62
 
57
63
 
58
- def expand_vars(vars: Dict[str, Primitive]) -> Dict[str, Primitive]:
59
- return {key: expand_var(value) for key, value in vars.items()}
64
+ def expand_vars(recvars: RecVars) -> Vars:
65
+ vars = {}
66
+
67
+ def solve(rec: RecVars, prefix: str) -> None:
68
+ nonlocal vars
69
+ for k, v in rec.items():
70
+ if isinstance(v, dict):
71
+ solve(v, f'{prefix}.{k}')
72
+ else:
73
+ key = f'{prefix}.{k}'.strip('.')
74
+ vars[key] = expand_var(v)
75
+
76
+ solve(recvars, '')
77
+ return vars