rbx.cp 0.5.36__py3-none-any.whl → 0.5.38__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 CHANGED
@@ -1,4 +1,3 @@
1
- import importlib
2
1
  import importlib.resources
3
2
  import pathlib
4
3
  import re
rbx/box/builder.py CHANGED
@@ -4,7 +4,6 @@ from rbx import console, utils
4
4
  from rbx.box import environment, package
5
5
  from rbx.box.environment import VerificationLevel
6
6
  from rbx.box.generators import (
7
- extract_generation_testcases_from_groups,
8
7
  generate_outputs_for_testcases,
9
8
  generate_testcases,
10
9
  )
@@ -13,6 +12,7 @@ from rbx.box.solutions import (
13
12
  print_run_report,
14
13
  run_solutions,
15
14
  )
15
+ from rbx.box.testcase_extractors import extract_generation_testcases_from_groups
16
16
  from rbx.box.validators import (
17
17
  has_validation_errors,
18
18
  print_validation_report,
@@ -50,7 +50,10 @@ def build(
50
50
  'Validated [item]{processed}[/item] testcases...',
51
51
  keep=True,
52
52
  ) as s:
53
- infos = validate_testcases(s, groups=groups)
53
+ infos = validate_testcases(
54
+ s,
55
+ groups=groups,
56
+ )
54
57
  print_validation_report(infos)
55
58
 
56
59
  if has_validation_errors(infos):
@@ -3,7 +3,7 @@ from typing import Annotated, List, Optional
3
3
  import typer
4
4
 
5
5
  from rbx import annotations, console
6
- from rbx.box import builder, cd, environment, package
6
+ from rbx.box import cd, environment, package
7
7
  from rbx.box.contest.build_contest_statements import build_statement
8
8
  from rbx.box.contest.contest_package import (
9
9
  find_contest_package_or_die,
@@ -44,6 +44,8 @@ def build(
44
44
  contest = find_contest_package_or_die()
45
45
  # At most run the validators, only in samples.
46
46
  if samples:
47
+ from rbx.box import builder
48
+
47
49
  for problem in contest.problems:
48
50
  console.console.print(
49
51
  f'Processing problem [item]{problem.short_name}[/item]...'
rbx/box/generators.py CHANGED
@@ -1,12 +1,8 @@
1
- import abc
2
1
  import pathlib
3
- import shlex
4
2
  import shutil
5
- from pathlib import PosixPath
6
- from typing import Dict, Iterable, List, Optional, Set, Tuple
3
+ from typing import Dict, List, Optional, Set
7
4
 
8
5
  import typer
9
- from pydantic import BaseModel
10
6
 
11
7
  from rbx import console
12
8
  from rbx.box import checkers, package, testcase_utils, validators
@@ -19,10 +15,19 @@ from rbx.box.schema import (
19
15
  CodeItem,
20
16
  GeneratorCall,
21
17
  Testcase,
22
- TestcaseSubgroup,
23
18
  )
24
- from rbx.box.stressing import generator_parser
25
- from rbx.box.testcase_utils import TestcaseEntry, TestcasePattern, find_built_testcases
19
+ from rbx.box.testcase_extractors import (
20
+ GenerationMetadata,
21
+ GenerationTestcaseEntry,
22
+ TestcaseGroupVisitor,
23
+ extract_generation_testcases,
24
+ run_testcase_visitor,
25
+ )
26
+ from rbx.box.testcase_utils import (
27
+ TestcaseEntry,
28
+ fill_output_for_defined_testcase,
29
+ find_built_testcases,
30
+ )
26
31
  from rbx.grading.steps import (
27
32
  DigestHolder,
28
33
  DigestOrDest,
@@ -35,33 +40,11 @@ def _compile_generator(generator: CodeItem) -> str:
35
40
  return compile_item(generator, sanitized=SanitizationLevel.PREFER)
36
41
 
37
42
 
38
- def _get_group_input(
39
- group_path: pathlib.Path, subgroup_prefix: str, i: int
40
- ) -> pathlib.Path:
41
- return group_path / f'{subgroup_prefix}{i:03d}.in'
42
-
43
-
44
- def _get_group_output(
45
- group_path: pathlib.Path, subgroup_prefix: str, i: int
46
- ) -> pathlib.Path:
47
- return group_path / f'{subgroup_prefix}{i:03d}.out'
48
-
49
-
50
- def _fill_output_for_defined_testcase(testcase: Testcase) -> Testcase:
51
- res = testcase.model_copy()
52
- if res.outputPath is not None:
53
- return res
54
- output_path = res.inputPath.with_suffix('.ans')
55
- if output_path.is_file():
56
- res.outputPath = output_path
57
- return res
58
-
59
-
60
43
  def _copy_testcase_over(
61
44
  testcase: Testcase,
62
45
  dest: Testcase,
63
46
  ):
64
- testcase = _fill_output_for_defined_testcase(testcase)
47
+ testcase = fill_output_for_defined_testcase(testcase)
65
48
  dest.inputPath.parent.mkdir(parents=True, exist_ok=True)
66
49
  shutil.copy(
67
50
  str(testcase.inputPath),
@@ -90,230 +73,6 @@ def get_call_from_string(call_str: str) -> GeneratorCall:
90
73
  return GeneratorCall(name=name, args=args)
91
74
 
92
75
 
93
- def _run_generator_script(testcase: TestcaseSubgroup) -> str:
94
- assert testcase.generatorScript is not None
95
-
96
- cacher = package.get_file_cacher()
97
-
98
- if not testcase.generatorScript.path.is_file():
99
- console.console.print(
100
- f'[error]Generator script not found: [item]{testcase.generatorScript.path}[/item][/error]'
101
- )
102
- raise typer.Exit(1)
103
-
104
- script_digest = DigestHolder()
105
- if testcase.generatorScript.path.suffix == '.txt':
106
- script_digest.value = cacher.put_file_from_path(testcase.generatorScript.path)
107
- else:
108
- try:
109
- compiled_digest = compile_item(testcase.generatorScript)
110
- except:
111
- console.console.print(
112
- f'[error]Failed compiling generator script for group [item]{testcase.name}[/item].[/error]'
113
- )
114
- raise
115
-
116
- run_stderr = DigestHolder()
117
- run_log = run_item(
118
- testcase.generatorScript,
119
- DigestOrSource.create(compiled_digest),
120
- stdout=DigestOrDest.create(script_digest),
121
- stderr=DigestOrDest.create(run_stderr),
122
- )
123
-
124
- if run_log is None or run_log.exitcode != 0:
125
- console.console.print(
126
- f'Could not run generator script for group {testcase.name}'
127
- )
128
- if run_log is not None:
129
- console.console.print(
130
- f'[error]Summary:[/error] {run_log.get_summary()}'
131
- )
132
- if run_stderr.value is not None:
133
- console.console.print('[error]Stderr:[/error]')
134
- console.console.print(
135
- package.get_digest_as_string(run_stderr.value) or ''
136
- )
137
- raise typer.Exit(1)
138
-
139
- assert script_digest.value
140
- script = cacher.get_file_content(script_digest.value).decode()
141
- return script
142
-
143
-
144
- def _extract_script_lines(script: str) -> Iterable[Tuple[str, str, int]]:
145
- lines = script.splitlines()
146
- for i, line in enumerate(lines):
147
- line = line.strip()
148
- if not line:
149
- continue
150
- if line.startswith('#'):
151
- continue
152
- yield shlex.split(line)[0], shlex.join(shlex.split(line)[1:]), i + 1
153
-
154
-
155
- class GeneratorScriptEntry(BaseModel):
156
- path: pathlib.Path
157
- line: int
158
-
159
-
160
- class GenerationMetadata(BaseModel):
161
- copied_to: Testcase
162
-
163
- copied_from: Optional[Testcase] = None
164
- generator_call: Optional[GeneratorCall] = None
165
- generator_script: Optional[GeneratorScriptEntry] = None
166
-
167
-
168
- class GenerationTestcaseEntry(BaseModel):
169
- group_entry: TestcaseEntry
170
- subgroup_entry: TestcaseEntry
171
-
172
- metadata: GenerationMetadata
173
-
174
-
175
- class TestcaseVisitor(abc.ABC):
176
- @abc.abstractmethod
177
- def visit(self, entry: GenerationTestcaseEntry):
178
- pass
179
-
180
- def should_visit_group(self, group_name: str) -> bool:
181
- return True
182
-
183
- def should_visit_subgroup(self, subgroup_path: str) -> bool:
184
- return True
185
-
186
- def should_visit_generator_scripts(
187
- self, group_name: str, subgroup_path: str
188
- ) -> bool:
189
- return True
190
-
191
-
192
- class TestcaseGroupVisitor(TestcaseVisitor):
193
- def __init__(self, groups: Optional[Set[str]] = None):
194
- self.groups = groups
195
-
196
- def should_visit_group(self, group_name: str) -> bool:
197
- return self.groups is None or group_name in self.groups
198
-
199
-
200
- def run_testcase_visitor(visitor: TestcaseVisitor):
201
- pkg = package.find_problem_package_or_die()
202
-
203
- def _explore_subgroup(
204
- subgroup: TestcaseSubgroup, subgroup_index: Optional[int], prefix: List[str]
205
- ):
206
- assert prefix and len(prefix) >= 1 and len(prefix) <= 2
207
- group_path = prefix[0]
208
- subgroup_path = '/'.join(prefix)
209
- if not visitor.should_visit_subgroup(subgroup_path):
210
- return
211
-
212
- def _entry(i: int) -> TestcaseEntry:
213
- return TestcaseEntry(group=group_path, index=i)
214
-
215
- def _sub_entry(i: int) -> TestcaseEntry:
216
- return TestcaseEntry(group=subgroup_path, index=i)
217
-
218
- def _copied_to(i: int) -> Testcase:
219
- group_fs_path = package.get_build_testgroup_path(group_path)
220
- group_prefix = ''
221
- if subgroup_index is not None:
222
- group_prefix = f'{subgroup_index}-'
223
- if len(prefix) == 2:
224
- group_prefix += f'{prefix[1]}-'
225
- return Testcase(
226
- inputPath=_get_group_input(group_fs_path, group_prefix, i),
227
- outputPath=_get_group_output(group_fs_path, group_prefix, i),
228
- )
229
-
230
- # Go through testcases.
231
- i = 0
232
- # Individual testcases.
233
- for tc in subgroup.testcases or []:
234
- visitor.visit(
235
- GenerationTestcaseEntry(
236
- group_entry=_entry(i),
237
- subgroup_entry=_sub_entry(i),
238
- metadata=GenerationMetadata(
239
- copied_from=_fill_output_for_defined_testcase(tc),
240
- copied_to=_copied_to(i),
241
- ),
242
- )
243
- )
244
- i += 1
245
-
246
- # Glob testcases.
247
- if subgroup.testcaseGlob:
248
- matched_inputs = sorted(PosixPath().glob(subgroup.testcaseGlob))
249
-
250
- for input_path in matched_inputs:
251
- if not input_path.is_file() or input_path.suffix != '.in':
252
- continue
253
-
254
- tc = Testcase(inputPath=input_path)
255
- visitor.visit(
256
- GenerationTestcaseEntry(
257
- group_entry=_entry(i),
258
- subgroup_entry=_sub_entry(i),
259
- metadata=GenerationMetadata(
260
- copied_from=_fill_output_for_defined_testcase(tc),
261
- copied_to=_copied_to(i),
262
- ),
263
- )
264
- )
265
- i += 1
266
-
267
- # Single generators.
268
- for generator_call in subgroup.generators:
269
- visitor.visit(
270
- GenerationTestcaseEntry(
271
- group_entry=_entry(i),
272
- subgroup_entry=_sub_entry(i),
273
- metadata=GenerationMetadata(
274
- generator_call=generator_call,
275
- copied_to=_copied_to(i),
276
- ),
277
- )
278
- )
279
- i += 1
280
-
281
- if not visitor.should_visit_generator_scripts(group_path, subgroup_path):
282
- return
283
-
284
- # Run generator script.
285
- if subgroup.generatorScript is not None:
286
- script = _run_generator_script(subgroup)
287
-
288
- # Run each line from generator script.
289
- for generator_name, args, line_number in _extract_script_lines(script):
290
- call = GeneratorCall(name=generator_name, args=args)
291
- visitor.visit(
292
- GenerationTestcaseEntry(
293
- group_entry=_entry(i),
294
- subgroup_entry=_sub_entry(i),
295
- metadata=GenerationMetadata(
296
- generator_call=call,
297
- generator_script=GeneratorScriptEntry(
298
- path=subgroup.generatorScript.path,
299
- line=line_number,
300
- ),
301
- copied_to=_copied_to(i),
302
- ),
303
- )
304
- )
305
- i += 1
306
-
307
- for group in pkg.testcases:
308
- if not visitor.should_visit_group(group.name):
309
- continue
310
-
311
- _explore_subgroup(group, 0 if group.subgroups else None, [group.name])
312
-
313
- for i, subgroup in enumerate(group.subgroups):
314
- _explore_subgroup(subgroup, i + 1, [group.name, subgroup.name])
315
-
316
-
317
76
  def _get_necessary_generators_for_groups(
318
77
  groups: Optional[Set[str]] = None,
319
78
  ) -> Set[str]:
@@ -359,6 +118,8 @@ def compile_generators(
359
118
 
360
119
 
361
120
  def expand_generator_call(call: GeneratorCall) -> GeneratorCall:
121
+ from rbx.box.stressing import generator_parser
122
+
362
123
  vars = package.find_problem_package_or_die().expanded_vars
363
124
  generator_for_args = generator_parser.Generator(vars)
364
125
  parsed_args = generator_parser.parse(call.args or '')
@@ -439,14 +200,14 @@ def generate_standalone(
439
200
  _, validator_digest = validator_tp
440
201
  if progress:
441
202
  progress.update('Validating test...')
442
- ok, message, *_ = validators.validate_test(
203
+ validation_info = validators.validate_one_off(
443
204
  spec.copied_to.inputPath,
444
205
  validator,
445
206
  validator_digest,
446
207
  )
447
- if not ok:
208
+ if not validation_info.ok:
448
209
  _print_error_header('failed validating testcase.')
449
- console.console.print(f'[error]Message:[/error] {message}')
210
+ console.console.print(f'[error]Message:[/error] {validation_info.message}')
450
211
  console.console.print(
451
212
  f'Testcase written at [item]{spec.copied_to.inputPath}[/item]'
452
213
  )
@@ -552,67 +313,6 @@ def generate_output_for_testcase(
552
313
  raise typer.Exit(1)
553
314
 
554
315
 
555
- def extract_generation_testcases(
556
- entries: List[TestcaseEntry],
557
- ) -> List[GenerationTestcaseEntry]:
558
- # TODO: support subgroups.
559
- groups = set(entry.group for entry in entries)
560
- entry_keys = set(entry.key() for entry in entries)
561
-
562
- res: List[GenerationTestcaseEntry] = []
563
-
564
- class ExtractGenerationTestcasesVisitor(TestcaseVisitor):
565
- def should_visit_group(self, group_name: str) -> bool:
566
- return group_name in groups
567
-
568
- def visit(self, entry: GenerationTestcaseEntry):
569
- # TODO: support subgroups.
570
- if entry.group_entry.key() not in entry_keys:
571
- return
572
- res.append(entry)
573
-
574
- run_testcase_visitor(ExtractGenerationTestcasesVisitor())
575
- return res
576
-
577
-
578
- def extract_generation_testcases_from_groups(
579
- groups: Optional[Set[str]] = None,
580
- ) -> List[GenerationTestcaseEntry]:
581
- res: List[GenerationTestcaseEntry] = []
582
-
583
- class ExtractGenerationTestcasesVisitor(TestcaseGroupVisitor):
584
- def visit(self, entry: GenerationTestcaseEntry):
585
- res.append(entry)
586
-
587
- run_testcase_visitor(ExtractGenerationTestcasesVisitor(groups))
588
- return res
589
-
590
-
591
- def extract_generation_testcases_from_patterns(
592
- patterns: List[TestcasePattern],
593
- ) -> List[GenerationTestcaseEntry]:
594
- res: List[GenerationTestcaseEntry] = []
595
-
596
- class ExtractGenerationTestcasesVisitor(TestcaseVisitor):
597
- def should_visit_group(self, group_name: str) -> bool:
598
- return any(pattern.intersecting_group(group_name) for pattern in patterns)
599
-
600
- def should_visit_subgroup(self, subgroup_path: str) -> bool:
601
- return any(
602
- pattern.intersecting_group(subgroup_path) for pattern in patterns
603
- )
604
-
605
- def visit(self, entry: GenerationTestcaseEntry):
606
- if not any(
607
- pattern.match(entry.group_entry) for pattern in patterns
608
- ) and not any(pattern.match(entry.subgroup_entry) for pattern in patterns):
609
- return
610
- res.append(entry)
611
-
612
- run_testcase_visitor(ExtractGenerationTestcasesVisitor())
613
- return res
614
-
615
-
616
316
  def generate_outputs_for_testcases(
617
317
  entries: List[TestcaseEntry],
618
318
  progress: Optional[StatusProgress] = None,
@@ -4,10 +4,10 @@ import pytest
4
4
 
5
5
  from rbx.box import package
6
6
  from rbx.box.generators import (
7
- extract_generation_testcases_from_groups,
8
7
  generate_outputs_for_testcases,
9
8
  generate_testcases,
10
9
  )
10
+ from rbx.box.testcase_extractors import extract_generation_testcases_from_groups
11
11
  from rbx.testing_utils import print_directory_tree
12
12
 
13
13
 
@@ -0,0 +1,7 @@
1
+ import sys
2
+
3
+ from rbx.box import main # noqa
4
+
5
+ if __name__ == '__main__':
6
+ for m in sys.modules:
7
+ print(m)
@@ -0,0 +1,25 @@
1
+ import subprocess
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ LAZY_MODULES = {
6
+ 'gitpython',
7
+ 'questionary',
8
+ 'fastapi',
9
+ 'requests',
10
+ 'pydantic_xml',
11
+ 'rbx.box.packaging.polygon.packager',
12
+ 'rbx.box.stresses',
13
+ }
14
+
15
+
16
+ def test_rich_not_imported_unnecessary():
17
+ file_path = Path(__file__).parent / 'lazy_importing_main.py'
18
+ result = subprocess.run(
19
+ [sys.executable, '-m', 'coverage', 'run', str(file_path)],
20
+ capture_output=True,
21
+ encoding='utf-8',
22
+ )
23
+ modules = result.stdout.splitlines()
24
+ modules = [module for module in modules if module in LAZY_MODULES]
25
+ assert not modules
rbx/box/main.py CHANGED
@@ -18,11 +18,9 @@ from typing import Annotated, List, Optional
18
18
  import rich
19
19
  import rich.prompt
20
20
  import typer
21
- import questionary
22
21
 
23
22
  from rbx import annotations, config, console, utils
24
23
  from rbx.box import (
25
- builder,
26
24
  cd,
27
25
  setter_config,
28
26
  state,
@@ -33,7 +31,6 @@ from rbx.box import (
33
31
  package,
34
32
  compile,
35
33
  presets,
36
- stresses,
37
34
  validators,
38
35
  )
39
36
  from rbx.box.contest import main as contest
@@ -116,6 +113,8 @@ def edit():
116
113
  @app.command('build, b', help='Build all tests for the problem.')
117
114
  @package.within_problem
118
115
  def build(verification: environment.VerificationParam):
116
+ from rbx.box import builder
117
+
119
118
  builder.build(verification=verification)
120
119
 
121
120
 
@@ -189,6 +188,8 @@ def run(
189
188
  console.console.print('[error]No solutions selected. Exiting.[/error]')
190
189
  raise typer.Exit(1)
191
190
 
191
+ from rbx.box import builder
192
+
192
193
  if not builder.build(verification=verification, output=check):
193
194
  return
194
195
 
@@ -322,6 +323,8 @@ def time(
322
323
  )
323
324
  check = False
324
325
 
326
+ from rbx.box import builder
327
+
325
328
  verification = VerificationLevel.ALL_SOLUTIONS.value
326
329
  if not builder.build(verification=verification, output=check):
327
330
  return None
@@ -522,6 +525,8 @@ def stress(
522
525
  )
523
526
  raise typer.Exit(1)
524
527
 
528
+ from rbx.box import stresses
529
+
525
530
  with utils.StatusProgress('Running stress...') as s:
526
531
  report = stresses.run_stress(
527
532
  name,
@@ -555,6 +560,8 @@ def stress(
555
560
  and group.generatorScript.path.suffix == '.txt'
556
561
  }
557
562
 
563
+ import questionary
564
+
558
565
  testgroup = questionary.select(
559
566
  'Choose the testgroup to add the tests to.\nOnly test groups that have a .txt generatorScript are shown below: ',
560
567
  choices=list(groups_by_name) + ['(create new script)', '(skip)'],
@@ -635,6 +642,8 @@ def compile_command(
635
642
  ),
636
643
  ):
637
644
  if path is None:
645
+ import questionary
646
+
638
647
  path = questionary.path("What's the path to your asset?").ask()
639
648
  if path is None:
640
649
  console.console.print('[error]No path specified.[/error]')
@@ -14,7 +14,6 @@ from rbx.box.packaging.packager import (
14
14
  BuiltContestStatement,
15
15
  BuiltProblemPackage,
16
16
  )
17
- from rbx.box.packaging.polygon.packager import PolygonContestPackager, PolygonPackager
18
17
 
19
18
  app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
20
19
 
@@ -77,6 +76,11 @@ def run_contest_packager(
77
76
  def polygon(
78
77
  verification: environment.VerificationParam,
79
78
  ):
79
+ from rbx.box.packaging.polygon.packager import (
80
+ PolygonContestPackager,
81
+ PolygonPackager,
82
+ )
83
+
80
84
  run_contest_packager(
81
85
  PolygonContestPackager, PolygonPackager, verification=verification
82
86
  )
rbx/box/packaging/main.py CHANGED
@@ -5,11 +5,9 @@ from typing import Type
5
5
  import typer
6
6
 
7
7
  from rbx import annotations, console
8
- from rbx.box import builder, environment, package
8
+ from rbx.box import environment, package
9
9
  from rbx.box.package import get_build_path
10
- from rbx.box.packaging.boca.packager import BocaPackager
11
10
  from rbx.box.packaging.packager import BasePackager, BuiltStatement
12
- from rbx.box.packaging.polygon.packager import PolygonPackager
13
11
  from rbx.box.statements.build_statements import build_statement
14
12
 
15
13
  app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
@@ -19,6 +17,8 @@ def run_packager(
19
17
  packager_cls: Type[BasePackager],
20
18
  verification: environment.VerificationParam,
21
19
  ) -> pathlib.Path:
20
+ from rbx.box import builder
21
+
22
22
  if not builder.verify(verification=verification):
23
23
  console.console.print(
24
24
  '[error]Build or verification failed, check the report.[/error]'
@@ -58,6 +58,8 @@ def run_packager(
58
58
  def polygon(
59
59
  verification: environment.VerificationParam,
60
60
  ):
61
+ from rbx.box.packaging.polygon.packager import PolygonPackager
62
+
61
63
  run_packager(PolygonPackager, verification=verification)
62
64
 
63
65
 
@@ -65,4 +67,6 @@ def polygon(
65
67
  def boca(
66
68
  verification: environment.VerificationParam,
67
69
  ):
70
+ from rbx.box.packaging.boca.packager import BocaPackager
71
+
68
72
  run_packager(BocaPackager, verification=verification)
@@ -3,8 +3,6 @@ import shutil
3
3
  import tempfile
4
4
  from typing import Annotated, Iterable, List, Optional, Sequence, Union
5
5
 
6
- import git
7
- import questionary
8
6
  import rich
9
7
  import rich.prompt
10
8
  import typer
@@ -306,6 +304,8 @@ def optionally_install_environment_from_preset(
306
304
  if env_path.is_file():
307
305
  if digest_file(preset_env_path) == digest_file(env_path):
308
306
  return
307
+ import questionary
308
+
309
309
  overwrite = questionary.confirm(
310
310
  'Preset environment file has changed. Overwrite?',
311
311
  default=False,
@@ -371,6 +371,8 @@ def _install(root: pathlib.Path = pathlib.Path(), force: bool = False):
371
371
 
372
372
 
373
373
  def install_from_remote(fetch_info: PresetFetchInfo, force: bool = False) -> str:
374
+ import git
375
+
374
376
  assert fetch_info.fetch_uri is not None
375
377
  with tempfile.TemporaryDirectory() as d:
376
378
  console.console.print(
@@ -484,6 +486,8 @@ def update(
484
486
 
485
487
  for preset_name in presets:
486
488
  if preset_name == LOCAL:
489
+ import questionary
490
+
487
491
  if not questionary.confirm(
488
492
  'Updating local preset will remove all custom changes you made to the preset.',
489
493
  default=False,
rbx/box/schema.py CHANGED
@@ -232,6 +232,13 @@ A generator script to call to generate testcases for this group.
232
232
  """,
233
233
  )
234
234
 
235
+ extraValidators: List[CodeItem] = Field(
236
+ default=[],
237
+ description="""
238
+ A list of extra validators to use to validate the testcases of this subgroup.
239
+ """,
240
+ )
241
+
235
242
  @model_validator(mode='after')
236
243
  def check_oneof(self) -> 'TestcaseSubgroup':
237
244
  _check_oneof(
@@ -260,7 +267,7 @@ A list of test subgroups to define for this group.
260
267
  default=None,
261
268
  description="""
262
269
  A validator to use to validate the testcases of this group.
263
- If not specified, will use the package-level validator.
270
+ If specified, will use this validator instead of the package-level validator.
264
271
  Useful in cases where the constraints vary across test groups.
265
272
  """,
266
273
  )
rbx/box/setter_config.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import functools
2
- import importlib
3
2
  import importlib.resources
4
3
  import pathlib
5
4
  import shlex