Skip to content

Commit c0180a7

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 0bf4235 commit c0180a7

File tree

3 files changed

+147
-12
lines changed

3 files changed

+147
-12
lines changed

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Upcoming (TBD)
44
Features
55
---------
66
* Respond to `-h` alone with the helpdoc.
7+
* Add a `--progress` progress-bar option with `--batch`.
78

89

910
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
@@ -2165,6 +2167,10 @@ class CliArgs:
21652167
default=0.0,
21662168
help='Pause in seconds between queries in batch mode.',
21672169
)
2170+
progress: bool = clickdc.option(
2171+
is_flag=True,
2172+
help='Show progress with --batch.',
2173+
)
21682174
use_keyring: str | None = clickdc.option(
21692175
type=click.Choice(['true', 'false', 'reset']),
21702176
default=None,
@@ -2709,17 +2715,70 @@ def dispatch_batch_statements(statements: str, batch_counter: int) -> None:
27092715
click.secho(str(e), err=True, fg="red")
27102716
sys.exit(1)
27112717

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

19781980

1981+
def test_batch_file_with_progress(monkeypatch):
1982+
mycli_main, MockMyCli = _noninteractive_mock_mycli(monkeypatch)
1983+
runner = CliRunner()
1984+
1985+
class DummyProgressBar:
1986+
calls = []
1987+
1988+
def __init__(self, *args, **kwargs):
1989+
pass
1990+
1991+
def __enter__(self):
1992+
return self
1993+
1994+
def __exit__(self, exc_type, exc, tb):
1995+
return False
1996+
1997+
def __call__(self, iterable):
1998+
values = list(iterable)
1999+
DummyProgressBar.calls.append(values)
2000+
return values
2001+
2002+
monkeypatch.setattr(mycli_main, 'ProgressBar', DummyProgressBar)
2003+
monkeypatch.setattr(mycli_main.prompt_toolkit.output, 'create_output', lambda **kwargs: object())
2004+
monkeypatch.setattr(
2005+
mycli_main,
2006+
'sys',
2007+
SimpleNamespace(
2008+
stdin=SimpleNamespace(isatty=lambda: False),
2009+
stderr=SimpleNamespace(isatty=lambda: True),
2010+
exit=sys.exit,
2011+
),
2012+
)
2013+
2014+
with NamedTemporaryFile(prefix=TEMPFILE_PREFIX, mode='w', delete=False) as batch_file:
2015+
batch_file.write('select 2;\nselect 2;\nselect 2;\n')
2016+
batch_file.flush()
2017+
2018+
try:
2019+
result = runner.invoke(
2020+
mycli_main.click_entrypoint,
2021+
args=['--batch', batch_file.name, '--progress'],
2022+
)
2023+
assert result.exit_code == 0
2024+
assert MockMyCli.ran_queries == ['select 2;\n', 'select 2;\n', 'select 2;\n']
2025+
assert DummyProgressBar.calls == [[0, 1, 2]]
2026+
finally:
2027+
os.remove(batch_file.name)
2028+
2029+
2030+
def test_batch_file_with_progress_requires_plain_file(monkeypatch, tmp_path):
2031+
mycli_main, MockMyCli = _noninteractive_mock_mycli(monkeypatch)
2032+
runner = CliRunner()
2033+
2034+
monkeypatch.setattr(
2035+
mycli_main,
2036+
'sys',
2037+
SimpleNamespace(
2038+
stdin=SimpleNamespace(isatty=lambda: False),
2039+
stderr=SimpleNamespace(isatty=lambda: True),
2040+
exit=sys.exit,
2041+
),
2042+
)
2043+
2044+
result = runner.invoke(
2045+
mycli_main.click_entrypoint,
2046+
args=['--batch', str(tmp_path), '--progress'],
2047+
)
2048+
2049+
assert result.exit_code != 0
2050+
assert '--progress is only compatible with a plain file.' in result.output
2051+
assert MockMyCli.ran_queries == []
2052+
2053+
19792054
def test_execute_arg_warns_about_ignoring_stdin(monkeypatch):
19802055
mycli_main, MockMyCli = _noninteractive_mock_mycli(monkeypatch)
19812056
runner = CliRunner()

0 commit comments

Comments
 (0)