AtCoderStudyBooster 0.4.0__py3-none-any.whl → 0.4.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.
- atcdr/ai.py +434 -0
- atcdr/cli.py +2 -2
- atcdr/download.py +32 -27
- atcdr/login.py +10 -11
- atcdr/logout.py +4 -3
- atcdr/markdown.py +9 -5
- atcdr/open.py +5 -4
- atcdr/submit.py +21 -24
- atcdr/test.py +21 -16
- atcdr/util/fileops.py +5 -4
- atcdr/util/i18n.py +317 -0
- atcdr/util/openai.py +45 -0
- atcdr/util/parse.py +15 -10
- atcdr/util/problem.py +3 -2
- atcdr/util/session.py +15 -14
- atcoderstudybooster-0.4.2.dist-info/METADATA +249 -0
- atcoderstudybooster-0.4.2.dist-info/RECORD +22 -0
- atcdr/generate.py +0 -202
- atcdr/util/gpt.py +0 -118
- atcoderstudybooster-0.4.0.dist-info/METADATA +0 -261
- atcoderstudybooster-0.4.0.dist-info/RECORD +0 -21
- {atcoderstudybooster-0.4.0.dist-info → atcoderstudybooster-0.4.2.dist-info}/WHEEL +0 -0
- {atcoderstudybooster-0.4.0.dist-info → atcoderstudybooster-0.4.2.dist-info}/entry_points.txt +0 -0
atcdr/login.py
CHANGED
@@ -6,6 +6,7 @@ import webview
|
|
6
6
|
from requests import Session
|
7
7
|
from rich.console import Console
|
8
8
|
|
9
|
+
from atcdr.util.i18n import _
|
9
10
|
from atcdr.util.session import load_session, save_session, validate_session
|
10
11
|
|
11
12
|
ATCODER_LOGIN_URL = 'https://atcoder.jp/login'
|
@@ -14,17 +15,17 @@ ATCODER_HOME_URL = 'https://atcoder.jp/home'
|
|
14
15
|
console = Console()
|
15
16
|
|
16
17
|
|
17
|
-
@click.command(short_help='
|
18
|
+
@click.command(short_help=_('cmd_login'), help=_('cmd_login'))
|
18
19
|
def login() -> None:
|
19
20
|
"""AtCoderへログインします."""
|
20
21
|
session = load_session()
|
21
22
|
if validate_session(session):
|
22
|
-
console.print('[green][+][/]
|
23
|
+
console.print('[green][+][/] ' + _('already_logged_in'))
|
23
24
|
return
|
24
25
|
|
25
26
|
# Prompt in CLI
|
26
|
-
username = console.input('[cyan]
|
27
|
-
password = console.input('[cyan]
|
27
|
+
username = console.input('[cyan]' + _('username') + '[/]').strip()
|
28
|
+
password = console.input('[cyan]' + _('password') + '[/]').strip()
|
28
29
|
|
29
30
|
window = webview.create_window('AtCoder Login', ATCODER_LOGIN_URL, hidden=False)
|
30
31
|
|
@@ -36,16 +37,14 @@ def login() -> None:
|
|
36
37
|
window.evaluate_js(js_fill)
|
37
38
|
|
38
39
|
def poll_and_submit():
|
39
|
-
with console.status(
|
40
|
-
'キャプチャー認証を解決してください', spinner='circleHalves'
|
41
|
-
):
|
40
|
+
with console.status(_('solve_captcha'), spinner='circleHalves'):
|
42
41
|
while True:
|
43
42
|
try:
|
44
43
|
token = window.evaluate_js(
|
45
44
|
'document.querySelector(\'input[name=\\"cf-turnstile-response\\"]\').value'
|
46
45
|
)
|
47
46
|
if token:
|
48
|
-
console.print('[green][+][/]
|
47
|
+
console.print('[green][+][/] ' + _('logging_in'))
|
49
48
|
window.evaluate_js(
|
50
49
|
"document.getElementById('submit').click();"
|
51
50
|
)
|
@@ -55,7 +54,7 @@ def login() -> None:
|
|
55
54
|
|
56
55
|
time.sleep(0.5)
|
57
56
|
|
58
|
-
with console.status('
|
57
|
+
with console.status(_('waiting_login_result'), spinner='circleHalves'):
|
59
58
|
while True:
|
60
59
|
try:
|
61
60
|
current_url = window.get_current_url()
|
@@ -63,7 +62,7 @@ def login() -> None:
|
|
63
62
|
current_url = None
|
64
63
|
|
65
64
|
if current_url and current_url.startswith(ATCODER_HOME_URL):
|
66
|
-
console.print('[green][+][/]
|
65
|
+
console.print('[green][+][/] ' + _('login_success'))
|
67
66
|
|
68
67
|
session = Session()
|
69
68
|
session = move_cookies_from_webview_to_session(window, session)
|
@@ -82,7 +81,7 @@ def login() -> None:
|
|
82
81
|
err = ''
|
83
82
|
|
84
83
|
if err:
|
85
|
-
console.print(
|
84
|
+
console.print('[red][-][/] ' + _('error', err))
|
86
85
|
session = Session()
|
87
86
|
session = move_cookies_from_webview_to_session(window, session)
|
88
87
|
save_session(session)
|
atcdr/logout.py
CHANGED
@@ -2,21 +2,22 @@ import rich_click as click
|
|
2
2
|
import webview
|
3
3
|
from rich import print
|
4
4
|
|
5
|
+
from atcdr.util.i18n import _
|
5
6
|
from atcdr.util.session import delete_session, load_session, validate_session
|
6
7
|
|
7
8
|
ATCODER_LOGIN_URL = 'https://atcoder.jp/login'
|
8
9
|
|
9
10
|
|
10
|
-
@click.command(short_help='
|
11
|
+
@click.command(short_help=_('cmd_logout'), help=_('cmd_logout'))
|
11
12
|
def logout() -> None:
|
12
13
|
"""AtCoderからログアウトします."""
|
13
14
|
session = load_session()
|
14
15
|
if not validate_session(session):
|
15
|
-
print('[red][-][/]
|
16
|
+
print('[red][-][/] ' + _('not_logged_in'))
|
16
17
|
return
|
17
18
|
|
18
19
|
delete_session()
|
19
|
-
print('[green][+][/]
|
20
|
+
print('[green][+][/] ' + _('logout_success'))
|
20
21
|
|
21
22
|
window = webview.create_window('AtCoder Logout', ATCODER_LOGIN_URL, hidden=True)
|
22
23
|
|
atcdr/markdown.py
CHANGED
@@ -6,6 +6,7 @@ from rich.markdown import Markdown
|
|
6
6
|
|
7
7
|
from atcdr.util.fileops import add_file_selector
|
8
8
|
from atcdr.util.filetype import FILE_EXTENSIONS, Lang
|
9
|
+
from atcdr.util.i18n import _, i18n
|
9
10
|
from atcdr.util.parse import ProblemHTML
|
10
11
|
|
11
12
|
|
@@ -19,7 +20,7 @@ def save_markdown(html_path: str, lang: str) -> None:
|
|
19
20
|
|
20
21
|
with open(md_path, 'w', encoding='utf-8') as f:
|
21
22
|
f.write(md)
|
22
|
-
console.print('[green][+][/green]
|
23
|
+
console.print('[green][+][/green] ' + _('markdown_created'))
|
23
24
|
|
24
25
|
|
25
26
|
def print_markdown(html_path: str, lang: str) -> None:
|
@@ -30,12 +31,15 @@ def print_markdown(html_path: str, lang: str) -> None:
|
|
30
31
|
console.print(Markdown(md))
|
31
32
|
|
32
33
|
|
33
|
-
@click.command(short_help='
|
34
|
+
@click.command(short_help=_('cmd_markdown'), help=_('cmd_markdown'))
|
34
35
|
@add_file_selector('files', filetypes=[Lang.HTML])
|
35
|
-
@click.option('--lang', default=
|
36
|
-
@click.option('--save', is_flag=True, help='
|
36
|
+
@click.option('--lang', default=None, help=_('opt_lang'))
|
37
|
+
@click.option('--save', is_flag=True, help=_('opt_save'))
|
37
38
|
def markdown(files, lang, save):
|
38
|
-
"""Markdown
|
39
|
+
"""問題をMarkdown形式で表示します"""
|
40
|
+
# langが指定されていない場合は現在のロケールを使用
|
41
|
+
if lang is None:
|
42
|
+
lang = i18n.language
|
39
43
|
for path in files:
|
40
44
|
if save:
|
41
45
|
save_markdown(path, lang)
|
atcdr/open.py
CHANGED
@@ -2,6 +2,7 @@ import webbrowser # noqa: I001
|
|
2
2
|
from rich.panel import Panel
|
3
3
|
from rich.console import Console
|
4
4
|
|
5
|
+
from atcdr.util.i18n import _
|
5
6
|
from atcdr.util.filetype import Lang
|
6
7
|
from atcdr.util.fileops import add_file_selector
|
7
8
|
import rich_click as click
|
@@ -16,7 +17,7 @@ def open_html(file: str) -> None:
|
|
16
17
|
except FileNotFoundError:
|
17
18
|
console.print(
|
18
19
|
Panel(
|
19
|
-
f"{file}' [red]
|
20
|
+
f"{file}' [red]" + _('not_found') + '[/]',
|
20
21
|
border_style='red',
|
21
22
|
)
|
22
23
|
)
|
@@ -27,20 +28,20 @@ def open_html(file: str) -> None:
|
|
27
28
|
webbrowser.open_new_tab(url)
|
28
29
|
console.print(
|
29
30
|
Panel(
|
30
|
-
|
31
|
+
'[green]' + _('url_opened') + f'[/] {url}',
|
31
32
|
border_style='green',
|
32
33
|
)
|
33
34
|
)
|
34
35
|
else:
|
35
36
|
console.print(
|
36
37
|
Panel(
|
37
|
-
f'{file} [yellow]
|
38
|
+
f'{file} [yellow]' + _('url_not_found_in') + '[/]',
|
38
39
|
border_style='yellow',
|
39
40
|
)
|
40
41
|
)
|
41
42
|
|
42
43
|
|
43
|
-
@click.command(short_help='
|
44
|
+
@click.command(short_help=_('cmd_open'), help=_('cmd_open'))
|
44
45
|
@add_file_selector('files', filetypes=[Lang.HTML])
|
45
46
|
def open_files(files):
|
46
47
|
"""指定したHTMLファイルをブラウザで開きます。"""
|
atcdr/submit.py
CHANGED
@@ -29,6 +29,7 @@ from atcdr.util.filetype import (
|
|
29
29
|
lang2str,
|
30
30
|
str2lang,
|
31
31
|
)
|
32
|
+
from atcdr.util.i18n import _
|
32
33
|
from atcdr.util.parse import ProblemHTML, get_submission_id
|
33
34
|
from atcdr.util.session import load_session, validate_session
|
34
35
|
|
@@ -61,14 +62,14 @@ def choose_langid_interactively(lang_dict: dict, lang: Lang) -> int:
|
|
61
62
|
options = [*filter(lambda option: option.lang == lang, options)]
|
62
63
|
|
63
64
|
langid = q.select(
|
64
|
-
message=
|
65
|
+
message=_('select_implementation', lang2str(lang)),
|
65
66
|
qmark='',
|
66
67
|
pointer='❯❯❯',
|
67
68
|
choices=[
|
68
69
|
q.Choice(title=f'{option.display_name}', value=option.id)
|
69
70
|
for option in options
|
70
71
|
],
|
71
|
-
instruction='\n
|
72
|
+
instruction='\n ' + _('navigate_with_arrows'),
|
72
73
|
style=q.Style(
|
73
74
|
[
|
74
75
|
('question', 'fg:#2196F3 bold'),
|
@@ -136,24 +137,24 @@ def post_source(source_path: str, url: str, session: requests.Session) -> Option
|
|
136
137
|
|
137
138
|
window.events.loaded += on_loaded
|
138
139
|
|
139
|
-
with Status('
|
140
|
+
with Status(_('solve_captcha'), spinner='circleHalves'):
|
140
141
|
webview.start(private_mode=False)
|
141
142
|
|
142
143
|
if 'submit' in api.url:
|
143
|
-
print('[red][-][/red]
|
144
|
+
print('[red][-][/red] ' + _('submission_failed'))
|
144
145
|
return None
|
145
146
|
elif 'submissions' in api.url:
|
146
147
|
submission_id = get_submission_id(api.html)
|
147
148
|
if not submission_id:
|
148
|
-
print('[red][-][/red]
|
149
|
+
print('[red][-][/red] ' + _('submission_id_not_found'))
|
149
150
|
return None
|
150
151
|
|
151
152
|
url = api.url.replace('/me', f'/{submission_id}')
|
152
|
-
print('[green][+][/green]
|
153
|
-
print(
|
153
|
+
print('[green][+][/green] ' + _('submission_success'))
|
154
|
+
print(_('submission_details', submission_id, url))
|
154
155
|
return url + '/status/json'
|
155
156
|
else:
|
156
|
-
print('[red][-][/red]
|
157
|
+
print('[red][-][/red] ' + _('submission_failed'))
|
157
158
|
return None
|
158
159
|
|
159
160
|
|
@@ -211,19 +212,19 @@ def print_status_submission(
|
|
211
212
|
BarColumn(),
|
212
213
|
)
|
213
214
|
|
214
|
-
with Status('
|
215
|
-
for
|
215
|
+
with Status(_('waiting_judge'), spinner='dots'):
|
216
|
+
for i in range(15):
|
216
217
|
time.sleep(1)
|
217
218
|
data = session.get(api_url).json()
|
218
219
|
status = parse_submission_status_json(data)
|
219
220
|
if status.total or status.current:
|
220
221
|
break
|
221
222
|
else:
|
222
|
-
print('[red][-][/]
|
223
|
+
print('[red][-][/] ' + _('judge_timeout'))
|
223
224
|
return
|
224
225
|
|
225
226
|
total = status.total or 0
|
226
|
-
task_id = progress.add_task(description='
|
227
|
+
task_id = progress.add_task(description=_('judging'), total=total)
|
227
228
|
|
228
229
|
test_info = TestInformation(
|
229
230
|
lang=detect_language(path),
|
@@ -248,24 +249,22 @@ def print_status_submission(
|
|
248
249
|
test_info.summary = status.status
|
249
250
|
test_info.results = [ResultStatus.AC] * total
|
250
251
|
|
251
|
-
progress.update(task_id, description='
|
252
|
+
progress.update(task_id, description=_('judge_completed'), completed=total)
|
252
253
|
live.update(create_renderable_test_info(test_info, progress))
|
253
254
|
|
254
255
|
|
255
256
|
def submit_source(path: str, no_test: bool, no_feedback: bool) -> None:
|
256
257
|
session = load_session()
|
257
258
|
if not validate_session(session):
|
258
|
-
print('[red][-][/]
|
259
|
+
print('[red][-][/] ' + _('not_logged_in'))
|
259
260
|
login()
|
260
261
|
if not validate_session(session):
|
261
|
-
print('[red][-][/]
|
262
|
+
print('[red][-][/] ' + _('login_failed'))
|
262
263
|
return
|
263
264
|
|
264
265
|
html_files = [file for file in os.listdir('.') if file.endswith('.html')]
|
265
266
|
if not html_files:
|
266
|
-
print(
|
267
|
-
'問題のファイルが見つかりません \n問題のファイルが存在するディレクトリーに移動してから実行してください'
|
268
|
-
)
|
267
|
+
print(_('problem_file_not_found'))
|
269
268
|
return
|
270
269
|
|
271
270
|
with open(html_files[0], 'r') as file:
|
@@ -279,7 +278,7 @@ def submit_source(path: str, no_test: bool, no_feedback: bool) -> None:
|
|
279
278
|
print(create_renderable_test_info(test.info))
|
280
279
|
|
281
280
|
if test.info.summary != ResultStatus.AC and not no_test:
|
282
|
-
print('[red][-][/]
|
281
|
+
print('[red][-][/] ' + _('sample_not_ac'))
|
283
282
|
return
|
284
283
|
|
285
284
|
api_status_link = post_source(path, url, session)
|
@@ -290,12 +289,10 @@ def submit_source(path: str, no_test: bool, no_feedback: bool) -> None:
|
|
290
289
|
print_status_submission(api_status_link, path, session)
|
291
290
|
|
292
291
|
|
293
|
-
@click.command(short_help='
|
292
|
+
@click.command(short_help=_('cmd_submit'), help=_('cmd_submit'))
|
294
293
|
@add_file_selector('files', filetypes=COMPILED_LANGUAGES + INTERPRETED_LANGUAGES)
|
295
|
-
@click.option('--no-test', is_flag=True, default=False, help='
|
296
|
-
@click.option(
|
297
|
-
'--no-feedback', is_flag=True, default=False, help='フィードバックをスキップ'
|
298
|
-
)
|
294
|
+
@click.option('--no-test', is_flag=True, default=False, help=_('opt_no_test'))
|
295
|
+
@click.option('--no-feedback', is_flag=True, default=False, help=_('opt_no_feedback'))
|
299
296
|
def submit(files, no_test, no_feedback):
|
300
297
|
"""指定したファイルをAtCoderへ提出します。"""
|
301
298
|
for path in files:
|
atcdr/test.py
CHANGED
@@ -26,6 +26,7 @@ from atcdr.util.filetype import (
|
|
26
26
|
detect_language,
|
27
27
|
lang2str,
|
28
28
|
)
|
29
|
+
from atcdr.util.i18n import _
|
29
30
|
from atcdr.util.parse import ProblemHTML
|
30
31
|
|
31
32
|
|
@@ -159,7 +160,7 @@ class TestRunner:
|
|
159
160
|
]
|
160
161
|
self.exe = None
|
161
162
|
else:
|
162
|
-
raise ValueError(
|
163
|
+
raise ValueError(_('runner_not_found', lang))
|
163
164
|
|
164
165
|
return self
|
165
166
|
|
@@ -300,9 +301,9 @@ def create_renderable_test_info(
|
|
300
301
|
status_text = STATUS_TEXT_MAP[test_info.summary]
|
301
302
|
|
302
303
|
header_text = Text.assemble(
|
303
|
-
Text.from_markup(f'[cyan]{test_info.sourcename}[/]
|
304
|
+
Text.from_markup(f'[cyan]{test_info.sourcename}[/]' + _('test_of', '') + '\n'),
|
304
305
|
Text.from_markup(
|
305
|
-
|
306
|
+
'[italic #0f0f0f]' + _('compile_time', test_info.compile_time) + '\n'
|
306
307
|
)
|
307
308
|
if test_info.compile_time
|
308
309
|
else Text(''),
|
@@ -319,7 +320,7 @@ def create_renderable_test_info(
|
|
319
320
|
|
320
321
|
if test_info.compiler_message:
|
321
322
|
rule = Rule(
|
322
|
-
title='
|
323
|
+
title=_('compiler_message'),
|
323
324
|
style=COLOR_MAP[ResultStatus.CE],
|
324
325
|
)
|
325
326
|
components.append(rule)
|
@@ -342,27 +343,29 @@ def create_renderable_test_result(
|
|
342
343
|
|
343
344
|
# 以下の部分は if-else ブロックの外に移動
|
344
345
|
status_header = Text.assemble(
|
345
|
-
'
|
346
|
+
_('status'),
|
346
347
|
STATUS_TEXT_MAP[test_result.result.passed], # status_text をここに追加
|
347
348
|
)
|
348
349
|
|
349
350
|
execution_time_text = None
|
350
351
|
if test_result.result.executed_time is not None:
|
351
352
|
execution_time_text = Text.from_markup(
|
352
|
-
|
353
|
+
_('execution_time', test_result.result.executed_time)
|
353
354
|
)
|
354
355
|
|
355
356
|
table = Table(show_header=True, header_style='bold')
|
356
|
-
table.add_column('
|
357
|
+
table.add_column(_('input'), style='cyan', min_width=10)
|
357
358
|
|
358
359
|
if test_result.result.passed != ResultStatus.AC:
|
359
360
|
table.add_column(
|
360
|
-
'
|
361
|
+
_('output'),
|
361
362
|
style=COLOR_MAP[test_result.result.passed],
|
362
363
|
min_width=10,
|
363
364
|
overflow='fold',
|
364
365
|
)
|
365
|
-
table.add_column(
|
366
|
+
table.add_column(
|
367
|
+
_('expected_output'), style=COLOR_MAP[ResultStatus.AC], min_width=10
|
368
|
+
)
|
366
369
|
table.add_row(
|
367
370
|
escape(test_result.testcase.input),
|
368
371
|
escape(test_result.result.output),
|
@@ -370,7 +373,7 @@ def create_renderable_test_result(
|
|
370
373
|
)
|
371
374
|
else:
|
372
375
|
table.add_column(
|
373
|
-
'
|
376
|
+
_('output'), style=COLOR_MAP[test_result.result.passed], min_width=10
|
374
377
|
)
|
375
378
|
table.add_row(
|
376
379
|
escape(test_result.testcase.input), escape(test_result.result.output)
|
@@ -393,7 +396,9 @@ def render_results(test: TestRunner) -> None:
|
|
393
396
|
SpinnerColumn(style='white', spinner_name='simpleDots'),
|
394
397
|
BarColumn(),
|
395
398
|
)
|
396
|
-
task_id = progress.add_task(
|
399
|
+
task_id = progress.add_task(
|
400
|
+
description=_('test_in_progress'), total=test.info.case_number
|
401
|
+
)
|
397
402
|
|
398
403
|
current_display = [create_renderable_test_info(test.info, progress)]
|
399
404
|
|
@@ -404,7 +409,9 @@ def render_results(test: TestRunner) -> None:
|
|
404
409
|
current_display.insert(-1, (create_renderable_test_result(i, result)))
|
405
410
|
live.update(Group(*current_display))
|
406
411
|
|
407
|
-
progress.update(
|
412
|
+
progress.update(
|
413
|
+
task_id, description=_('test_completed')
|
414
|
+
) # 完了メッセージに更新
|
408
415
|
current_display[-1] = create_renderable_test_info(test.info, progress)
|
409
416
|
live.update(Group(*current_display))
|
410
417
|
|
@@ -412,9 +419,7 @@ def render_results(test: TestRunner) -> None:
|
|
412
419
|
def run_test(path_of_code: str) -> None:
|
413
420
|
html_paths = [f for f in os.listdir('.') if f.endswith('.html')]
|
414
421
|
if not html_paths:
|
415
|
-
print(
|
416
|
-
'問題のファイルが見つかりません。\n問題のファイルが存在するディレクトリーに移動してから実行してください。'
|
417
|
-
)
|
422
|
+
print(_('problem_file_not_found'))
|
418
423
|
return
|
419
424
|
|
420
425
|
with open(html_paths[0], 'r') as file:
|
@@ -425,7 +430,7 @@ def run_test(path_of_code: str) -> None:
|
|
425
430
|
render_results(test)
|
426
431
|
|
427
432
|
|
428
|
-
@click.command(short_help='
|
433
|
+
@click.command(short_help=_('cmd_test'), help=_('cmd_test'))
|
429
434
|
@add_file_selector('files', filetypes=COMPILED_LANGUAGES + INTERPRETED_LANGUAGES)
|
430
435
|
def test(files):
|
431
436
|
"""指定したソースコードをサンプルケースでテストします。"""
|
atcdr/util/fileops.py
CHANGED
@@ -7,6 +7,7 @@ import questionary as q
|
|
7
7
|
import rich_click as click
|
8
8
|
|
9
9
|
from atcdr.util.filetype import FILE_EXTENSIONS, Lang
|
10
|
+
from atcdr.util.i18n import _
|
10
11
|
|
11
12
|
|
12
13
|
def collect_files(
|
@@ -41,9 +42,9 @@ def collect_files(
|
|
41
42
|
|
42
43
|
def select_files_interactively(files: List[str]) -> List[str]:
|
43
44
|
target_file = q.select(
|
44
|
-
message='
|
45
|
+
message=_('multiple_files_found'),
|
45
46
|
choices=[q.Choice(title=file, value=file) for file in files],
|
46
|
-
instruction='\n
|
47
|
+
instruction='\n ' + _('navigate_with_arrows'),
|
47
48
|
pointer='❯',
|
48
49
|
qmark='',
|
49
50
|
style=q.Style(
|
@@ -78,7 +79,7 @@ def add_file_selector(
|
|
78
79
|
# 2) ファイル収集 (非再帰固定)
|
79
80
|
files = collect_files(patterns, tuple(exts), recursive=False)
|
80
81
|
if not files:
|
81
|
-
click.echo('
|
82
|
+
click.echo(_('target_file_not_found'))
|
82
83
|
ctx.exit(1)
|
83
84
|
|
84
85
|
# 3) 候補が1つなら即実行
|
@@ -89,7 +90,7 @@ def add_file_selector(
|
|
89
90
|
if not patterns:
|
90
91
|
selected = select_files_interactively(files)
|
91
92
|
if not selected:
|
92
|
-
click.echo('
|
93
|
+
click.echo(_('file_not_selected'))
|
93
94
|
ctx.exit(1)
|
94
95
|
selected_list = [selected]
|
95
96
|
return ctx.invoke(f, **{arg_name: selected_list}, **kwargs)
|