rbx.cp 0.11.2__py3-none-any.whl → 0.13.2__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 (40) hide show
  1. rbx/box/builder.py +3 -3
  2. rbx/box/cli.py +9 -0
  3. rbx/box/contest/build_contest_statements.py +5 -6
  4. rbx/box/contest/statements.py +0 -1
  5. rbx/box/generators.py +93 -23
  6. rbx/box/header.py +5 -0
  7. rbx/box/lang.py +25 -12
  8. rbx/box/package.py +56 -4
  9. rbx/box/packaging/contest_main.py +40 -7
  10. rbx/box/packaging/importer.py +37 -0
  11. rbx/box/packaging/main.py +18 -65
  12. rbx/box/packaging/packager.py +95 -2
  13. rbx/box/packaging/polygon/importer.py +232 -0
  14. rbx/box/packaging/polygon/packager.py +36 -5
  15. rbx/box/packaging/polygon/upload.py +34 -14
  16. rbx/box/packaging/polygon/xml_schema.py +15 -6
  17. rbx/box/schema.py +3 -3
  18. rbx/box/solutions.py +8 -12
  19. rbx/box/statements/build_statements.py +0 -1
  20. rbx/box/statements/latex.py +11 -0
  21. rbx/box/stresses.py +1 -1
  22. rbx/box/tooling/converter.py +76 -0
  23. rbx/box/tooling/main.py +54 -1
  24. rbx/grading/caching.py +1 -0
  25. rbx/grading/judge/sandbox.py +1 -0
  26. rbx/grading/steps.py +1 -0
  27. rbx/resources/presets/default/contest/.gitignore +15 -0
  28. rbx/resources/presets/default/contest/contest.rbx.yml +2 -2
  29. rbx/resources/presets/default/problem/.gitignore +15 -0
  30. rbx/resources/presets/default/problem/problem.rbx.yml +1 -4
  31. rbx/resources/presets/default/problem/testplan/random.py +1 -1
  32. rbx/resources/presets/default/problem/testplan/random.txt +2 -4
  33. rbx/resources/presets/default/problem/validator.cpp +2 -1
  34. rbx/resources/presets/default/shared/icpc.sty +1 -1
  35. rbx/utils.py +13 -0
  36. {rbx_cp-0.11.2.dist-info → rbx_cp-0.13.2.dist-info}/METADATA +2 -2
  37. {rbx_cp-0.11.2.dist-info → rbx_cp-0.13.2.dist-info}/RECORD +40 -37
  38. {rbx_cp-0.11.2.dist-info → rbx_cp-0.13.2.dist-info}/LICENSE +0 -0
  39. {rbx_cp-0.11.2.dist-info → rbx_cp-0.13.2.dist-info}/WHEEL +0 -0
  40. {rbx_cp-0.11.2.dist-info → rbx_cp-0.13.2.dist-info}/entry_points.txt +0 -0
rbx/box/packaging/main.py CHANGED
@@ -1,72 +1,16 @@
1
- import pathlib
2
- import tempfile
3
- from typing import Type
1
+ from typing import Optional
4
2
 
5
3
  import syncer
6
4
  import typer
7
5
 
8
- from rbx import annotations, console
9
- from rbx.box import environment, header, package
10
- from rbx.box.formatting import href
6
+ from rbx import annotations
7
+ from rbx.box import environment, package
11
8
  from rbx.box.naming import get_problem_name_with_contest_info
12
- from rbx.box.package import get_build_path
13
- from rbx.box.packaging.packager import BasePackager, BuiltStatement
14
- from rbx.box.statements.build_statements import build_statement
9
+ from rbx.box.packaging.packager import run_packager
15
10
 
16
11
  app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
17
12
 
18
13
 
19
- async def run_packager(
20
- packager_cls: Type[BasePackager],
21
- verification: environment.VerificationParam,
22
- **kwargs,
23
- ) -> pathlib.Path:
24
- from rbx.box import builder
25
-
26
- header.generate_header()
27
-
28
- if not await builder.verify(verification=verification):
29
- console.console.print(
30
- '[error]Build or verification failed, check the report.[/error]'
31
- )
32
- raise typer.Exit(1)
33
-
34
- pkg = package.find_problem_package_or_die()
35
-
36
- if pkg.type not in packager_cls.task_types():
37
- console.console.print(
38
- f'[error]Packager [item]{packager_cls.name()}[/item] does not support task type [item]{pkg.type}[/item].[/error]'
39
- )
40
- raise typer.Exit(1)
41
-
42
- packager = packager_cls(**kwargs)
43
-
44
- statement_types = packager.statement_types()
45
- built_statements = []
46
-
47
- for statement_type in statement_types:
48
- languages = packager.languages()
49
- for language in languages:
50
- statement = packager.get_statement_for_language(language)
51
- statement_path = build_statement(statement, pkg, statement_type)
52
- built_statements.append(
53
- BuiltStatement(statement, statement_path, statement_type)
54
- )
55
-
56
- console.console.print(f'Packaging problem for [item]{packager.name()}[/item]...')
57
-
58
- with tempfile.TemporaryDirectory() as td:
59
- result_path = packager.package(
60
- get_build_path(), pathlib.Path(td), built_statements
61
- )
62
-
63
- console.console.print(
64
- f'[success]Problem packaged for [item]{packager.name()}[/item]![/success]'
65
- )
66
- console.console.print(f'Package was saved at {href(result_path)}')
67
- return result_path
68
-
69
-
70
14
  @app.command('polygon', help='Build a package for Polygon.')
71
15
  @package.within_problem
72
16
  @syncer.sync
@@ -78,22 +22,31 @@ async def polygon(
78
22
  '-u',
79
23
  help='If set, will upload the package to Polygon.',
80
24
  ),
81
- preserve_language: bool = typer.Option(
25
+ language: Optional[str] = typer.Option(
26
+ None,
27
+ '--language',
28
+ '-l',
29
+ help='If set, will use the given language as the main language.',
30
+ ),
31
+ upload_as_english: bool = typer.Option(
82
32
  False,
83
- '--preserve-language',
84
- help='If set, will preserve the original language of the statement.',
33
+ '--upload-as-english',
34
+ help='If set, will force the main statement to be uploaded in English.',
85
35
  ),
86
36
  ):
87
37
  from rbx.box.packaging.polygon.packager import PolygonPackager
88
38
 
89
- await run_packager(PolygonPackager, verification=verification)
39
+ await run_packager(
40
+ PolygonPackager, verification=verification, main_language=language
41
+ )
90
42
 
91
43
  if upload:
92
44
  from rbx.box.packaging.polygon.upload import upload_problem
93
45
 
94
46
  await upload_problem(
95
47
  name=get_problem_name_with_contest_info(),
96
- preserve_language=preserve_language,
48
+ main_language=language,
49
+ upload_as_english=upload_as_english,
97
50
  )
98
51
 
99
52
 
@@ -1,13 +1,20 @@
1
1
  import dataclasses
2
2
  import pathlib
3
+ import shutil
4
+ import tempfile
3
5
  from abc import ABC, abstractmethod
4
- from typing import List, Tuple
6
+ from typing import List, Tuple, Type
5
7
 
6
- from rbx.box import naming, package
8
+ import typer
9
+
10
+ from rbx import console
11
+ from rbx.box import environment, header, naming, package
7
12
  from rbx.box.contest import contest_package
8
13
  from rbx.box.contest.schema import ContestProblem, ContestStatement
14
+ from rbx.box.formatting import href
9
15
  from rbx.box.generators import get_all_built_testcases
10
16
  from rbx.box.schema import Package, TaskType, Testcase, TestcaseGroup
17
+ from rbx.box.statements.build_statements import build_statement
11
18
  from rbx.box.statements.schema import Statement, StatementType
12
19
 
13
20
 
@@ -127,3 +134,89 @@ class BaseContestPackager(ABC):
127
134
  if statement.language == lang:
128
135
  return statement
129
136
  raise
137
+
138
+
139
+ class ContestZipper(BaseContestPackager):
140
+ def __init__(
141
+ self, filename: str, zip_inner: bool = False, prefer_shortname: bool = True
142
+ ):
143
+ super().__init__()
144
+ self.zip_inner = zip_inner
145
+ self.filename = filename
146
+ self.prefer_shortname = prefer_shortname
147
+
148
+ def package(
149
+ self,
150
+ built_packages: List[BuiltProblemPackage],
151
+ build_path: pathlib.Path,
152
+ into_path: pathlib.Path,
153
+ built_statements: List[BuiltContestStatement],
154
+ ) -> pathlib.Path:
155
+ for built_package in built_packages:
156
+ if self.prefer_shortname:
157
+ pkg_path = into_path / 'problems' / built_package.problem.short_name
158
+ else:
159
+ pkg_path = into_path / 'problems' / built_package.package.name
160
+
161
+ if self.zip_inner:
162
+ pkg_path.parent.mkdir(parents=True, exist_ok=True)
163
+ shutil.copy(built_package.path, pkg_path.with_suffix('.zip'))
164
+ else:
165
+ pkg_path.mkdir(parents=True, exist_ok=True)
166
+ shutil.unpack_archive(built_package.path, pkg_path, format='zip')
167
+
168
+ # Zip all.
169
+ shutil.make_archive(str(build_path / self.filename), 'zip', into_path)
170
+
171
+ return build_path / pathlib.Path(self.filename).with_suffix('.zip')
172
+
173
+
174
+ async def run_packager(
175
+ packager_cls: Type[BasePackager],
176
+ verification: environment.VerificationParam,
177
+ **kwargs,
178
+ ) -> pathlib.Path:
179
+ from rbx.box import builder
180
+
181
+ header.generate_header()
182
+
183
+ if not await builder.verify(verification=verification):
184
+ console.console.print(
185
+ '[error]Build or verification failed, check the report.[/error]'
186
+ )
187
+ raise typer.Exit(1)
188
+
189
+ pkg = package.find_problem_package_or_die()
190
+
191
+ if pkg.type not in packager_cls.task_types():
192
+ console.console.print(
193
+ f'[error]Packager [item]{packager_cls.name()}[/item] does not support task type [item]{pkg.type}[/item].[/error]'
194
+ )
195
+ raise typer.Exit(1)
196
+
197
+ packager = packager_cls(**kwargs)
198
+
199
+ statement_types = packager.statement_types()
200
+ built_statements = []
201
+
202
+ for statement_type in statement_types:
203
+ languages = packager.languages()
204
+ for language in languages:
205
+ statement = packager.get_statement_for_language(language)
206
+ statement_path = build_statement(statement, pkg, statement_type)
207
+ built_statements.append(
208
+ BuiltStatement(statement, statement_path, statement_type)
209
+ )
210
+
211
+ console.console.print(f'Packaging problem for [item]{packager.name()}[/item]...')
212
+
213
+ with tempfile.TemporaryDirectory() as td:
214
+ result_path = packager.package(
215
+ package.get_build_path(), pathlib.Path(td), built_statements
216
+ )
217
+
218
+ console.console.print(
219
+ f'[success]Problem packaged for [item]{packager.name()}[/item]![/success]'
220
+ )
221
+ console.console.print(f'Package was saved at {href(result_path)}')
222
+ return result_path
@@ -0,0 +1,232 @@
1
+ import pathlib
2
+ import shutil
3
+ from typing import List, Optional
4
+
5
+ import typer
6
+
7
+ from rbx import console, utils
8
+ from rbx.box import lang
9
+ from rbx.box.packaging.importer import BaseImporter
10
+ from rbx.box.packaging.polygon.xml_schema import File, Problem, Statement, Testset
11
+ from rbx.box.schema import CodeItem, Interactor, Package, TaskType, TestcaseGroup
12
+ from rbx.box.statements.schema import Statement as BoxStatement
13
+ from rbx.box.statements.schema import StatementType
14
+
15
+
16
+ def _get_main_testset(problem: Problem) -> Testset:
17
+ for testset in problem.judging.testsets:
18
+ if testset.name == 'tests':
19
+ return testset
20
+ console.console.print(
21
+ '[error][item]tests[/item] testset not found[/error]',
22
+ )
23
+ raise typer.Exit(1)
24
+
25
+
26
+ def _get_pdf_statements(problem: Problem) -> List[Statement]:
27
+ statements = []
28
+ for statement in problem.statements:
29
+ if statement.type == 'application/pdf':
30
+ statements.append(statement)
31
+ return statements
32
+
33
+
34
+ def _get_statement_path(statement: Statement) -> pathlib.Path:
35
+ return pathlib.Path('statements') / f'{statement.language}.pdf'
36
+
37
+
38
+ def _populate_tests(
39
+ testset: Testset, pkg: Package, pkg_path: pathlib.Path, into_path: pathlib.Path
40
+ ):
41
+ if not testset.answerPattern:
42
+ console.console.print(
43
+ '[error][item]answer pattern[/item] not found for testset[/error]',
44
+ )
45
+ raise typer.Exit(1)
46
+
47
+ for d, test in enumerate(testset.tests):
48
+ folder_name = 'tests/samples' if test.sample else 'tests/tests'
49
+ i = d + 1
50
+
51
+ input_path = pathlib.Path(testset.inputPattern % i)
52
+ dest_input_path = into_path / folder_name / f'{i:03d}.in'
53
+ dest_input_path.parent.mkdir(parents=True, exist_ok=True)
54
+ shutil.copy(pkg_path / input_path, dest_input_path)
55
+
56
+ answer_path = pathlib.Path(testset.answerPattern % i)
57
+ dest_answer_path = into_path / folder_name / f'{i:03d}.ans'
58
+ dest_answer_path.parent.mkdir(parents=True, exist_ok=True)
59
+ shutil.copy(pkg_path / answer_path, dest_answer_path)
60
+
61
+ pkg.testcases = [
62
+ TestcaseGroup(
63
+ name='samples',
64
+ testcaseGlob='tests/samples/*.in',
65
+ ),
66
+ TestcaseGroup(
67
+ name='tests',
68
+ testcaseGlob='tests/tests/*.in',
69
+ ),
70
+ ]
71
+
72
+
73
+ def _populate_statements(
74
+ problem: Problem,
75
+ pkg: Package,
76
+ pkg_path: pathlib.Path,
77
+ into_path: pathlib.Path,
78
+ main_language: Optional[str] = None,
79
+ ):
80
+ name_per_language = {name.language: name for name in problem.names}
81
+ pdf_statements = _get_pdf_statements(problem)
82
+ pkg_statements = []
83
+ found_main = False
84
+
85
+ for statement in pdf_statements:
86
+ if statement.language not in name_per_language:
87
+ continue
88
+ name = name_per_language[statement.language]
89
+ statement_path = into_path / _get_statement_path(statement)
90
+ statement_path.parent.mkdir(parents=True, exist_ok=True)
91
+ shutil.copy(pkg_path / statement.path, statement_path)
92
+
93
+ iso639_code = lang.lang_to_code(statement.language)
94
+
95
+ pkg_statement = BoxStatement(
96
+ name=f'statement-{name.language}',
97
+ title=name.value,
98
+ language=iso639_code,
99
+ path=_get_statement_path(statement),
100
+ type=StatementType.PDF,
101
+ )
102
+
103
+ if (
104
+ main_language is not None
105
+ and main_language == iso639_code
106
+ and not found_main
107
+ ):
108
+ # If main statement, add it to the front of the list
109
+ pkg_statements = [pkg_statement] + pkg_statements
110
+ found_main = True
111
+ continue
112
+
113
+ if name.main and not found_main:
114
+ # If main statement, add it to the front of the list
115
+ pkg_statements = [pkg_statement] + pkg_statements
116
+ found_main = True
117
+ continue
118
+
119
+ pkg_statements.append(pkg_statement)
120
+
121
+ pkg.statements = pkg_statements
122
+
123
+ if main_language is not None and not found_main:
124
+ console.console.print(
125
+ f'[error]Main statement of language [item]{main_language}[/item] not found.[/error]',
126
+ )
127
+ raise typer.Exit(1)
128
+
129
+
130
+ def _is_cpp_source(source: File) -> bool:
131
+ if source.type is None:
132
+ return False
133
+ return 'cpp' in source.type
134
+
135
+
136
+ def _copy_checker(
137
+ problem: Problem, pkg: Package, pkg_path: pathlib.Path, into_path: pathlib.Path
138
+ ):
139
+ if problem.checker is None:
140
+ return
141
+ if problem.checker.type != 'testlib' or not _is_cpp_source(problem.checker.source):
142
+ console.console.print(
143
+ f'[error][item]checker type[/item] not supported: [item]{problem.checker.type}[/item][/error]',
144
+ )
145
+ raise typer.Exit(1)
146
+ shutil.copy(pkg_path / problem.checker.source.path, into_path / 'checker.cpp')
147
+
148
+ pkg.checker = CodeItem(
149
+ path=pathlib.Path('checker.cpp'),
150
+ )
151
+
152
+
153
+ def _copy_interactor(
154
+ problem: Problem, pkg: Package, pkg_path: pathlib.Path, into_path: pathlib.Path
155
+ ):
156
+ if problem.interactor is None:
157
+ return
158
+ shutil.copy(pkg_path / problem.interactor.source.path, into_path / 'interactor.cpp')
159
+
160
+ if not _is_cpp_source(problem.interactor.source):
161
+ console.console.print(
162
+ f'[error]Only C++ interactor is supported, got [item]{problem.interactor.source.type}[/item][/error]',
163
+ )
164
+ raise typer.Exit(1)
165
+
166
+ pkg.type = TaskType.COMMUNICATION
167
+ pkg.interactor = Interactor(
168
+ path=pathlib.Path('interactor.cpp'),
169
+ legacy=True,
170
+ )
171
+
172
+
173
+ def _copy_headers(
174
+ problem: Problem, pkg: Package, pkg_path: pathlib.Path, into_path: pathlib.Path
175
+ ):
176
+ headers = []
177
+ for file in problem.files:
178
+ if file.type is None or not file.type.startswith('h.'):
179
+ continue
180
+ header_path = pkg_path / file.path
181
+ dest_path = into_path / header_path.name
182
+ if header_path.name == 'rbx.h':
183
+ dest_path = into_path / 'rbx.override.h'
184
+ shutil.copy(header_path, dest_path)
185
+ headers.append(dest_path.name)
186
+
187
+ if pkg.checker is not None:
188
+ pkg.checker.compilationFiles = headers
189
+
190
+ if pkg.interactor is not None:
191
+ pkg.interactor.compilationFiles = headers
192
+
193
+
194
+ class PolygonImporter(BaseImporter):
195
+ def __init__(self, main_language: Optional[str]):
196
+ self.main_language = main_language
197
+
198
+ @classmethod
199
+ def name(cls) -> str:
200
+ return 'polygon'
201
+
202
+ async def import_package(self, pkg_path: pathlib.Path, into_path: pathlib.Path):
203
+ problem_xml = pkg_path / 'problem.xml'
204
+ if not problem_xml.exists():
205
+ console.console.print(
206
+ '[error][item]problem.xml[/item] not found[/error]',
207
+ )
208
+ raise typer.Exit(1)
209
+
210
+ problem = Problem.from_xml(problem_xml.read_bytes())
211
+ testset = _get_main_testset(problem)
212
+
213
+ if testset.timelimit is None:
214
+ testset.timelimit = 1000
215
+
216
+ if testset.memorylimit is None:
217
+ testset.memorylimit = 256 * 1024 * 1024
218
+
219
+ pkg = Package(
220
+ name=problem.short_name,
221
+ timeLimit=testset.timelimit,
222
+ memoryLimit=testset.memorylimit // (1024 * 1024),
223
+ outputLimit=64 * 1024,
224
+ )
225
+
226
+ _populate_tests(testset, pkg, pkg_path, into_path)
227
+ _populate_statements(problem, pkg, pkg_path, into_path, self.main_language)
228
+ _copy_checker(problem, pkg, pkg_path, into_path)
229
+ _copy_interactor(problem, pkg, pkg_path, into_path)
230
+ _copy_headers(problem, pkg, pkg_path, into_path)
231
+
232
+ (into_path / 'problem.rbx.yml').write_text(utils.model_to_yaml(pkg))
@@ -6,7 +6,7 @@ import typer
6
6
 
7
7
  from rbx import console, utils
8
8
  from rbx.box import header, package
9
- from rbx.box.lang import code_to_langs, is_valid_lang_code
9
+ from rbx.box.lang import code_to_lang, code_to_langs, is_valid_lang_code
10
10
  from rbx.box.packaging.packager import (
11
11
  BaseContestPackager,
12
12
  BasePackager,
@@ -27,7 +27,31 @@ DAT_TEMPLATE = """
27
27
  """
28
28
 
29
29
 
30
+ def _select_main_language(
31
+ names: List[polygon_schema.Name], main_language: Optional[str]
32
+ ):
33
+ if names:
34
+ if main_language is not None:
35
+ lang_name = code_to_lang(main_language)
36
+ found = False
37
+ for name in names:
38
+ if name.language == lang_name:
39
+ name.main = True
40
+ found = True
41
+ break
42
+ if not found:
43
+ console.console.print(
44
+ f'[error]Main language [item]{main_language}[/item] not found.[/error]'
45
+ )
46
+ raise typer.Exit(1)
47
+ else:
48
+ names[0].main = True
49
+
50
+
30
51
  class PolygonPackager(BasePackager):
52
+ def __init__(self, main_language: Optional[str] = None):
53
+ self.main_language = main_language
54
+
31
55
  @classmethod
32
56
  def task_types(cls) -> List[TaskType]:
33
57
  return [TaskType.BATCH, TaskType.COMMUNICATION]
@@ -49,7 +73,7 @@ class PolygonPackager(BasePackager):
49
73
  raise typer.Exit(1)
50
74
 
51
75
  def _get_names(self) -> List[polygon_schema.Name]:
52
- return [
76
+ names = [
53
77
  polygon_schema.Name(
54
78
  language=code_to_langs([lang])[0],
55
79
  value=self.get_statement_for_language(lang).title,
@@ -57,6 +81,10 @@ class PolygonPackager(BasePackager):
57
81
  for lang in self.languages()
58
82
  ]
59
83
 
84
+ _select_main_language(names, self.main_language)
85
+
86
+ return names
87
+
60
88
  def _get_checker(self) -> polygon_schema.Checker:
61
89
  # TODO: support other checker languages
62
90
  return polygon_schema.Checker(
@@ -155,6 +183,7 @@ class PolygonPackager(BasePackager):
155
183
  pkg = package.find_problem_package_or_die()
156
184
 
157
185
  problem = polygon_schema.Problem(
186
+ short_name=pkg.name,
158
187
  names=self._get_names(),
159
188
  checker=self._get_checker(),
160
189
  judging=self._get_judging(),
@@ -209,6 +238,9 @@ class PolygonPackager(BasePackager):
209
238
 
210
239
 
211
240
  class PolygonContestPackager(BaseContestPackager):
241
+ def __init__(self, main_language: Optional[str] = None):
242
+ self.main_language = main_language
243
+
212
244
  @classmethod
213
245
  def name(cls) -> str:
214
246
  return 'polygon'
@@ -222,8 +254,7 @@ class PolygonContestPackager(BaseContestPackager):
222
254
  for lang in self.languages()
223
255
  if is_valid_lang_code(lang)
224
256
  ]
225
- if names:
226
- names[0].main = True
257
+ _select_main_language(names, self.main_language)
227
258
  return names
228
259
 
229
260
  def _process_statement(
@@ -319,4 +350,4 @@ class PolygonContestPackager(BaseContestPackager):
319
350
  # Zip all.
320
351
  shutil.make_archive(str(build_path / 'contest'), 'zip', into_path)
321
352
 
322
- return pathlib.Path('contest.zip')
353
+ return build_path / 'contest.zip'
@@ -214,8 +214,7 @@ def _upload_testcases(problem: api.Problem):
214
214
 
215
215
  def _upload_solutions(problem: api.Problem):
216
216
  console.console.print('Uploading main solution...')
217
- pkg = package.find_problem_package_or_die()
218
- main_solution = pkg.solutions[0]
217
+ main_solution = package.get_main_solution()
219
218
  if main_solution is None or main_solution.outcome != ExpectedOutcome.ACCEPTED:
220
219
  return
221
220
  problem.save_solution(
@@ -225,7 +224,7 @@ def _upload_solutions(problem: api.Problem):
225
224
  tag=api.SolutionTag.MA,
226
225
  )
227
226
 
228
- for i, solution in enumerate(pkg.solutions):
227
+ for i, solution in enumerate(package.get_solutions()):
229
228
  console.console.print(
230
229
  f'Uploading solution [item]{solution.path.name}[/item] (tag: [item]{_get_solution_tag(solution, is_first=i == 0)}[/item])...'
231
230
  )
@@ -296,19 +295,32 @@ def _upload_statement_resources(problem: api.Problem, statement: Statement):
296
295
  )
297
296
 
298
297
 
299
- def _upload_statement(problem: api.Problem, preserve_language: bool = False):
298
+ def _upload_statement(
299
+ problem: api.Problem, main_language: Optional[str], upload_as_english: bool = False
300
+ ):
300
301
  pkg = package.find_problem_package_or_die()
301
302
 
303
+ lang_list = []
302
304
  languages = set()
303
305
  for statement in pkg.expanded_statements:
304
306
  if not is_valid_lang_code(statement.language):
305
307
  continue
306
308
  languages.add(statement.language)
307
-
309
+ lang_list.append(statement.language)
308
310
  uploaded_languages = set()
309
311
 
312
+ if main_language is None:
313
+ main_language = lang_list[0]
314
+
315
+ # Put the main language first.
316
+ lang_list = list(languages)
317
+ for i in range(len(lang_list)):
318
+ if lang_list[i] == main_language:
319
+ lang_list[i], lang_list[0] = lang_list[0], lang_list[i]
320
+ break
321
+
310
322
  # Prioritize English statements.
311
- for language in ['en'] + list(languages):
323
+ for language in lang_list:
312
324
  statement = _get_statement_for_language(language)
313
325
  if statement is None:
314
326
  continue
@@ -318,15 +330,19 @@ def _upload_statement(problem: api.Problem, preserve_language: bool = False):
318
330
  console.console.print(
319
331
  f'Uploading statement for language [item]{language}[/item] (polygon language: [item]{statement_lang}[/item])...'
320
332
  )
321
- uploaded_language = statement_lang if preserve_language else 'english'
333
+ uploaded_language = statement_lang
334
+ if main_language == language:
335
+ if not upload_as_english:
336
+ console.console.print(
337
+ '[warning]By default, Polygon statements are uploaded respecting their original language.\n'
338
+ 'Codeforces does not work well with statements in other languages. If you want a better experience, '
339
+ 'use the [item]--upload-as-english[/item] option to force the main statement to be uploaded in English.[/warning]'
340
+ )
341
+ else:
342
+ uploaded_language = 'english'
322
343
  if uploaded_language in uploaded_languages:
323
344
  continue
324
345
  uploaded_languages.add(uploaded_language)
325
- if not preserve_language and statement_lang != 'english':
326
- console.console.print(
327
- '[warning]By default, Polygon statements are uploaded in English.\n'
328
- 'If you want to preserve the original language of your statement, use [item]--preserve-language[/item].'
329
- )
330
346
  blocks = _get_statement_blocks(statement)
331
347
  polygon_statement = api.Statement(
332
348
  encoding='utf-8',
@@ -351,7 +367,9 @@ def _normalize_problem_name(name: str) -> str:
351
367
  return name.replace(' ', '-').replace('_', '-').lower()
352
368
 
353
369
 
354
- async def upload_problem(name: str, preserve_language: bool = False):
370
+ async def upload_problem(
371
+ name: str, main_language: Optional[str], upload_as_english: bool = False
372
+ ):
355
373
  pkg = package.find_problem_package_or_die()
356
374
  name = _normalize_problem_name(name)
357
375
  problem = _find_or_create_problem(name)
@@ -370,7 +388,9 @@ async def upload_problem(name: str, preserve_language: bool = False):
370
388
 
371
389
  _upload_solutions(problem)
372
390
  _upload_testcases(problem)
373
- _upload_statement(problem, preserve_language=preserve_language)
391
+ _upload_statement(
392
+ problem, main_language=main_language, upload_as_english=upload_as_english
393
+ )
374
394
 
375
395
  # Commit.
376
396
  console.console.print('Committing changes...')