rbx.cp 0.5.16__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/builder.py +2 -2
- rbx/box/cd.py +12 -2
- rbx/box/checkers.py +38 -4
- rbx/box/code.py +126 -18
- rbx/box/compile.py +40 -8
- rbx/box/contest/build_contest_statements.py +5 -3
- rbx/box/contest/contest_package.py +2 -1
- rbx/box/contest/main.py +2 -2
- rbx/box/contest/statements.py +3 -3
- rbx/box/deferred.py +26 -0
- rbx/box/extensions.py +0 -8
- rbx/box/generators.py +6 -4
- rbx/box/main.py +185 -16
- rbx/box/package.py +2 -2
- rbx/box/packaging/contest_main.py +3 -3
- rbx/box/sanitizers/warning_stack.py +90 -0
- rbx/box/schema.py +15 -1
- rbx/box/setter_config.py +132 -0
- rbx/box/solutions.py +237 -149
- rbx/box/solutions_test.py +2 -1
- rbx/box/stresses.py +5 -3
- rbx/box/validators.py +5 -5
- rbx/config.py +3 -0
- rbx/grading/caching.py +4 -0
- rbx/grading/judge/sandboxes/stupid_sandbox.py +2 -0
- rbx/grading/steps.py +125 -15
- rbx/grading/steps_with_caching.py +2 -0
- rbx/resources/default_setter_config.mac.yml +31 -0
- rbx/resources/default_setter_config.yml +29 -0
- rbx/utils.py +1 -1
- {rbx_cp-0.5.16.dist-info → rbx_cp-0.5.17.dist-info}/METADATA +1 -1
- {rbx_cp-0.5.16.dist-info → rbx_cp-0.5.17.dist-info}/RECORD +35 -30
- {rbx_cp-0.5.16.dist-info → rbx_cp-0.5.17.dist-info}/LICENSE +0 -0
- {rbx_cp-0.5.16.dist-info → rbx_cp-0.5.17.dist-info}/WHEEL +0 -0
- {rbx_cp-0.5.16.dist-info → rbx_cp-0.5.17.dist-info}/entry_points.txt +0 -0
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
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
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
|
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
|
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
|
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:
|
rbx/box/setter_config.py
ADDED
@@ -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.
|