rbx.cp 0.5.73__py3-none-any.whl → 0.6.1__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 (86) hide show
  1. rbx/annotations.py +21 -1
  2. rbx/box/cd.py +11 -1
  3. rbx/box/checkers.py +9 -1
  4. rbx/box/cli.py +59 -46
  5. rbx/box/code.py +142 -3
  6. rbx/box/contest/build_contest_statements.py +44 -34
  7. rbx/box/contest/contest_package.py +4 -7
  8. rbx/box/contest/main.py +7 -58
  9. rbx/box/contest/schema.py +52 -8
  10. rbx/box/contest/statements.py +53 -25
  11. rbx/box/creation.py +3 -36
  12. rbx/box/environment.py +21 -9
  13. rbx/box/fields.py +35 -0
  14. rbx/box/lang.py +27 -0
  15. rbx/box/linting.py +26 -0
  16. rbx/box/package.py +4 -35
  17. rbx/box/packaging/boca/packager.py +48 -5
  18. rbx/box/packaging/contest_main.py +13 -0
  19. rbx/box/packaging/main.py +13 -2
  20. rbx/box/packaging/packager.py +4 -4
  21. rbx/box/packaging/pkg/packager.py +142 -0
  22. rbx/box/packaging/polygon/packager.py +2 -24
  23. rbx/box/packaging/polygon/upload.py +35 -17
  24. rbx/box/presets/__init__.py +362 -281
  25. rbx/box/presets/lock_schema.py +1 -2
  26. rbx/box/presets/schema.py +13 -5
  27. rbx/box/remote.py +2 -2
  28. rbx/box/retries.py +8 -0
  29. rbx/box/schema.py +82 -19
  30. rbx/box/solutions.py +77 -15
  31. rbx/box/statements/build_statements.py +44 -27
  32. rbx/box/statements/builders.py +18 -10
  33. rbx/box/statements/expander.py +49 -0
  34. rbx/box/statements/latex_jinja.py +61 -4
  35. rbx/box/statements/schema.py +33 -9
  36. rbx/box/stats.py +92 -0
  37. rbx/box/tasks.py +6 -3
  38. rbx/box/testcase_utils.py +19 -47
  39. rbx/box/tooling/__init__.py +0 -0
  40. rbx/box/tooling/boca/__init__.py +0 -0
  41. rbx/box/tooling/boca/main.py +13 -0
  42. rbx/box/tooling/boca/scrape.py +34 -0
  43. rbx/box/{packaging/boca/upload.py → tooling/boca/scraper.py} +77 -8
  44. rbx/box/tooling/main.py +8 -0
  45. rbx/box/ui/utils/run_ui.py +1 -1
  46. rbx/box/ui/widgets/interaction_box.py +19 -1
  47. rbx/grading/caching.py +18 -2
  48. rbx/grading/judge/sandbox.py +60 -5
  49. rbx/grading/judge/sandboxes/isolate.py +1 -0
  50. rbx/grading/judge/sandboxes/stupid_sandbox.py +11 -5
  51. rbx/grading/judge/sandboxes/timeit.py +36 -15
  52. rbx/grading/processing_context.py +62 -78
  53. rbx/grading/steps.py +92 -40
  54. rbx/resources/packagers/boca/checker.sh +4 -1
  55. rbx/resources/packagers/boca/compile/c +2 -6
  56. rbx/resources/packagers/boca/compile/cc +2 -6
  57. rbx/resources/packagers/boca/compile/cpp +2 -6
  58. rbx/resources/packagers/boca/compile/java +1 -6
  59. rbx/resources/packagers/boca/compile/kt +24 -28
  60. rbx/resources/packagers/boca/compile/py2 +2 -6
  61. rbx/resources/packagers/boca/compile/py3 +2 -6
  62. rbx/resources/packagers/boca/interactive/c +15 -83
  63. rbx/resources/packagers/boca/interactive/cc +15 -83
  64. rbx/resources/packagers/boca/interactive/cpp +15 -83
  65. rbx/resources/packagers/boca/interactive/java +15 -88
  66. rbx/resources/packagers/boca/interactive/kt +15 -88
  67. rbx/resources/packagers/boca/interactive/py2 +15 -88
  68. rbx/resources/packagers/boca/interactive/py3 +15 -88
  69. rbx/resources/packagers/boca/interactor_compile.sh +5 -2
  70. rbx/resources/packagers/boca/interactor_run.sh +174 -0
  71. rbx/resources/packagers/boca/safeexec.c +530 -0
  72. rbx/resources/packagers/boca/safeexec_compile.sh +49 -0
  73. rbx/resources/presets/default/contest/contest.rbx.yml +9 -8
  74. rbx/resources/presets/default/problem/problem.rbx.yml +38 -26
  75. rbx/resources/presets/default/problem/random.txt +3 -1
  76. rbx/resources/presets/default/problem/rbx.h +92 -0
  77. rbx/resources/presets/default/problem/statement/statement.rbx.tex +4 -7
  78. rbx/resources/presets/default/problem/validator.cpp +8 -8
  79. rbx/resources/templates/rbx.h +2 -3
  80. {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.1.dist-info}/METADATA +23 -6
  81. {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.1.dist-info}/RECORD +84 -71
  82. {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.1.dist-info}/WHEEL +1 -1
  83. rbx/resources/packagers/boca/compile/pas +0 -172
  84. rbx/resources/presets/default/problem/statement/projecao.png +0 -0
  85. {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.1.dist-info}/LICENSE +0 -0
  86. {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.1.dist-info}/entry_points.txt +0 -0
@@ -32,11 +32,11 @@ class BocaPackager(BasePackager):
32
32
  def _get_main_statement(self) -> Statement:
33
33
  pkg = package.find_problem_package_or_die()
34
34
 
35
- if not pkg.statements:
35
+ if not pkg.expanded_statements:
36
36
  console.console.print('[error]No statements found.[/error]')
37
37
  raise typer.Exit(1)
38
38
 
39
- return pkg.statements[0]
39
+ return pkg.expanded_statements[0]
40
40
 
41
41
  def _get_main_built_statement(
42
42
  self, built_statements: List[BuiltStatement]
@@ -119,10 +119,18 @@ class BocaPackager(BasePackager):
119
119
 
120
120
  def _get_limits(self, language: BocaLanguage) -> str:
121
121
  pkg = package.find_problem_package_or_die()
122
- no_of_runs = self._get_number_of_runs(language)
122
+ if pkg.type == TaskType.COMMUNICATION:
123
+ # Interactive tasks only support a single run.
124
+ no_of_runs = 1
125
+ time_limit = f'{self._get_pkg_timelimit(language) / 1000:.2f}'
126
+ else:
127
+ no_of_runs = self._get_number_of_runs(language)
128
+ time_limit = test_time(
129
+ self._get_pkg_timelimit(language) / 1000 * no_of_runs
130
+ )
123
131
  return (
124
132
  '#!/bin/bash\n'
125
- f'echo {test_time(self._get_pkg_timelimit(language) / 1000 * no_of_runs)}\n'
133
+ f'echo {time_limit}\n'
126
134
  f'echo {no_of_runs}\n'
127
135
  f'echo {self._get_pkg_memorylimit(language)}\n'
128
136
  f'echo {pkg.outputLimit}\n'
@@ -176,6 +184,25 @@ class BocaPackager(BasePackager):
176
184
  '{{rbxFlags}}', extension.flags_with_defaults()['cc']
177
185
  ).replace('{{interactor_content}}', interactor)
178
186
 
187
+ def _get_safeexec(self) -> str:
188
+ safeexec_script_path = (
189
+ get_default_app_path() / 'packagers' / 'boca' / 'safeexec_compile.sh'
190
+ )
191
+ safeexec_path = get_default_app_path() / 'packagers' / 'boca' / 'safeexec.c'
192
+ if not safeexec_script_path.exists():
193
+ console.console.print(
194
+ '[error]BOCA template safeexec compile script not found.[/error]'
195
+ )
196
+ raise typer.Exit(1)
197
+ if not safeexec_path.exists():
198
+ console.console.print(
199
+ '[error]BOCA template safeexec source code not found.[/error]'
200
+ )
201
+ raise typer.Exit(1)
202
+ return safeexec_script_path.read_text().replace(
203
+ '{{safeexec_content}}', safeexec_path.read_text()
204
+ )
205
+
179
206
  def _get_compile(self, language: BocaLanguage) -> str:
180
207
  pkg = package.find_problem_package_or_die()
181
208
  extension = get_extension_or_default('boca', BocaExtension)
@@ -196,6 +223,9 @@ class BocaPackager(BasePackager):
196
223
  compile_text = compile_text.replace(
197
224
  'umask 0022', 'umask 0022\n\n' + self._get_interactor()
198
225
  )
226
+ compile_text = compile_text.replace(
227
+ 'umask 0022', 'umask 0022\n\n' + self._get_safeexec()
228
+ )
199
229
  compile_text = compile_text.replace(
200
230
  'umask 0022', 'umask 0022\n\n' + self._get_checker()
201
231
  )
@@ -206,7 +236,6 @@ class BocaPackager(BasePackager):
206
236
  return compile_text
207
237
 
208
238
  def _copy_solutions(self, into_path: pathlib.Path):
209
- into_path = into_path / 'solutions'
210
239
  for solution in package.get_solutions():
211
240
  dest_path = (
212
241
  into_path
@@ -218,6 +247,19 @@ class BocaPackager(BasePackager):
218
247
  dest_path.parent.mkdir(parents=True, exist_ok=True)
219
248
  shutil.copy(str(solution.path), dest_path)
220
249
 
250
+ def _expand_run_script(self, run_path: pathlib.Path):
251
+ pkg = package.find_problem_package_or_die()
252
+ if pkg.type == TaskType.COMMUNICATION:
253
+ runit_content = (
254
+ get_default_app_path() / 'packagers' / 'boca' / 'interactor_run.sh'
255
+ ).read_text()
256
+ run_path.write_text(
257
+ run_path.read_text().replace(
258
+ '{{runit_content}}',
259
+ runit_content,
260
+ )
261
+ )
262
+
221
263
  @classmethod
222
264
  def name(cls) -> str:
223
265
  return 'boca'
@@ -263,6 +305,7 @@ class BocaPackager(BasePackager):
263
305
  )
264
306
  raise typer.Exit(1)
265
307
  shutil.copyfile(run_orig_path, run_path / language)
308
+ self._expand_run_script(run_path / language)
266
309
 
267
310
  # Prepare compile.
268
311
  compile_path = into_path / 'compile'
@@ -86,3 +86,16 @@ async def polygon(
86
86
  await run_contest_packager(
87
87
  PolygonContestPackager, PolygonPackager, verification=verification
88
88
  )
89
+
90
+
91
+ @app.command('pkg', help='Build a contest package for PKG.')
92
+ @contest_package.within_contest
93
+ @syncer.sync
94
+ async def pkg(
95
+ verification: environment.VerificationParam,
96
+ ):
97
+ from rbx.box.packaging.pkg.packager import PkgContestPackager, PkgPackager
98
+
99
+ await run_contest_packager(
100
+ PkgContestPackager, PkgPackager, verification=verification
101
+ )
rbx/box/packaging/main.py CHANGED
@@ -114,9 +114,9 @@ async def boca(
114
114
  result_path = await run_packager(BocaPackager, verification=verification)
115
115
 
116
116
  if upload:
117
- from rbx.box.packaging.boca.upload import get_boca_uploader
117
+ from rbx.box.tooling.boca.scraper import get_boca_scraper
118
118
 
119
- uploader = get_boca_uploader()
119
+ uploader = get_boca_scraper()
120
120
  uploader.login_and_upload(result_path)
121
121
 
122
122
 
@@ -132,3 +132,14 @@ async def moj(
132
132
  from rbx.box.packaging.moj.packager import MojPackager
133
133
 
134
134
  await run_packager(MojPackager, verification=verification, for_boca=for_boca)
135
+
136
+
137
+ @app.command('pkg', help='Build a package for PKG.')
138
+ @package.within_problem
139
+ @syncer.sync
140
+ async def pkg(
141
+ verification: environment.VerificationParam,
142
+ ):
143
+ from rbx.box.packaging.pkg.packager import PkgPackager
144
+
145
+ await run_packager(PkgPackager, verification=verification)
@@ -46,7 +46,7 @@ class BasePackager(ABC):
46
46
  pkg = package.find_problem_package_or_die()
47
47
 
48
48
  res = set()
49
- for statement in pkg.statements:
49
+ for statement in pkg.expanded_statements:
50
50
  res.add(statement.language)
51
51
  return list(res)
52
52
 
@@ -89,7 +89,7 @@ class BasePackager(ABC):
89
89
 
90
90
  def get_statement_for_language(self, lang: str) -> Statement:
91
91
  pkg = package.find_problem_package_or_die()
92
- for statement in pkg.statements:
92
+ for statement in pkg.expanded_statements:
93
93
  if statement.language == lang:
94
94
  return statement
95
95
  raise
@@ -114,7 +114,7 @@ class BaseContestPackager(ABC):
114
114
  pkg = contest_package.find_contest_package_or_die()
115
115
 
116
116
  res = set()
117
- for statement in pkg.statements:
117
+ for statement in pkg.expanded_statements:
118
118
  res.add(statement.language)
119
119
  return list(res)
120
120
 
@@ -123,7 +123,7 @@ class BaseContestPackager(ABC):
123
123
 
124
124
  def get_statement_for_language(self, lang: str) -> ContestStatement:
125
125
  contest = contest_package.find_contest_package_or_die()
126
- for statement in contest.statements:
126
+ for statement in contest.expanded_statements:
127
127
  if statement.language == lang:
128
128
  return statement
129
129
  raise
@@ -0,0 +1,142 @@
1
+ import pathlib
2
+ import shutil
3
+ from typing import List, Optional
4
+
5
+ from rbx.box import naming, package
6
+ from rbx.box.contest import contest_package
7
+ from rbx.box.contest.schema import ContestStatement
8
+ from rbx.box.packaging.packager import (
9
+ BaseContestPackager,
10
+ BasePackager,
11
+ BuiltContestStatement,
12
+ BuiltProblemPackage,
13
+ BuiltStatement,
14
+ )
15
+ from rbx.box.schema import ExpectedOutcome, TaskType
16
+ from rbx.box.statements.schema import Statement
17
+
18
+
19
+ class PkgPackager(BasePackager):
20
+ @classmethod
21
+ def task_types(cls) -> List[TaskType]:
22
+ return [TaskType.BATCH, TaskType.COMMUNICATION]
23
+
24
+ @classmethod
25
+ def name(cls) -> str:
26
+ return 'pkg'
27
+
28
+ def _get_problem_basename(self) -> str:
29
+ shortname = naming.get_problem_shortname()
30
+ if shortname is not None:
31
+ return shortname
32
+ return self.package_basename()
33
+
34
+ def _get_main_statement(self) -> Optional[Statement]:
35
+ pkg = package.find_problem_package_or_die()
36
+ if not pkg.expanded_statements:
37
+ return None
38
+ return pkg.expanded_statements[0]
39
+
40
+ def _get_main_built_statement(
41
+ self, built_statements: List[BuiltStatement]
42
+ ) -> Optional[BuiltStatement]:
43
+ statement = self._get_main_statement()
44
+ if statement is None:
45
+ return None
46
+ for built_statement in built_statements:
47
+ if built_statement.statement == statement:
48
+ return built_statement
49
+ return None
50
+
51
+ def _copy_accepted_solutions(self, into_path: pathlib.Path):
52
+ for solution in package.get_solutions():
53
+ if solution.outcome != ExpectedOutcome.ACCEPTED:
54
+ continue
55
+ dest_path = into_path / solution.path.name
56
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
57
+ shutil.copy(str(solution.path), dest_path)
58
+
59
+ def package(
60
+ self,
61
+ build_path: pathlib.Path,
62
+ into_path: pathlib.Path,
63
+ built_statements: List[BuiltStatement],
64
+ ) -> pathlib.Path:
65
+ into_path.mkdir(parents=True, exist_ok=True)
66
+
67
+ main_statement = self._get_main_built_statement(built_statements)
68
+ if main_statement is not None:
69
+ statement_path = into_path / 'statement.pdf'
70
+ shutil.copyfile(main_statement.path, statement_path)
71
+
72
+ # Prepare tests
73
+ tests_path = into_path / 'tests'
74
+ tests_path.mkdir(parents=True, exist_ok=True)
75
+
76
+ testcases = self.get_flattened_built_testcases()
77
+ for i, testcase in enumerate(testcases):
78
+ shutil.copyfile(testcase.inputPath, tests_path / f'{i + 1:03d}.in')
79
+ if testcase.outputPath is not None:
80
+ shutil.copyfile(testcase.outputPath, tests_path / f'{i + 1:03d}.ans')
81
+ else:
82
+ (tests_path / f'{i + 1:03d}.ans').touch()
83
+
84
+ # Copy solutions.
85
+ solutions_path = into_path / 'solutions'
86
+ solutions_path.mkdir(parents=True, exist_ok=True)
87
+ self._copy_accepted_solutions(solutions_path)
88
+
89
+ # Zip all.
90
+ shutil.make_archive(
91
+ str(build_path / self._get_problem_basename()), 'zip', into_path
92
+ )
93
+
94
+ return (build_path / self._get_problem_basename()).with_suffix('.zip')
95
+
96
+
97
+ class PkgContestPackager(BaseContestPackager):
98
+ @classmethod
99
+ def name(cls) -> str:
100
+ return 'pkg'
101
+
102
+ def _get_main_statement(self) -> Optional[ContestStatement]:
103
+ pkg = contest_package.find_contest_package_or_die()
104
+ if not pkg.expanded_statements:
105
+ return None
106
+ return pkg.expanded_statements[0]
107
+
108
+ def _get_main_built_statement(
109
+ self, built_statements: List[BuiltContestStatement]
110
+ ) -> Optional[BuiltContestStatement]:
111
+ statement = self._get_main_statement()
112
+ if statement is None:
113
+ return None
114
+ for built_statement in built_statements:
115
+ if built_statement.statement == statement:
116
+ return built_statement
117
+ return None
118
+
119
+ def package(
120
+ self,
121
+ built_packages: List[BuiltProblemPackage],
122
+ build_path: pathlib.Path,
123
+ into_path: pathlib.Path,
124
+ built_statements: List[BuiltContestStatement],
125
+ ) -> pathlib.Path:
126
+ into_path.mkdir(parents=True, exist_ok=True)
127
+
128
+ # Add contest-level statement.
129
+ main_statement = self._get_main_built_statement(built_statements)
130
+ if main_statement is not None:
131
+ statement_path = into_path / 'statement.pdf'
132
+ shutil.copyfile(main_statement.path, statement_path)
133
+
134
+ # Add problems.
135
+ for built_package in built_packages:
136
+ pkg_path = into_path / built_package.problem.short_name
137
+ shutil.unpack_archive(built_package.path, pkg_path, format='zip')
138
+
139
+ # Zip all.
140
+ shutil.make_archive(str(build_path / 'contest'), 'zip', into_path)
141
+
142
+ return (build_path / 'contest').with_suffix('.zip')
@@ -1,13 +1,12 @@
1
- import functools
2
1
  import pathlib
3
2
  import shutil
4
3
  from typing import List, Optional
5
4
 
6
- import iso639
7
5
  import typer
8
6
 
9
7
  from rbx import console
10
8
  from rbx.box import header, package
9
+ from rbx.box.lang import code_to_langs, is_valid_lang_code
11
10
  from rbx.box.packaging.packager import (
12
11
  BaseContestPackager,
13
12
  BasePackager,
@@ -28,27 +27,6 @@ DAT_TEMPLATE = """
28
27
  """
29
28
 
30
29
 
31
- def langs_to_code(langs: List[str]) -> List[str]:
32
- return [iso639.Language.from_name(lang).part1 for lang in langs]
33
-
34
-
35
- def code_to_langs(langs: List[str]) -> List[str]:
36
- return [iso639.Language.from_part1(lang).name.lower() for lang in langs]
37
-
38
-
39
- @functools.cache
40
- def is_valid_lang_code(lang: str) -> bool:
41
- try:
42
- code_to_langs([lang])
43
- except iso639.LanguageNotFoundError:
44
- console.console.print(
45
- f'[warning]Language [item]{lang}[/item] is being skipped because it is not a iso639 language.[/warning]'
46
- )
47
- return False
48
-
49
- return True
50
-
51
-
52
30
  class PolygonPackager(BasePackager):
53
31
  @classmethod
54
32
  def task_types(cls) -> List[TaskType]:
@@ -59,7 +37,7 @@ class PolygonPackager(BasePackager):
59
37
  pkg = package.find_problem_package_or_die()
60
38
 
61
39
  lang_codes = set()
62
- for statement in pkg.statements:
40
+ for statement in pkg.expanded_statements:
63
41
  lang_codes.add(statement.title)
64
42
 
65
43
  for lang in langs:
@@ -10,8 +10,8 @@ import typer
10
10
  from rbx import console
11
11
  from rbx.box import header, package
12
12
  from rbx.box.generators import get_all_built_testcases
13
+ from rbx.box.lang import code_to_langs, is_valid_lang_code
13
14
  from rbx.box.packaging.polygon import polygon_api as api
14
- from rbx.box.packaging.polygon.packager import code_to_langs, is_valid_lang_code
15
15
  from rbx.box.schema import CodeItem, ExpectedOutcome, Solution, TaskType, Testcase
16
16
  from rbx.box.statements.build_statements import get_relative_assets
17
17
  from rbx.box.statements.builders import (
@@ -20,7 +20,11 @@ from rbx.box.statements.builders import (
20
20
  render_jinja_blocks,
21
21
  )
22
22
  from rbx.box.statements.schema import Statement, StatementType
23
- from rbx.box.testcase_utils import get_alternate_interaction_texts, parse_interaction
23
+ from rbx.box.testcase_utils import (
24
+ TestcaseInteractionParsingError,
25
+ get_alternate_interaction_texts,
26
+ parse_interaction,
27
+ )
24
28
 
25
29
  _API_URL = 'https://polygon.codeforces.com/api'
26
30
 
@@ -163,17 +167,23 @@ def _get_test_params_for_statement(
163
167
 
164
168
  pio_path = testcase.outputPath.with_suffix('.pio')
165
169
  if pio_path.is_file():
166
- interaction = parse_interaction(pio_path)
167
- res['test_input_for_statements'], res['test_output_for_statements'] = (
168
- get_alternate_interaction_texts(interaction)
169
- )
170
- else:
171
- pin_path = testcase.outputPath.with_suffix('.pin')
172
- if pin_path.is_file():
173
- res['test_input_for_statements'] = pin_path.read_text()
174
- pout_path = testcase.outputPath.with_suffix('.pout')
175
- if pout_path.is_file():
176
- res['test_output_for_statements'] = pout_path.read_text()
170
+ try:
171
+ interaction = parse_interaction(pio_path)
172
+ except TestcaseInteractionParsingError:
173
+ pass
174
+ else:
175
+ res['test_input_for_statements'], res['test_output_for_statements'] = (
176
+ get_alternate_interaction_texts(interaction)
177
+ )
178
+ return res
179
+
180
+ # .pio does not exist or is not parseable, fallback to .pin and .pout.
181
+ pin_path = testcase.outputPath.with_suffix('.pin')
182
+ if pin_path.is_file():
183
+ res['test_input_for_statements'] = pin_path.read_text()
184
+ pout_path = testcase.outputPath.with_suffix('.pout')
185
+ if pout_path.is_file():
186
+ res['test_output_for_statements'] = pout_path.read_text()
177
187
  return res
178
188
 
179
189
 
@@ -229,7 +239,7 @@ def _upload_solutions(problem: api.Problem):
229
239
 
230
240
  def _get_statement_for_language(language: str) -> Optional[Statement]:
231
241
  pkg = package.find_problem_package_or_die()
232
- for statement in pkg.statements:
242
+ for statement in pkg.expanded_statements:
233
243
  if statement.language == language:
234
244
  return statement
235
245
  return None
@@ -290,11 +300,15 @@ def _upload_statement(problem: api.Problem, preserve_language: bool = False):
290
300
  pkg = package.find_problem_package_or_die()
291
301
 
292
302
  languages = set()
293
- for statement in pkg.statements:
303
+ for statement in pkg.expanded_statements:
294
304
  if not is_valid_lang_code(statement.language):
295
305
  continue
296
306
  languages.add(statement.language)
297
- for language in languages:
307
+
308
+ uploaded_languages = set()
309
+
310
+ # Prioritize English statements.
311
+ for language in ['en'] + list(languages):
298
312
  statement = _get_statement_for_language(language)
299
313
  if statement is None:
300
314
  continue
@@ -304,6 +318,10 @@ def _upload_statement(problem: api.Problem, preserve_language: bool = False):
304
318
  console.console.print(
305
319
  f'Uploading statement for language [item]{language}[/item] (polygon language: [item]{statement_lang}[/item])...'
306
320
  )
321
+ uploaded_language = statement_lang if preserve_language else 'english'
322
+ if uploaded_language in uploaded_languages:
323
+ continue
324
+ uploaded_languages.add(uploaded_language)
307
325
  if not preserve_language and statement_lang != 'english':
308
326
  console.console.print(
309
327
  '[warning]By default, Polygon statements are uploaded in English.\n'
@@ -322,7 +340,7 @@ def _upload_statement(problem: api.Problem, preserve_language: bool = False):
322
340
  notes=_get_notes_with_explanations(blocks) or '',
323
341
  )
324
342
  problem.save_statement(
325
- lang=statement_lang if preserve_language else 'english',
343
+ lang=uploaded_language,
326
344
  problem_statement=polygon_statement,
327
345
  )
328
346