rbx.cp 0.5.16__py3-none-any.whl → 0.5.18__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/box/deferred.py ADDED
@@ -0,0 +1,26 @@
1
+ from typing import Awaitable, Callable, Generic, Optional, TypeVar
2
+
3
+ T = TypeVar('T')
4
+ U = TypeVar('U')
5
+
6
+
7
+ class Deferred(Generic[T]):
8
+ def __init__(self, func: Callable[[], Awaitable[T]]):
9
+ self.func = func
10
+ self.cache: Optional[T] = None
11
+
12
+ def __call__(self) -> Awaitable[T]:
13
+ async def async_wrapper():
14
+ if self.cache is None:
15
+ self.cache = await self.func()
16
+ return self.cache
17
+
18
+ return async_wrapper()
19
+
20
+ def peek(self) -> Optional[T]:
21
+ return self.cache
22
+
23
+ def wrap_with(
24
+ self, wrapper: Callable[[Awaitable[T]], Awaitable[U]]
25
+ ) -> 'Deferred[U]':
26
+ return Deferred(lambda: wrapper(self()))
rbx/box/extensions.py CHANGED
@@ -5,16 +5,8 @@ from pydantic import BaseModel, Field
5
5
  from rbx.box.packaging.boca.extension import BocaExtension, BocaLanguageExtension
6
6
 
7
7
 
8
- # List of extensions defined in-place.
9
- class MacExtension(BaseModel):
10
- gpp_alternative: Optional[str] = None
11
-
12
-
13
8
  # Extension abstractions.
14
9
  class Extensions(BaseModel):
15
- mac: Optional[MacExtension] = Field(
16
- None, description='Extension for setting mac-only configuration.'
17
- )
18
10
  boca: Optional[BocaExtension] = Field(
19
11
  None, description='Environment-level extensions for BOCA packaging.'
20
12
  )
rbx/box/generators.py CHANGED
@@ -8,7 +8,7 @@ import typer
8
8
 
9
9
  from rbx import console
10
10
  from rbx.box import checkers, package, testcases, validators
11
- from rbx.box.code import compile_item, run_item
11
+ from rbx.box.code import SanitizationLevel, compile_item, run_item
12
12
  from rbx.box.environment import (
13
13
  EnvironmentSandbox,
14
14
  ExecutionConfig,
@@ -32,7 +32,7 @@ from rbx.utils import StatusProgress
32
32
 
33
33
 
34
34
  def _compile_generator(generator: CodeItem) -> str:
35
- return compile_item(generator)
35
+ return compile_item(generator, sanitized=SanitizationLevel.PREFER)
36
36
 
37
37
 
38
38
  def _get_group_input(
@@ -322,7 +322,7 @@ def generate_standalone(
322
322
  # Get generator item
323
323
  generator = package.get_generator(call.name)
324
324
  if generator_digest is None:
325
- generator_digest = compile_item(generator)
325
+ generator_digest = _compile_generator(generator)
326
326
 
327
327
  generation_log = run_item(
328
328
  generator,
@@ -351,7 +351,9 @@ def generate_standalone(
351
351
  # Run validator, if it is available.
352
352
  if validator is not None and validate:
353
353
  if validator_digest is None:
354
- validator_digest = compile_item(validator)
354
+ validator_tp = validators.compile_main_validator()
355
+ assert validator_tp is not None
356
+ _, validator_digest = validator_tp
355
357
  ok, message, *_ = validators.validate_test(output, validator, validator_digest)
356
358
  if not ok:
357
359
  console.console.print(
rbx/box/main.py CHANGED
@@ -3,10 +3,11 @@ from gevent import monkey
3
3
 
4
4
  monkey.patch_all()
5
5
 
6
+
7
+ import asyncio
6
8
  import tempfile
7
9
  import shlex
8
10
  import sys
9
- import typing
10
11
 
11
12
  from rbx.box.schema import CodeItem, ExpectedOutcome, TestcaseGroup
12
13
 
@@ -24,6 +25,7 @@ from rbx import annotations, config, console, utils
24
25
  from rbx.box import (
25
26
  builder,
26
27
  cd,
28
+ setter_config,
27
29
  creation,
28
30
  download,
29
31
  environment,
@@ -38,6 +40,8 @@ from rbx.box.contest import main as contest
38
40
  from rbx.box.environment import VerificationLevel, get_environment_path
39
41
  from rbx.box.packaging import main as packaging
40
42
  from rbx.box.solutions import (
43
+ estimate_time_limit,
44
+ get_exact_matching_solutions,
41
45
  get_matching_solutions,
42
46
  print_run_report,
43
47
  run_and_print_interactive_solutions,
@@ -46,6 +50,12 @@ from rbx.box.solutions import (
46
50
  from rbx.box.statements import build_statements
47
51
 
48
52
  app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
53
+ app.add_typer(
54
+ setter_config.app,
55
+ name='config, cfg',
56
+ cls=annotations.AliasGroup,
57
+ help='Manage setter configuration.',
58
+ )
49
59
  app.add_typer(
50
60
  build_statements.app,
51
61
  name='statements, st',
@@ -122,6 +132,18 @@ def run(
122
132
  '-d',
123
133
  help='Whether to print a detailed view of the tests using tables.',
124
134
  ),
135
+ timeit: bool = typer.Option(
136
+ False,
137
+ '--time',
138
+ '-t',
139
+ help='Whether to use estimate a time limit based on accepted solutions.',
140
+ ),
141
+ sanitized: bool = typer.Option(
142
+ False,
143
+ '--sanitized',
144
+ '-s',
145
+ help='Whether to compile the solutions with sanitizers enabled.',
146
+ ),
125
147
  ):
126
148
  main_solution = package.get_main_solution()
127
149
  if check and main_solution is None:
@@ -139,7 +161,27 @@ def run(
139
161
  )
140
162
  return
141
163
 
164
+ override_tl = None
165
+ if timeit:
166
+ if sanitized:
167
+ console.console.print(
168
+ '[error]Sanitizers are known to be time-hungry, so they cannot be used for time estimation.\n'
169
+ 'Remove either the [item]-s[/item] flag or the [item]-t[/item] flag to run solutions without sanitizers.[/error]'
170
+ )
171
+ raise typer.Exit(1)
172
+
173
+ # Never use sanitizers for time estimation.
174
+ override_tl = _time_impl(check=check, detailed=False)
175
+ if override_tl is None:
176
+ raise typer.Exit(1)
177
+
142
178
  with utils.StatusProgress('Running solutions...') as s:
179
+ if sanitized:
180
+ console.console.print(
181
+ '[warning]Sanitizers are running, so the time limit for the problem will be dropped, '
182
+ 'and the environment default time limit will be used instead.[/warning]'
183
+ )
184
+
143
185
  tracked_solutions = None
144
186
  if outcome is not None:
145
187
  tracked_solutions = {
@@ -148,23 +190,113 @@ def run(
148
190
  }
149
191
  if solution:
150
192
  tracked_solutions = {solution}
193
+
194
+ if sanitized and tracked_solutions is None:
195
+ console.console.print(
196
+ '[warning]Sanitizers are running, and no solutions were specified to run. Will only run [item]ACCEPTED[/item] solutions.'
197
+ )
198
+ tracked_solutions = {
199
+ str(solution.path)
200
+ for solution in get_exact_matching_solutions(ExpectedOutcome.ACCEPTED)
201
+ }
202
+
151
203
  solution_result = run_solutions(
152
204
  progress=s,
153
205
  tracked_solutions=tracked_solutions,
154
206
  check=check,
155
- group_first=detailed,
156
207
  verification=VerificationLevel(verification),
208
+ timelimit_override=override_tl,
209
+ sanitized=sanitized,
157
210
  )
158
211
 
159
212
  console.console.print()
160
213
  console.console.rule('[status]Run report[/status]', style='status')
161
- print_run_report(
162
- solution_result,
163
- console.console,
164
- verification,
165
- detailed=detailed,
214
+ asyncio.run(
215
+ print_run_report(
216
+ solution_result,
217
+ console.console,
218
+ verification,
219
+ detailed=detailed,
220
+ )
221
+ )
222
+
223
+
224
+ def _time_impl(check: bool, detailed: bool) -> Optional[int]:
225
+ if package.get_main_solution() is None:
226
+ console.console.print(
227
+ '[warning]No main solution found, so cannot estimate a time limit.[/warning]'
228
+ )
229
+ return None
230
+
231
+ verification = VerificationLevel.ALL_SOLUTIONS.value
232
+
233
+ with utils.StatusProgress('Running ACCEPTED solutions...') as s:
234
+ tracked_solutions = {
235
+ str(solution.path)
236
+ for solution in get_exact_matching_solutions(ExpectedOutcome.ACCEPTED)
237
+ }
238
+ solution_result = run_solutions(
239
+ progress=s,
240
+ tracked_solutions=tracked_solutions,
241
+ check=check,
242
+ verification=VerificationLevel(verification),
243
+ )
244
+
245
+ console.console.print()
246
+ console.console.rule(
247
+ '[status]Run report (for time estimation)[/status]', style='status'
248
+ )
249
+ ok = asyncio.run(
250
+ print_run_report(
251
+ solution_result,
252
+ console.console,
253
+ verification,
254
+ detailed=detailed,
255
+ )
166
256
  )
167
257
 
258
+ if not ok:
259
+ console.console.print(
260
+ '[error]Failed to run ACCEPTED solutions, so cannot estimate a reliable time limit.[/error]'
261
+ )
262
+ return None
263
+
264
+ console.console.print()
265
+ return asyncio.run(estimate_time_limit(console.console, solution_result))
266
+
267
+
268
+ @app.command(
269
+ 'time, t',
270
+ help='Estimate a time limit for the problem based on a time limit formula and timings of accepted solutions.',
271
+ )
272
+ @package.within_problem
273
+ def time(
274
+ check: bool = typer.Option(
275
+ True,
276
+ '--nocheck',
277
+ flag_value=False,
278
+ help='Whether to not build outputs for tests and run checker.',
279
+ ),
280
+ detailed: bool = typer.Option(
281
+ False,
282
+ '--detailed',
283
+ '-d',
284
+ help='Whether to print a detailed view of the tests using tables.',
285
+ ),
286
+ ):
287
+ main_solution = package.get_main_solution()
288
+ if check and main_solution is None:
289
+ console.console.print(
290
+ '[warning]No main solution found, running without checkers.[/warning]'
291
+ )
292
+ check = False
293
+
294
+ verification = VerificationLevel.ALL_SOLUTIONS.value
295
+ if not builder.build(verification=verification, output=check):
296
+ return None
297
+
298
+ _time_impl(check, detailed)
299
+
168
300
 
169
301
  @app.command(
170
302
  'irun, ir', help='Build and run solution(s) by passing testcases in the CLI.'
@@ -199,6 +331,12 @@ def irun(
199
331
  print: bool = typer.Option(
200
332
  False, '--print', '-p', help='Whether to print outputs to terminal.'
201
333
  ),
334
+ sanitized: bool = typer.Option(
335
+ False,
336
+ '--sanitized',
337
+ '-s',
338
+ help='Whether to compile the solutions with sanitizers enabled.',
339
+ ),
202
340
  ):
203
341
  if not print:
204
342
  console.console.print(
@@ -225,14 +363,26 @@ def irun(
225
363
  }
226
364
  if solution:
227
365
  tracked_solutions = {solution}
228
- run_and_print_interactive_solutions(
229
- tracked_solutions=tracked_solutions,
230
- check=check,
231
- verification=VerificationLevel(verification),
232
- generator=generators.get_call_from_string(generator)
233
- if generator is not None
234
- else None,
235
- print=print,
366
+ if sanitized and tracked_solutions is None:
367
+ console.console.print(
368
+ '[warning]Sanitizers are running, and no solutions were specified to run. Will only run [item]ACCEPTED[/item] solutions.'
369
+ )
370
+ tracked_solutions = {
371
+ str(solution.path)
372
+ for solution in get_exact_matching_solutions(ExpectedOutcome.ACCEPTED)
373
+ }
374
+
375
+ asyncio.run(
376
+ run_and_print_interactive_solutions(
377
+ tracked_solutions=tracked_solutions,
378
+ check=check,
379
+ verification=VerificationLevel(verification),
380
+ generator=generators.get_call_from_string(generator)
381
+ if generator is not None
382
+ else None,
383
+ print=print,
384
+ sanitized=sanitized,
385
+ )
236
386
  )
237
387
 
238
388
 
@@ -294,6 +444,12 @@ def stress(
294
444
  '--verbose',
295
445
  help='Whether to print verbose output for checkers and finders.',
296
446
  ),
447
+ sanitized: bool = typer.Option(
448
+ False,
449
+ '--sanitized',
450
+ '-s',
451
+ help='Whether to compile the solutions with sanitizers enabled.',
452
+ ),
297
453
  ):
298
454
  if finder and not generator_args or generator_args and not finder:
299
455
  console.console.print(
@@ -310,6 +466,7 @@ def stress(
310
466
  findingsLimit=findings,
311
467
  progress=s,
312
468
  verbose=verbose,
469
+ sanitized=sanitized,
313
470
  )
314
471
 
315
472
  stresses.print_stress_report(report)
@@ -351,10 +508,17 @@ def stress(
351
508
  groups_by_name[testgroup] = TestcaseGroup(
352
509
  name=testgroup, generatorScript=CodeItem(path=new_script_path)
353
510
  )
354
- console.console.print(
355
- f'[warning]A testgroup for [item]{new_script_path}[/item] will not be automatically added to the problem.rbx.yml file for you.\n'
356
- 'Please add it manually. [/warning]'
511
+ ru, problem_yml = package.get_ruyaml()
512
+ problem_yml['testcases'].append(
513
+ {
514
+ 'name': testgroup,
515
+ 'generatorScript': new_script_path.name,
516
+ }
357
517
  )
518
+ dest = package.find_problem_yaml()
519
+ assert dest is not None
520
+ utils.save_ruyaml(dest, ru, problem_yml)
521
+ package.clear_package_cache()
358
522
 
359
523
  if testgroup not in groups_by_name:
360
524
  break
@@ -387,8 +551,20 @@ def stress(
387
551
  @package.within_problem
388
552
  def compile_command(
389
553
  path: Annotated[str, typer.Argument(help='Path to the asset to compile.')],
554
+ sanitized: bool = typer.Option(
555
+ False,
556
+ '--sanitized',
557
+ '-s',
558
+ help='Whether to compile the asset with sanitizers enabled.',
559
+ ),
560
+ warnings: bool = typer.Option(
561
+ False,
562
+ '--warnings',
563
+ '-w',
564
+ help='Whether to compile the asset with warnings enabled.',
565
+ ),
390
566
  ):
391
- compile.any(path)
567
+ compile.any(path, sanitized, warnings)
392
568
 
393
569
 
394
570
  @app.command('validate', help='Run the validator in a one-off fashion, interactively.')
rbx/box/package.py CHANGED
@@ -1,12 +1,14 @@
1
1
  import functools
2
2
  import pathlib
3
+ import sys
3
4
  from typing import Dict, List, Optional, Tuple
4
5
 
6
+ import ruyaml
5
7
  import typer
6
8
  from pydantic import ValidationError
7
9
 
8
10
  from rbx import config, console, utils
9
- from rbx.box import environment
11
+ from rbx.box import cd, environment
10
12
  from rbx.box.environment import get_sandbox_type
11
13
  from rbx.box.presets import get_installed_preset_or_null, get_preset_lock
12
14
  from rbx.box.schema import (
@@ -102,7 +104,7 @@ def find_problem(root: pathlib.Path = pathlib.Path()) -> pathlib.Path:
102
104
  def within_problem(func):
103
105
  @functools.wraps(func)
104
106
  def wrapper(*args, **kwargs):
105
- with utils.new_cd(find_problem()):
107
+ with cd.new_package_cd(find_problem()):
106
108
  return func(*args, **kwargs)
107
109
 
108
110
  return wrapper
@@ -119,6 +121,17 @@ def save_package(
119
121
  problem_yaml_path.write_text(utils.model_to_yaml(package))
120
122
 
121
123
 
124
+ def get_ruyaml() -> Tuple[ruyaml.YAML, ruyaml.Any]:
125
+ problem_yaml_path = find_problem_yaml()
126
+ if problem_yaml_path is None:
127
+ console.console.print(
128
+ f'Problem not found in {pathlib.Path().absolute()}', style='error'
129
+ )
130
+ raise typer.Exit(1)
131
+ res = ruyaml.YAML()
132
+ return res, res.load(problem_yaml_path.read_text())
133
+
134
+
122
135
  @functools.cache
123
136
  def get_problem_cache_dir(root: pathlib.Path = pathlib.Path()) -> pathlib.Path:
124
137
  cache_dir = find_problem(root) / '.box'
@@ -328,3 +341,12 @@ def get_compilation_files(code: CodeItem) -> List[Tuple[pathlib.Path, pathlib.Pa
328
341
  )
329
342
  )
330
343
  return res
344
+
345
+
346
+ def clear_package_cache():
347
+ pkgs = [sys.modules[__name__]]
348
+
349
+ for pkg in pkgs:
350
+ for fn in pkg.__dict__.values():
351
+ if hasattr(fn, 'cache_clear'):
352
+ fn.cache_clear()
@@ -4,9 +4,9 @@ from typing import Type
4
4
 
5
5
  import typer
6
6
 
7
- from rbx import annotations, console, utils
8
- from rbx.box import environment, package
9
- from rbx.box.contest import build_contest_statements, contest_package, contest_utils
7
+ from rbx import annotations, console
8
+ from rbx.box import cd, environment, package
9
+ from rbx.box.contest import build_contest_statements, contest_package
10
10
  from rbx.box.packaging.main import run_packager
11
11
  from rbx.box.packaging.packager import (
12
12
  BaseContestPackager,
@@ -32,8 +32,8 @@ def run_contest_packager(
32
32
  console.console.print(
33
33
  f'Processing problem [item]{problem.short_name}[/item]...'
34
34
  )
35
- with utils.new_cd(problem.get_path()):
36
- contest_utils.clear_package_cache()
35
+ with cd.new_package_cd(problem.get_path()):
36
+ package.clear_package_cache()
37
37
  package_path = run_packager(packager_cls, verification=verification)
38
38
  built_packages.append(
39
39
  BuiltProblemPackage(
@@ -0,0 +1,90 @@
1
+ import functools
2
+ import pathlib
3
+ import shutil
4
+
5
+ from rbx import console
6
+ from rbx.box.schema import CodeItem
7
+ from rbx.grading.judge.storage import Storage
8
+ from rbx.grading.steps import GradingFileOutput
9
+
10
+
11
+ class WarningStack:
12
+ def __init__(self, root: pathlib.Path):
13
+ self.root = root
14
+ self.warnings = set()
15
+ self.sanitizer_warnings = {}
16
+
17
+ def add_warning(self, code: CodeItem):
18
+ self.warnings.add(code.path)
19
+
20
+ def add_sanitizer_warning(
21
+ self, storage: Storage, code: CodeItem, reference: GradingFileOutput
22
+ ):
23
+ if code.path in self.sanitizer_warnings:
24
+ return
25
+ dest_path = _get_warning_runs_dir(self.root).joinpath(
26
+ code.path.with_suffix(code.path.suffix + '.log')
27
+ )
28
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
29
+ f = reference.get_file(storage)
30
+ if f is None:
31
+ return
32
+ with dest_path.open('wb') as fout:
33
+ shutil.copyfileobj(f, fout)
34
+ f.close()
35
+ self.sanitizer_warnings[code.path] = dest_path
36
+
37
+ def clear(self):
38
+ self.warnings.clear()
39
+ self.sanitizer_warnings.clear()
40
+
41
+
42
+ @functools.cache
43
+ def _get_warning_stack(root: pathlib.Path) -> WarningStack:
44
+ return WarningStack(root)
45
+
46
+
47
+ @functools.cache
48
+ def _get_cache_dir(root: pathlib.Path) -> pathlib.Path:
49
+ dir = root / '.box'
50
+ dir.mkdir(parents=True, exist_ok=True)
51
+ return dir
52
+
53
+
54
+ @functools.cache
55
+ def _get_warning_runs_dir(root: pathlib.Path) -> pathlib.Path:
56
+ dir = _get_cache_dir(root) / 'warnings'
57
+ shutil.rmtree(dir, ignore_errors=True)
58
+ dir.mkdir(parents=True, exist_ok=True)
59
+ return dir
60
+
61
+
62
+ def get_warning_stack() -> WarningStack:
63
+ current_root = pathlib.Path.cwd().resolve()
64
+ return _get_warning_stack(current_root)
65
+
66
+
67
+ def print_warning_stack_report():
68
+ stack = get_warning_stack()
69
+ if not stack.warnings and not stack.sanitizer_warnings:
70
+ return
71
+ console.console.rule('[status]Warning stack[/status]')
72
+ console.console.print(
73
+ f'[warning]There were some warnings within the code that run at [item]{stack.root.absolute()}[/item][/warning]'
74
+ )
75
+ if stack.warnings:
76
+ console.console.print(f'{len(stack.warnings)} compilation warnings')
77
+ console.console.print(
78
+ 'You can use [item]rbx compile[/item] to reproduce the issues with the files below.'
79
+ )
80
+ for path in sorted(stack.warnings):
81
+ console.console.print(f'- [item]{path}[/item]')
82
+ console.console.print()
83
+
84
+ if stack.sanitizer_warnings:
85
+ console.console.print(f'{len(stack.sanitizer_warnings)} sanitizer warnings')
86
+ for path in sorted(stack.sanitizer_warnings):
87
+ console.console.print(
88
+ f'- [item]{path}[/item], example log at [item]{stack.sanitizer_warnings[path]}[/item]'
89
+ )
90
+ console.console.print()
rbx/box/schema.py CHANGED
@@ -54,6 +54,18 @@ class ExpectedOutcome(AutoEnum):
54
54
  ACCEPTED = alias('accepted', 'ac', 'correct') # type: ignore
55
55
  """Expected outcome for correct solutions (AC)."""
56
56
 
57
+ ACCEPTED_OR_TLE = alias(
58
+ 'accepted or time limit exceeded',
59
+ 'accepted or tle',
60
+ 'ac or tle',
61
+ 'ac/tle',
62
+ 'ac+tle',
63
+ ) # type: ignore
64
+ """Expected outcome for solutions that finish with either AC or TLE.
65
+
66
+ Especially useful when you do not care about the running time of this solution, and
67
+ want it to not be considered when calculating the timelimit for the problem."""
68
+
57
69
  WRONG_ANSWER = alias('wrong answer', 'wa') # type: ignore
58
70
  """Expected outcome for solutions that finish successfully,
59
71
  but the produced output are incorrect (WA)."""
@@ -75,7 +87,7 @@ class ExpectedOutcome(AutoEnum):
75
87
 
76
88
  TLE_OR_RTE = alias('tle or rte', 'tle/rte', 'tle+rte') # type: ignore
77
89
  """Expected outcome for solutions that finish with either TLE or RTE.
78
-
90
+
79
91
  Especially useful for environments where TLE and RTE are indistinguishable."""
80
92
 
81
93
  def style(self) -> str:
@@ -99,6 +111,8 @@ class ExpectedOutcome(AutoEnum):
99
111
  def match(self, outcome: Outcome) -> bool:
100
112
  if self == ExpectedOutcome.ACCEPTED:
101
113
  return outcome == Outcome.ACCEPTED
114
+ if self == ExpectedOutcome.ACCEPTED_OR_TLE:
115
+ return outcome in {Outcome.ACCEPTED, Outcome.TIME_LIMIT_EXCEEDED}
102
116
  if self == ExpectedOutcome.WRONG_ANSWER:
103
117
  return outcome == Outcome.WRONG_ANSWER
104
118
  if self == ExpectedOutcome.INCORRECT: