Skip to content

Commit ea9c5a8

Browse files
committed
add a --progress progress-bar option
which only works in conjunction with --batch, not STDIN, as it needs to read over the file to count the number of goal statements. Care is also taken that a plain file, not a FIFO, is passed to --batch.
1 parent 1343081 commit ea9c5a8

3 files changed

Lines changed: 147 additions & 12 deletions

File tree

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Features
55
---------
66
* Respond to `-h` alone with the helpdoc.
77
* Allow `--hostname` as an alias for `--host`.
8+
* Add a `--progress` progress-bar option with `--batch`.
89

910

1011
Bug Fixes

mycli/main.py

Lines changed: 71 additions & 12 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
@@ -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 with --batch.',
2175+
)
21702176
use_keyring: str | None = clickdc.option(
21712177
type=click.Choice(['true', 'false', 'reset']),
21722178
default=None,
@@ -2711,17 +2717,70 @@ def dispatch_batch_statements(statements: str, batch_counter: int) -> None:
27112717
click.secho(str(e), err=True, fg="red")
27122718
sys.exit(1)
27132719

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

20332035

2036+
def test_batch_file_with_progress(monkeypatch):
2037+
mycli_main, MockMyCli = _noninteractive_mock_mycli(monkeypatch)
2038+
runner = CliRunner()
2039+
2040+
class DummyProgressBar:
2041+
calls = []
2042+
2043+
def __init__(self, *args, **kwargs):
2044+
pass
2045+
2046+
def __enter__(self):
2047+
return self
2048+
2049+
def __exit__(self, exc_type, exc, tb):
2050+
return False
2051+
2052+
def __call__(self, iterable):
2053+
values = list(iterable)
2054+
DummyProgressBar.calls.append(values)
2055+
return values
2056+
2057+
monkeypatch.setattr(mycli_main, 'ProgressBar', DummyProgressBar)
2058+
monkeypatch.setattr(mycli_main.prompt_toolkit.output, 'create_output', lambda **kwargs: object())
2059+
monkeypatch.setattr(
2060+
mycli_main,
2061+
'sys',
2062+
SimpleNamespace(
2063+
stdin=SimpleNamespace(isatty=lambda: False),
2064+
stderr=SimpleNamespace(isatty=lambda: True),
2065+
exit=sys.exit,
2066+
),
2067+
)
2068+
2069+
with NamedTemporaryFile(prefix=TEMPFILE_PREFIX, mode='w', delete=False) as batch_file:
2070+
batch_file.write('select 2;\nselect 2;\nselect 2;\n')
2071+
batch_file.flush()
2072+
2073+
try:
2074+
result = runner.invoke(
2075+
mycli_main.click_entrypoint,
2076+
args=['--batch', batch_file.name, '--progress'],
2077+
)
2078+
assert result.exit_code == 0
2079+
assert MockMyCli.ran_queries == ['select 2;\n', 'select 2;\n', 'select 2;\n']
2080+
assert DummyProgressBar.calls == [[0, 1, 2]]
2081+
finally:
2082+
os.remove(batch_file.name)
2083+
2084+
2085+
def test_batch_file_with_progress_requires_plain_file(monkeypatch, tmp_path):
2086+
mycli_main, MockMyCli = _noninteractive_mock_mycli(monkeypatch)
2087+
runner = CliRunner()
2088+
2089+
monkeypatch.setattr(
2090+
mycli_main,
2091+
'sys',
2092+
SimpleNamespace(
2093+
stdin=SimpleNamespace(isatty=lambda: False),
2094+
stderr=SimpleNamespace(isatty=lambda: True),
2095+
exit=sys.exit,
2096+
),
2097+
)
2098+
2099+
result = runner.invoke(
2100+
mycli_main.click_entrypoint,
2101+
args=['--batch', str(tmp_path), '--progress'],
2102+
)
2103+
2104+
assert result.exit_code != 0
2105+
assert '--progress is only compatible with a plain file.' in result.output
2106+
assert MockMyCli.ran_queries == []
2107+
2108+
20342109
def test_execute_arg_warns_about_ignoring_stdin(monkeypatch):
20352110
mycli_main, MockMyCli = _noninteractive_mock_mycli(monkeypatch)
20362111
runner = CliRunner()

0 commit comments

Comments
 (0)