Skip to content

Commit 54f738f

Browse files
authored
Merge pull request #1731 from dbcli/RW/add-batch-progress-bar
Add a `--progress` progress-bar option
2 parents 1f33072 + bde0c16 commit 54f738f

3 files changed

Lines changed: 148 additions & 13 deletions

File tree

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Features
77
* Allow `--hostname` as an alias for `--host`.
88
* Suggest tables with foreign key relationships for JOIN and ON (#975)
99
* Deprecate `$DSN` environment variable in favor of `$MYSQL_DSN`.
10+
* Add a `--progress` progress-bar option with `--batch`.
1011

1112

1213
Bug Fixes

mycli/main.py

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import clickdc
3636
from configobj import ConfigObj
3737
import keyring
38+
import prompt_toolkit
3839
from prompt_toolkit import print_formatted_text
3940
from prompt_toolkit.application.current import get_app
4041
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, ThreadedAutoSuggest
@@ -55,7 +56,8 @@
5556
from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
5657
from prompt_toolkit.lexers import PygmentsLexer
5758
from prompt_toolkit.output import ColorDepth
58-
from prompt_toolkit.shortcuts import CompleteStyle, PromptSession
59+
from prompt_toolkit.shortcuts import CompleteStyle, ProgressBar, PromptSession
60+
from prompt_toolkit.shortcuts.progress_bar import formatters as progress_bar_formatters
5961
import pymysql
6062
from pymysql.constants.CR import CR_SERVER_LOST
6163
from pymysql.constants.ER import ACCESS_DENIED_ERROR, HANDSHAKE_ERROR
@@ -2036,7 +2038,7 @@ class CliArgs:
20362038
)
20372039
ssl_verify_server_cert: bool = clickdc.option(
20382040
is_flag=True,
2039-
help=('Verify server\'s "Common Name" in its cert against hostname used when connecting. This option is disabled by default.'),
2041+
help=("""Verify server's "Common Name" in its cert against hostname used when connecting. This option is disabled by default."""),
20402042
)
20412043
verbose: bool = clickdc.option(
20422044
'-v',
@@ -2167,6 +2169,10 @@ class CliArgs:
21672169
default=0.0,
21682170
help='Pause in seconds between queries in batch mode.',
21692171
)
2172+
progress: bool = clickdc.option(
2173+
is_flag=True,
2174+
help='Show progress on the standard error with --batch.',
2175+
)
21702176
use_keyring: str | None = clickdc.option(
21712177
type=click.Choice(['true', 'false', 'reset']),
21722178
default=None,
@@ -2721,17 +2727,70 @@ def dispatch_batch_statements(statements: str, batch_counter: int) -> None:
27212727
click.secho(str(e), err=True, fg="red")
27222728
sys.exit(1)
27232729

2724-
if cli_args.batch or not sys.stdin.isatty():
2725-
if cli_args.batch:
2726-
if not sys.stdin.isatty() and cli_args.batch != '-':
2727-
click.secho('Ignoring STDIN since --batch was also given.', err=True, fg='red')
2728-
try:
2729-
batch_h = click.open_file(cli_args.batch)
2730-
except (OSError, FileNotFoundError):
2731-
click.secho(f'Failed to open --batch file: {cli_args.batch}', err=True, fg='red')
2732-
sys.exit(1)
2733-
else:
2734-
batch_h = click.get_text_stream('stdin')
2730+
if cli_args.batch and cli_args.batch != '-' and cli_args.progress and sys.stderr.isatty():
2731+
# The actual number of SQL statements can be greater, if there is more than
2732+
# one statement per line, but this is how the progress bar will count.
2733+
goal_statements = 0
2734+
if not sys.stdin.isatty() and cli_args.batch != '-':
2735+
click.secho('Ignoring STDIN since --batch was also given.', err=True, fg='yellow')
2736+
if os.path.exists(cli_args.batch) and not os.path.isfile(cli_args.batch):
2737+
click.secho('--progress is only compatible with a plain file.', err=True, fg='red')
2738+
sys.exit(1)
2739+
try:
2740+
batch_count_h = click.open_file(cli_args.batch)
2741+
for _statement, _counter in statements_from_filehandle(batch_count_h):
2742+
goal_statements += 1
2743+
batch_count_h.close()
2744+
batch_h = click.open_file(cli_args.batch)
2745+
except (OSError, FileNotFoundError):
2746+
click.secho(f'Failed to open --batch file: {cli_args.batch}', err=True, fg='red')
2747+
sys.exit(1)
2748+
except ValueError as e:
2749+
click.secho(f'Error reading --batch file: {cli_args.batch}: {e}', err=True, fg='red')
2750+
sys.exit(1)
2751+
try:
2752+
if goal_statements:
2753+
pb_style = prompt_toolkit.styles.Style.from_dict({'bar-a': 'reverse'})
2754+
custom_formatters = [
2755+
progress_bar_formatters.Bar(start='[', end=']', sym_a=' ', sym_b=' ', sym_c=' '),
2756+
progress_bar_formatters.Text(' '),
2757+
progress_bar_formatters.Progress(),
2758+
progress_bar_formatters.Text(' '),
2759+
progress_bar_formatters.Text('eta ', style='class:time-left'),
2760+
progress_bar_formatters.TimeLeft(),
2761+
progress_bar_formatters.Text(' ', style='class:time-left'),
2762+
]
2763+
err_output = prompt_toolkit.output.create_output(stdout=sys.stderr, always_prefer_tty=True)
2764+
with ProgressBar(style=pb_style, formatters=custom_formatters, output=err_output) as pb:
2765+
for pb_counter in pb(range(goal_statements)):
2766+
statement, _untrusted_counter = next(statements_from_filehandle(batch_h))
2767+
dispatch_batch_statements(statement, pb_counter)
2768+
except (ValueError, StopIteration) as e:
2769+
click.secho(str(e), err=True, fg='red')
2770+
sys.exit(1)
2771+
finally:
2772+
batch_h.close()
2773+
sys.exit(0)
2774+
2775+
if cli_args.batch:
2776+
if not sys.stdin.isatty() and cli_args.batch != '-':
2777+
click.secho('Ignoring STDIN since --batch was also given.', err=True, fg='red')
2778+
try:
2779+
batch_h = click.open_file(cli_args.batch)
2780+
except (OSError, FileNotFoundError):
2781+
click.secho(f'Failed to open --batch file: {cli_args.batch}', err=True, fg='red')
2782+
sys.exit(1)
2783+
try:
2784+
for statement, counter in statements_from_filehandle(batch_h):
2785+
dispatch_batch_statements(statement, counter)
2786+
batch_h.close()
2787+
except ValueError as e:
2788+
click.secho(str(e), err=True, fg='red')
2789+
sys.exit(1)
2790+
sys.exit(0)
2791+
2792+
if not sys.stdin.isatty():
2793+
batch_h = click.get_text_stream('stdin')
27352794
try:
27362795
for statement, counter in statements_from_filehandle(batch_h):
27372796
dispatch_batch_statements(statement, counter)

test/pytests/test_main.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
import io
77
import os
88
import shutil
9+
import sys
910
from tempfile import NamedTemporaryFile
1011
from textwrap import dedent
12+
from types import SimpleNamespace
1113

1214
import click
1315
from click.testing import CliRunner
@@ -2137,6 +2139,79 @@ def test_batch_file(monkeypatch):
21372139
os.remove(batch_file.name)
21382140

21392141

2142+
def test_batch_file_with_progress(monkeypatch):
2143+
mycli_main, MockMyCli = _noninteractive_mock_mycli(monkeypatch)
2144+
runner = CliRunner()
2145+
2146+
class DummyProgressBar:
2147+
calls = []
2148+
2149+
def __init__(self, *args, **kwargs):
2150+
pass
2151+
2152+
def __enter__(self):
2153+
return self
2154+
2155+
def __exit__(self, exc_type, exc, tb):
2156+
return False
2157+
2158+
def __call__(self, iterable):
2159+
values = list(iterable)
2160+
DummyProgressBar.calls.append(values)
2161+
return values
2162+
2163+
monkeypatch.setattr(mycli_main, 'ProgressBar', DummyProgressBar)
2164+
monkeypatch.setattr(mycli_main.prompt_toolkit.output, 'create_output', lambda **kwargs: object())
2165+
monkeypatch.setattr(
2166+
mycli_main,
2167+
'sys',
2168+
SimpleNamespace(
2169+
stdin=SimpleNamespace(isatty=lambda: False),
2170+
stderr=SimpleNamespace(isatty=lambda: True),
2171+
exit=sys.exit,
2172+
),
2173+
)
2174+
2175+
with NamedTemporaryFile(prefix=TEMPFILE_PREFIX, mode='w', delete=False) as batch_file:
2176+
batch_file.write('select 2;\nselect 2;\nselect 2;\n')
2177+
batch_file.flush()
2178+
2179+
try:
2180+
result = runner.invoke(
2181+
mycli_main.click_entrypoint,
2182+
args=['--batch', batch_file.name, '--progress'],
2183+
)
2184+
assert result.exit_code == 0
2185+
assert MockMyCli.ran_queries == ['select 2;\n', 'select 2;\n', 'select 2;\n']
2186+
assert DummyProgressBar.calls == [[0, 1, 2]]
2187+
finally:
2188+
os.remove(batch_file.name)
2189+
2190+
2191+
def test_batch_file_with_progress_requires_plain_file(monkeypatch, tmp_path):
2192+
mycli_main, MockMyCli = _noninteractive_mock_mycli(monkeypatch)
2193+
runner = CliRunner()
2194+
2195+
monkeypatch.setattr(
2196+
mycli_main,
2197+
'sys',
2198+
SimpleNamespace(
2199+
stdin=SimpleNamespace(isatty=lambda: False),
2200+
stderr=SimpleNamespace(isatty=lambda: True),
2201+
exit=sys.exit,
2202+
),
2203+
)
2204+
2205+
result = runner.invoke(
2206+
mycli_main.click_entrypoint,
2207+
args=['--batch', str(tmp_path), '--progress'],
2208+
)
2209+
2210+
assert result.exit_code != 0
2211+
assert '--progress is only compatible with a plain file.' in result.output
2212+
assert MockMyCli.ran_queries == []
2213+
2214+
21402215
def test_execute_arg_warns_about_ignoring_stdin(monkeypatch):
21412216
mycli_main, MockMyCli = _noninteractive_mock_mycli(monkeypatch)
21422217
runner = CliRunner()

0 commit comments

Comments
 (0)