rbx.cp 0.5.61__py3-none-any.whl → 0.5.62__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 (39) hide show
  1. rbx/box/cd.py +14 -0
  2. rbx/box/cli.py +6 -0
  3. rbx/box/code.py +34 -5
  4. rbx/box/contest/main.py +6 -2
  5. rbx/box/git_utils.py +28 -0
  6. rbx/box/package.py +23 -0
  7. rbx/box/packaging/boca/packager.py +3 -18
  8. rbx/box/packaging/moj/packager.py +1 -1
  9. rbx/box/packaging/polygon/upload.py +7 -5
  10. rbx/box/presets/__init__.py +80 -6
  11. rbx/box/presets/fetch.py +18 -1
  12. rbx/box/retries.py +2 -0
  13. rbx/box/solutions.py +238 -113
  14. rbx/box/solutions_test.py +3 -1
  15. rbx/box/tasks.py +6 -1
  16. rbx/box/testcase_utils.py +3 -0
  17. rbx/box/ui/css/app.tcss +14 -2
  18. rbx/box/ui/main.py +3 -5
  19. rbx/box/ui/screens/error.py +19 -0
  20. rbx/box/ui/screens/run.py +4 -12
  21. rbx/box/ui/screens/run_explorer.py +77 -1
  22. rbx/box/ui/screens/run_test_explorer.py +155 -0
  23. rbx/box/ui/screens/selector.py +26 -0
  24. rbx/box/ui/screens/test_explorer.py +20 -5
  25. rbx/box/ui/utils/__init__.py +0 -0
  26. rbx/box/ui/utils/run_ui.py +95 -0
  27. rbx/box/ui/widgets/__init__.py +0 -0
  28. rbx/box/ui/widgets/file_log.py +3 -1
  29. rbx/box/ui/widgets/test_output_box.py +104 -0
  30. rbx/box/ui/widgets/two_sided_test_output_box.py +56 -0
  31. rbx/grading/steps.py +1 -0
  32. rbx/resources/packagers/boca/compile/java +55 -59
  33. rbx/resources/packagers/boca/interactive/java +2 -2
  34. rbx/resources/packagers/boca/run/java +2 -2
  35. {rbx_cp-0.5.61.dist-info → rbx_cp-0.5.62.dist-info}/METADATA +1 -1
  36. {rbx_cp-0.5.61.dist-info → rbx_cp-0.5.62.dist-info}/RECORD +39 -30
  37. {rbx_cp-0.5.61.dist-info → rbx_cp-0.5.62.dist-info}/LICENSE +0 -0
  38. {rbx_cp-0.5.61.dist-info → rbx_cp-0.5.62.dist-info}/WHEEL +0 -0
  39. {rbx_cp-0.5.61.dist-info → rbx_cp-0.5.62.dist-info}/entry_points.txt +0 -0
rbx/box/cd.py CHANGED
@@ -25,6 +25,20 @@ def find_package(root: pathlib.Path = pathlib.Path()) -> Optional[pathlib.Path]:
25
25
  return root
26
26
 
27
27
 
28
+ def is_problem_package(root: pathlib.Path = pathlib.Path()) -> bool:
29
+ dir = find_package(root)
30
+ if dir is None:
31
+ return False
32
+ return (dir / 'problem.rbx.yml').is_file()
33
+
34
+
35
+ def is_contest_package(root: pathlib.Path = pathlib.Path()) -> bool:
36
+ dir = find_package(root)
37
+ if dir is None:
38
+ return False
39
+ return (dir / 'contest.rbx.yml').is_file()
40
+
41
+
28
42
  def within_closest_package(func):
29
43
  @functools.wraps(func)
30
44
  def wrapper(*args, **kwargs):
rbx/box/cli.py CHANGED
@@ -112,6 +112,12 @@ def main(
112
112
  help='Whether to save extra debug logs along with the evaluation results.',
113
113
  ),
114
114
  ):
115
+ if cd.is_problem_package() and not package.is_cache_valid():
116
+ console.console.print(
117
+ '[warning]Cache is incompatible with the current version of [item]rbx[/item], so it will be cleared.[/warning]'
118
+ )
119
+ clear()
120
+
115
121
  state.STATE.run_through_cli = True
116
122
  state.STATE.sanitized = sanitized
117
123
  if sanitized:
rbx/box/code.py CHANGED
@@ -155,6 +155,34 @@ def _ignore_warning_in_cxx_input(input: GradingFileInput):
155
155
  input.src = preprocessed_path
156
156
 
157
157
 
158
+ def _maybe_rename_java_class(
159
+ compilable_path: pathlib.Path, file_mapping: FileMapping
160
+ ) -> pathlib.Path:
161
+ mapped_path = PosixPath(file_mapping.compilable)
162
+ if mapped_path.suffix != '.java':
163
+ return compilable_path
164
+ import re
165
+
166
+ cls_name = mapped_path.stem
167
+
168
+ java_content = compilable_path.read_text()
169
+ regex = re.compile(r'public\s+class\s+[A-Za-z0-9_$]+([^A-Za-z0-9_$])')
170
+ match = regex.search(java_content)
171
+ if match is None:
172
+ console.console.print(
173
+ f'[error]Java public class not found in file: [item]{compilable_path}[/item][/error]'
174
+ )
175
+ raise typer.Exit(1)
176
+
177
+ new_content = regex.sub(f'public class {cls_name}\\1', java_content)
178
+ if new_content == java_content:
179
+ return compilable_path
180
+
181
+ preprocessed_path = package.get_problem_preprocessed_path(compilable_path)
182
+ preprocessed_path.write_text(new_content)
183
+ return preprocessed_path
184
+
185
+
158
186
  def _format_stack_limit(limit: int) -> str:
159
187
  if limit == resource.RLIM_INFINITY:
160
188
  return 'unlimited'
@@ -372,11 +400,11 @@ def compile_item(
372
400
  ) -> str:
373
401
  _check_stack_limit()
374
402
 
375
- generator_path = PosixPath(code.path)
403
+ compilable_path = PosixPath(code.path)
376
404
 
377
- if not generator_path.is_file():
405
+ if not compilable_path.is_file():
378
406
  console.console.print(
379
- f'[error]Compilation file not found: [item]{generator_path}[/item][/error]'
407
+ f'[error]Compilation file not found: [item]{compilable_path}[/item][/error]'
380
408
  )
381
409
  raise typer.Exit(1)
382
410
 
@@ -389,7 +417,7 @@ def compile_item(
389
417
 
390
418
  if not compilation_options.commands:
391
419
  # Language is not compiled.
392
- return sandbox.file_cacher.put_file_from_path(generator_path)
420
+ return sandbox.file_cacher.put_file_from_path(compilable_path)
393
421
 
394
422
  commands = get_mapped_commands(compilation_options.commands, file_mapping)
395
423
  commands = add_warning_flags(commands, force_warnings)
@@ -417,8 +445,9 @@ def compile_item(
417
445
  download.maybe_add_testlib(code, artifacts)
418
446
  download.maybe_add_jngen(code, artifacts)
419
447
  download.maybe_add_rbx_header(code, artifacts)
448
+ compilable_path = _maybe_rename_java_class(compilable_path, file_mapping)
420
449
  artifacts.inputs.append(
421
- GradingFileInput(src=generator_path, dest=PosixPath(file_mapping.compilable))
450
+ GradingFileInput(src=compilable_path, dest=PosixPath(file_mapping.compilable))
422
451
  )
423
452
 
424
453
  artifacts.outputs.append(
rbx/box/contest/main.py CHANGED
@@ -63,8 +63,10 @@ def create(
63
63
  )
64
64
  raise typer.Exit(1)
65
65
 
66
- if fetch_info.fetch_uri is not None:
66
+ if fetch_info.is_remote():
67
67
  preset = presets.install_from_remote(fetch_info)
68
+ elif fetch_info.is_local_dir():
69
+ preset = presets.install_from_local_dir(fetch_info)
68
70
 
69
71
  preset_cfg = presets.get_installed_preset(preset)
70
72
  preset_path = (
@@ -108,7 +110,9 @@ def create(
108
110
  lock.unlink(missing_ok=True)
109
111
 
110
112
  if local:
111
- shutil.copytree(str(preset_path), str(dest_path / '.local.rbx'))
113
+ presets.copy_local_preset(
114
+ preset_path, dest_path, remote_uri=fetch_info.uri or preset_cfg.uri
115
+ )
112
116
 
113
117
  with cd.new_package_cd(dest_path):
114
118
  contest_utils.clear_all_caches()
rbx/box/git_utils.py ADDED
@@ -0,0 +1,28 @@
1
+ import pathlib
2
+ from typing import Optional
3
+
4
+ import git
5
+
6
+
7
+ def get_repo_or_nil(
8
+ root: pathlib.Path = pathlib.Path(), search_parent_directories: bool = False
9
+ ) -> Optional[git.Repo]:
10
+ try:
11
+ return git.Repo(root, search_parent_directories=search_parent_directories)
12
+ except git.InvalidGitRepositoryError:
13
+ return None
14
+
15
+
16
+ def is_repo(path: pathlib.Path) -> bool:
17
+ return get_repo_or_nil(path, search_parent_directories=False) is not None
18
+
19
+
20
+ def is_within_repo(path: pathlib.Path) -> bool:
21
+ return get_repo_or_nil(path, search_parent_directories=True) is not None
22
+
23
+
24
+ def get_any_remote(repo: git.Repo) -> Optional[git.Remote]:
25
+ for remote in repo.remotes:
26
+ if remote.exists():
27
+ return remote
28
+ return None
rbx/box/package.py CHANGED
@@ -33,6 +33,7 @@ from rbx.grading.judge.storage import FilesystemStorage, Storage
33
33
  YAML_NAME = 'problem.rbx.yml'
34
34
  _DEFAULT_CHECKER = 'wcmp.cpp'
35
35
  TEMP_DIR = None
36
+ CACHE_STEP_VERSION = 1
36
37
 
37
38
 
38
39
  def warn_preset_deactivated(root: pathlib.Path = pathlib.Path()):
@@ -135,10 +136,17 @@ def get_ruyaml(root: pathlib.Path = pathlib.Path()) -> Tuple[ruyaml.YAML, ruyaml
135
136
  return res, res.load(problem_yaml_path.read_text())
136
137
 
137
138
 
139
+ def _get_fingerprint() -> str:
140
+ return f'{CACHE_STEP_VERSION}'
141
+
142
+
138
143
  @functools.cache
139
144
  def get_problem_cache_dir(root: pathlib.Path = pathlib.Path()) -> pathlib.Path:
140
145
  cache_dir = find_problem(root) / '.box'
141
146
  cache_dir.mkdir(parents=True, exist_ok=True)
147
+ fingerprint_file = cache_dir / 'fingerprint'
148
+ if not fingerprint_file.is_file():
149
+ fingerprint_file.write_text(_get_fingerprint())
142
150
  return cache_dir
143
151
 
144
152
 
@@ -426,6 +434,21 @@ def get_merged_capture_path(root: pathlib.Path = pathlib.Path()) -> pathlib.Path
426
434
  return path
427
435
 
428
436
 
437
+ @functools.cache
438
+ def is_cache_valid(root: pathlib.Path = pathlib.Path()):
439
+ cache_dir = find_problem(root) / '.box'
440
+ if not cache_dir.is_dir():
441
+ return
442
+
443
+ fingerprint_file = cache_dir / 'fingerprint'
444
+ if not fingerprint_file.is_file():
445
+ return False
446
+ fingerprint = fingerprint_file.read_text()
447
+ if fingerprint.strip() != _get_fingerprint():
448
+ return False
449
+ return True
450
+
451
+
429
452
  def clear_package_cache():
430
453
  pkgs = [sys.modules[__name__]]
431
454
 
@@ -67,7 +67,7 @@ class BocaPackager(BasePackager):
67
67
  return (
68
68
  f'basename={self._get_problem_basename()}\n'
69
69
  f'fullname={statement.title}\n'
70
- f'descfile={self._get_problem_name()}.pdf\n'
70
+ f'descfile={self._get_problem_basename()}.pdf\n'
71
71
  )
72
72
 
73
73
  def _get_pkg_timelimit(self, language: BocaLanguage) -> int:
@@ -205,7 +205,7 @@ class BocaPackager(BasePackager):
205
205
  compile_text = compile_text.replace('{{rbxFlags}}', flags[language])
206
206
  return compile_text
207
207
 
208
- def _copy_solutions(self, into_path: pathlib.Path, fix_java: bool = True):
208
+ def _copy_solutions(self, into_path: pathlib.Path):
209
209
  into_path = into_path / 'solutions'
210
210
  for solution in package.get_solutions():
211
211
  dest_path = (
@@ -218,21 +218,6 @@ class BocaPackager(BasePackager):
218
218
  dest_path.parent.mkdir(parents=True, exist_ok=True)
219
219
  shutil.copy(str(solution.path), dest_path)
220
220
 
221
- if solution.path.suffix == '.java':
222
- java_content = dest_path.read_text()
223
- if (
224
- 'class Main ' not in java_content
225
- and f'class {self._get_problem_name()} ' not in java_content
226
- ):
227
- console.console.print(
228
- '[error]For BOCA packaging, Java solutions must be named `class Main` or `class <ProblemName>`.[/error]'
229
- )
230
- dest_path.write_text(
231
- java_content.replace(
232
- 'class Main ', f'class {self._get_problem_name()} '
233
- )
234
- )
235
-
236
221
  @classmethod
237
222
  def name(cls) -> str:
238
223
  return 'boca'
@@ -297,7 +282,7 @@ class BocaPackager(BasePackager):
297
282
  (description_path / 'problem.info').write_text(self._get_problem_info())
298
283
  shutil.copyfile(
299
284
  self._get_main_built_statement(built_statements).path,
300
- (description_path / self._get_problem_name()).with_suffix('.pdf'),
285
+ (description_path / self._get_problem_basename()).with_suffix('.pdf'),
301
286
  )
302
287
 
303
288
  # Copy solutions
@@ -215,7 +215,7 @@ class MojPackager(BocaPackager):
215
215
 
216
216
  # Copy solutions
217
217
  if self.for_boca:
218
- self._copy_solutions(into_path, fix_java=False)
218
+ self._copy_solutions(into_path)
219
219
  else:
220
220
  self._copy_solutions_moj(into_path)
221
221
 
@@ -307,11 +307,13 @@ def _upload_statement(problem: api.Problem):
307
307
  polygon_statement = api.Statement(
308
308
  encoding='utf-8',
309
309
  name=statement.title,
310
- legend=blocks.blocks.get('legend'),
311
- input=blocks.blocks.get('input'),
312
- output=blocks.blocks.get('output'),
313
- interaction=blocks.blocks.get('interaction'),
314
- notes=_get_notes_with_explanations(blocks),
310
+ legend=blocks.blocks.get('legend') or '',
311
+ input=blocks.blocks.get('input') or '',
312
+ output=blocks.blocks.get('output') or '',
313
+ interaction=(blocks.blocks.get('interaction') or '')
314
+ if pkg.type == TaskType.COMMUNICATION
315
+ else None,
316
+ notes=_get_notes_with_explanations(blocks) or '',
315
317
  )
316
318
  problem.save_statement(
317
319
  lang=code_to_langs([language])[0], problem_statement=polygon_statement
@@ -3,13 +3,14 @@ import shutil
3
3
  import tempfile
4
4
  from typing import Annotated, Iterable, List, Optional, Sequence, Union
5
5
 
6
+ import questionary
6
7
  import rich
7
8
  import rich.prompt
8
9
  import typer
9
10
  from iso639.language import functools
10
11
 
11
12
  from rbx import console, utils
12
- from rbx.box import cd
13
+ from rbx.box import cd, git_utils
13
14
  from rbx.box.environment import get_environment_path
14
15
  from rbx.box.presets.fetch import PresetFetchInfo, get_preset_fetch_info
15
16
  from rbx.box.presets.lock_schema import LockedAsset, PresetLock
@@ -363,11 +364,17 @@ def _install(root: pathlib.Path = pathlib.Path(), force: bool = False):
363
364
  if not res:
364
365
  raise typer.Exit(1)
365
366
  shutil.rmtree(str(installation_path), ignore_errors=True)
366
- shutil.copytree(str(root), str(installation_path))
367
+ copy_tree_normalizing_gitdir(root, installation_path)
367
368
  shutil.rmtree(str(installation_path / 'build'), ignore_errors=True)
368
369
  shutil.rmtree(str(installation_path / '.box'), ignore_errors=True)
369
370
  shutil.rmtree(str(installation_path / '.local.rbx'), ignore_errors=True)
370
- shutil.rmtree(str(installation_path / '.git'), ignore_errors=True)
371
+
372
+
373
+ def install_from_local_dir(fetch_info: PresetFetchInfo, force: bool = False) -> str:
374
+ pd = pathlib.Path(fetch_info.inner_dir)
375
+ preset = get_preset_yaml(pd)
376
+ _install(pd, force=force)
377
+ return preset.name
371
378
 
372
379
 
373
380
  def install_from_remote(fetch_info: PresetFetchInfo, force: bool = False) -> str:
@@ -452,12 +459,75 @@ def _sync(try_update: bool = False):
452
459
  generate_lock(preset_lock.preset_name)
453
460
 
454
461
 
462
+ def copy_tree_normalizing_gitdir(src_path: pathlib.Path, dst_path: pathlib.Path):
463
+ shutil.copytree(str(src_path), str(dst_path))
464
+ if not (src_path / '.git').is_file():
465
+ return
466
+
467
+ src_repo = git_utils.get_repo_or_nil(src_path)
468
+ if src_repo is None:
469
+ return
470
+
471
+ gitdir_dst = dst_path / '.git'
472
+ shutil.rmtree(str(gitdir_dst), ignore_errors=True)
473
+ gitdir_dst.unlink(missing_ok=True)
474
+
475
+ shutil.copytree(str(src_repo.git_dir), str(gitdir_dst))
476
+
477
+
478
+ def copy_local_preset(
479
+ preset_path: pathlib.Path, dest_path: pathlib.Path, remote_uri: Optional[str] = None
480
+ ):
481
+ copy_tree_normalizing_gitdir(preset_path, dest_path / '.local.rbx')
482
+
483
+ from rbx.box import git_utils
484
+
485
+ preset_repo = git_utils.get_repo_or_nil(preset_path)
486
+ current_repo = git_utils.get_repo_or_nil(
487
+ pathlib.Path.cwd(), search_parent_directories=True
488
+ )
489
+
490
+ if preset_repo is None or current_repo is None:
491
+ return
492
+
493
+ fetch_info = get_preset_fetch_info(remote_uri)
494
+ remote_uri = fetch_info.fetch_uri if fetch_info is not None else None
495
+
496
+ preset_remote = git_utils.get_any_remote(preset_repo)
497
+ preset_remote_uri = preset_remote.url if preset_remote is not None else remote_uri
498
+ if preset_remote_uri is None:
499
+ return
500
+
501
+ add_submodule = questionary.confirm(
502
+ 'The preset is installed from a remote Git repository. Do you want to add it as a submodule of your project?',
503
+ default=False,
504
+ ).ask()
505
+ if not add_submodule:
506
+ return
507
+
508
+ dest_path_rel = dest_path.resolve().relative_to(pathlib.Path.cwd().resolve())
509
+ path_str = str(dest_path_rel / '.local.rbx')
510
+ try:
511
+ current_repo.git.submodule('add', preset_remote_uri, path_str)
512
+ except Exception as e:
513
+ console.console.print('[error]Failed to add preset as a submodule.[/error]')
514
+ console.console.print(f'[error]Error:[/error] {e}')
515
+ console.console.print(
516
+ '[error]You might want to do this manually with the [item]git submodule add[/item] command.[/error]'
517
+ )
518
+ raise typer.Exit(1) from None
519
+ console.console.print(
520
+ f'[success]Preset [item]{preset_remote_uri}[/item] was added as a submodule to your project at [item]{path_str}[/item].[/success]'
521
+ )
522
+
523
+
455
524
  @app.command(
456
525
  'install', help='Install preset from current directory or from the given URI.'
457
526
  )
458
527
  def install(
459
528
  uri: Optional[str] = typer.Argument(
460
- None, help='GitHub URI for the preset to install.'
529
+ None,
530
+ help='URI for the preset to install. Might be a Github repository, or even a local path.',
461
531
  ),
462
532
  ):
463
533
  if uri is None:
@@ -468,9 +538,13 @@ def install(
468
538
  if fetch_info is None:
469
539
  console.console.print(f'[error] Preset with URI {uri} not found.[/error]')
470
540
  raise typer.Exit(1)
471
- if fetch_info.fetch_uri is None:
541
+ if not fetch_info.is_local_dir() and not fetch_info.is_remote():
472
542
  console.console.print(f'[error]URI {uri} is invalid.[/error]')
473
- install_from_remote(fetch_info)
543
+ raise typer.Exit(1)
544
+ if fetch_info.is_remote():
545
+ install_from_remote(fetch_info)
546
+ else:
547
+ install_from_local_dir(fetch_info)
474
548
 
475
549
 
476
550
  @app.command('update', help='Update installed remote presets')
rbx/box/presets/fetch.py CHANGED
@@ -1,3 +1,4 @@
1
+ import pathlib
1
2
  import re
2
3
  from typing import Optional
3
4
 
@@ -17,6 +18,12 @@ class PresetFetchInfo(BaseModel):
17
18
  # Inner directory from where to pull the preset.
18
19
  inner_dir: str = ''
19
20
 
21
+ def is_remote(self) -> bool:
22
+ return self.fetch_uri is not None
23
+
24
+ def is_local_dir(self) -> bool:
25
+ return bool(self.inner_dir) and not self.is_remote()
26
+
20
27
 
21
28
  def get_preset_fetch_info(uri: Optional[str]) -> Optional[PresetFetchInfo]:
22
29
  if uri is None:
@@ -36,7 +43,7 @@ def get_preset_fetch_info(uri: Optional[str]) -> Optional[PresetFetchInfo]:
36
43
  )
37
44
 
38
45
  def get_short_github_fetch_info(s: str) -> Optional[PresetFetchInfo]:
39
- pattern = r'([\w\-]+\/[\w\.\-]+)(?:\/(.*))?'
46
+ pattern = r'(?:\@gh/)?([\w\-]+\/[\w\.\-]+)(?:\/(.*))?'
40
47
  compiled = re.compile(pattern)
41
48
  match = compiled.match(s)
42
49
  if match is None:
@@ -48,6 +55,15 @@ def get_preset_fetch_info(uri: Optional[str]) -> Optional[PresetFetchInfo]:
48
55
  inner_dir=match.group(2) or '',
49
56
  )
50
57
 
58
+ def get_local_dir_fetch_info(s: str) -> Optional[PresetFetchInfo]:
59
+ try:
60
+ path = pathlib.Path(s)
61
+ if not path.exists():
62
+ return None
63
+ except Exception:
64
+ return None
65
+ return PresetFetchInfo(name=path.name, inner_dir=str(path))
66
+
51
67
  def get_local_fetch_info(s: str) -> Optional[PresetFetchInfo]:
52
68
  pattern = r'[\w\-]+'
53
69
  compiled = re.compile(pattern)
@@ -59,6 +75,7 @@ def get_preset_fetch_info(uri: Optional[str]) -> Optional[PresetFetchInfo]:
59
75
  extractors = [
60
76
  get_github_fetch_info,
61
77
  get_short_github_fetch_info,
78
+ get_local_dir_fetch_info,
62
79
  get_local_fetch_info,
63
80
  ]
64
81
 
rbx/box/retries.py CHANGED
@@ -88,6 +88,8 @@ def _move_logs_to_temp_dir(
88
88
  recover.append(_move_to_temp_dir(eval.log.stderr_absolute_path, temp_dir))
89
89
  if eval.log.log_absolute_path is not None and eval.log.log_absolute_path.exists():
90
90
  recover.append(_move_to_temp_dir(eval.log.log_absolute_path, temp_dir))
91
+ if eval.log.eval_absolute_path is not None and eval.log.eval_absolute_path.exists():
92
+ recover.append(_move_to_temp_dir(eval.log.eval_absolute_path, temp_dir))
91
93
  return recover
92
94
 
93
95