rbx.cp 0.5.15__py3-none-any.whl → 0.5.17__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/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)
@@ -387,8 +544,20 @@ def stress(
387
544
  @package.within_problem
388
545
  def compile_command(
389
546
  path: Annotated[str, typer.Argument(help='Path to the asset to compile.')],
547
+ sanitized: bool = typer.Option(
548
+ False,
549
+ '--sanitized',
550
+ '-s',
551
+ help='Whether to compile the asset with sanitizers enabled.',
552
+ ),
553
+ warnings: bool = typer.Option(
554
+ False,
555
+ '--warnings',
556
+ '-w',
557
+ help='Whether to compile the asset with warnings enabled.',
558
+ ),
390
559
  ):
391
- compile.any(path)
560
+ compile.any(path, sanitized, warnings)
392
561
 
393
562
 
394
563
  @app.command('validate', help='Run the validator in a one-off fashion, interactively.')
rbx/box/package.py CHANGED
@@ -6,7 +6,7 @@ import typer
6
6
  from pydantic import ValidationError
7
7
 
8
8
  from rbx import config, console, utils
9
- from rbx.box import environment
9
+ from rbx.box import cd, environment
10
10
  from rbx.box.environment import get_sandbox_type
11
11
  from rbx.box.presets import get_installed_preset_or_null, get_preset_lock
12
12
  from rbx.box.schema import (
@@ -102,7 +102,7 @@ def find_problem(root: pathlib.Path = pathlib.Path()) -> pathlib.Path:
102
102
  def within_problem(func):
103
103
  @functools.wraps(func)
104
104
  def wrapper(*args, **kwargs):
105
- with utils.new_cd(find_problem()):
105
+ with cd.new_package_cd(find_problem()):
106
106
  return func(*args, **kwargs)
107
107
 
108
108
  return wrapper
@@ -4,8 +4,8 @@ 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
7
+ from rbx import annotations, console
8
+ from rbx.box import cd, environment, package
9
9
  from rbx.box.contest import build_contest_statements, contest_package, contest_utils
10
10
  from rbx.box.packaging.main import run_packager
11
11
  from rbx.box.packaging.packager import (
@@ -32,7 +32,7 @@ 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()):
35
+ with cd.new_package_cd(problem.get_path()):
36
36
  contest_utils.clear_package_cache()
37
37
  package_path = run_packager(packager_cls, verification=verification)
38
38
  built_packages.append(
@@ -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:
@@ -0,0 +1,132 @@
1
+ import functools
2
+ import importlib
3
+ import importlib.resources
4
+ import pathlib
5
+ import shlex
6
+ import sys
7
+ from typing import Dict
8
+
9
+ import typer
10
+ from pydantic import BaseModel, Field
11
+
12
+ from rbx import config, console, utils
13
+
14
+ app = typer.Typer(no_args_is_help=True)
15
+
16
+ _CONFIG_FILE_NAME = 'default_setter_config.yml'
17
+ _CONFIG_FILE_NAME_MAC = 'default_setter_config.mac.yml'
18
+
19
+
20
+ class SanitizersConfig(BaseModel):
21
+ enabled: bool = Field(
22
+ False,
23
+ description='Whether to use sanitizers when running solutions.',
24
+ )
25
+
26
+ command_substitutions: Dict[str, str] = Field(
27
+ {},
28
+ description='Substitutions to apply to commands before running them with sanitizers.',
29
+ )
30
+
31
+
32
+ class WarningsConfig(BaseModel):
33
+ enabled: bool = Field(
34
+ False,
35
+ description='Whether to use warning flags when running solutions.',
36
+ )
37
+
38
+
39
+ class SetterConfig(BaseModel):
40
+ sanitizers: SanitizersConfig = Field(
41
+ default_factory=SanitizersConfig, # type: ignore
42
+ description='Configuration for sanitizers.',
43
+ )
44
+ warnings: WarningsConfig = Field(
45
+ default_factory=WarningsConfig, # type: ignore
46
+ description='Configuration for warnings.',
47
+ )
48
+
49
+ command_substitutions: Dict[str, str] = Field(
50
+ {},
51
+ description='Substitutions to apply to commands before running them.',
52
+ )
53
+
54
+ def substitute_command(self, command: str, sanitized: bool = False) -> str:
55
+ exe = shlex.split(command)[0]
56
+ if sanitized and exe in self.sanitizers.command_substitutions:
57
+ exe = self.sanitizers.command_substitutions[exe]
58
+ return ' '.join([exe, *shlex.split(command)[1:]])
59
+ if exe in self.command_substitutions:
60
+ exe = self.command_substitutions[exe]
61
+ return ' '.join([exe, *shlex.split(command)[1:]])
62
+ return command
63
+
64
+
65
+ def get_default_setter_config_path() -> pathlib.Path:
66
+ cfg_name = _CONFIG_FILE_NAME
67
+ if sys.platform == 'darwin':
68
+ cfg_name = _CONFIG_FILE_NAME_MAC
69
+
70
+ with importlib.resources.as_file(
71
+ importlib.resources.files('rbx') / 'resources' / cfg_name
72
+ ) as file:
73
+ return file
74
+
75
+
76
+ def get_default_setter_config() -> SetterConfig:
77
+ return utils.model_from_yaml(
78
+ SetterConfig, get_default_setter_config_path().read_text()
79
+ )
80
+
81
+
82
+ def get_setter_config_path() -> pathlib.Path:
83
+ return config.get_app_path() / 'setter_config.yml'
84
+
85
+
86
+ @functools.cache
87
+ def get_setter_config() -> SetterConfig:
88
+ config_path = get_setter_config_path()
89
+ if not config_path.is_file():
90
+ utils.create_and_write(
91
+ config_path, get_default_setter_config_path().read_text()
92
+ )
93
+ return utils.model_from_yaml(SetterConfig, config_path.read_text())
94
+
95
+
96
+ def save_setter_config(config: SetterConfig):
97
+ config_path = get_setter_config_path()
98
+ config_path.write_text(utils.model_to_yaml(config))
99
+ get_setter_config.cache_clear()
100
+
101
+
102
+ @app.command(help='Show the path to the setter config.')
103
+ def path():
104
+ print(get_setter_config_path())
105
+
106
+
107
+ @app.command('list, ls')
108
+ def list():
109
+ """
110
+ Pretty print the config file.
111
+ """
112
+ console.console.print_json(utils.model_json(get_setter_config()))
113
+
114
+
115
+ @app.command(help='Open the setter config in an editor.')
116
+ def edit():
117
+ # Ensure config is created before calling the editor.
118
+ get_setter_config()
119
+
120
+ config.open_editor(get_setter_config_path())
121
+
122
+
123
+ @app.command()
124
+ def reset():
125
+ """
126
+ Reset the config file to the default one.
127
+ """
128
+ if not typer.confirm('Do you really want to reset your config to the default one?'):
129
+ return
130
+ cfg_path = get_setter_config_path()
131
+ cfg_path.unlink(missing_ok=True)
132
+ get_setter_config() # Reset the config.