Skip to content

Commit f3b0ea1

Browse files
authored
Merge pull request #1951 from dbcli/RW/batch-default-non-interactive
Make batch execution noninteractive by default
2 parents be011eb + 51c1839 commit f3b0ea1

9 files changed

Lines changed: 112 additions & 15 deletions

File tree

changelog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ Breaking Changes
1010
* Remove support for deprecated SSH jump functionality.
1111
* Remove support for `my.cnf` vendor MySQL option files.
1212
* Remove support for `.myclirc` files in the current working directory.
13+
* Make `--batch` and `--execute` non-interactive by default.
14+
15+
16+
Features
17+
---------
18+
* Add `--warn-batch` flag, which is off by default.
1319

1420

1521
Documentation

mycli/main.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,10 @@ class CliArgs:
201201
clickdc=None,
202202
help='Warn before running a destructive query.',
203203
)
204+
warn_batch: bool = clickdc.option(
205+
is_flag=True,
206+
help='Warn before running a destructive query when executing a script.',
207+
)
204208
local_infile: bool | None = clickdc.option(
205209
type=bool,
206210
is_flag=False,
@@ -236,9 +240,11 @@ class CliArgs:
236240
type=str,
237241
help='SQL script to execute in batch mode.',
238242
)
243+
# deprecated 2026-06-20
239244
noninteractive: bool = clickdc.option(
240245
is_flag=True,
241-
help="Don't prompt during batch input. Recommended.",
246+
hidden=True,
247+
deprecated='See --warn-batch.',
242248
)
243249
format: str | None = clickdc.option(
244250
type=click.Choice(['default', 'csv', 'tsv', 'table']),

mycli/main_modes/batch.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,17 +114,16 @@ def dispatch_batch_statements(
114114
else:
115115
mycli.main_formatter.format_name = 'tsv'
116116

117-
warn_confirmed: bool | None = True
118-
if not cli_args.noninteractive and mycli.destructive_warning and is_destructive(mycli.destructive_keywords, statements):
117+
execution_confirmed: bool | None = True
118+
if cli_args.warn_batch and is_destructive(mycli.destructive_keywords, statements):
119119
try:
120120
# this seems to work, even though we are reading from stdin above
121121
sys.stdin = open('/dev/tty')
122-
# bug: the prompt will not be visible if stdout is redirected
123-
warn_confirmed = confirm_destructive_query(mycli.destructive_keywords, statements)
122+
execution_confirmed = confirm_destructive_query(mycli.destructive_keywords, statements)
124123
except (IOError, OSError) as e:
125124
mycli.logger.warning('Unable to open TTY as stdin.')
126125
raise e
127-
if warn_confirmed:
126+
if execution_confirmed:
128127
if cli_args.throttle > 0 and batch_counter >= 1:
129128
time.sleep(cli_args.throttle)
130129
mycli.run_query(statements, checkpoint=cli_args.checkpoint, new_line=True)

mycli/main_modes/execute.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
import click
77

8+
from mycli.packages.interactive_utils import confirm_destructive_query
9+
from mycli.packages.sql_utils import is_destructive
10+
811
if TYPE_CHECKING:
912
from mycli.client import MyCli
1013
from mycli.main import CliArgs
@@ -34,8 +37,19 @@ def main_execute_from_cli(mycli: 'MyCli', cli_args: 'CliArgs') -> int:
3437
else:
3538
mycli.main_formatter.format_name = 'tsv'
3639

37-
mycli.run_query(execute_sql, checkpoint=cli_args.checkpoint)
38-
return 0
40+
execution_confirmed: bool | None = True
41+
if cli_args.warn_batch and is_destructive(mycli.destructive_keywords, execute_sql):
42+
try:
43+
sys.stdin = open('/dev/tty')
44+
execution_confirmed = confirm_destructive_query(mycli.destructive_keywords, execute_sql)
45+
except (IOError, OSError) as e:
46+
mycli.logger.warning('Unable to open TTY as stdin.')
47+
raise e
48+
if execution_confirmed:
49+
mycli.run_query(execute_sql, checkpoint=cli_args.checkpoint)
50+
return 0
51+
else:
52+
return 1
3953
except Exception as e:
4054
click.secho(str(e), err=True, fg="red")
4155
return 1

mycli/packages/interactive_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def confirm_destructive_query(keywords: list[str], queries: str) -> bool | None:
3636
"""
3737
prompt_text = "You're about to run a destructive command.\nDo you want to proceed? (y/n)"
3838
if is_destructive(keywords, queries) and sys.stdin.isatty():
39-
return prompt(prompt_text, type=BOOLEAN_TYPE)
39+
return prompt(prompt_text, type=BOOLEAN_TYPE, err=True)
4040
else:
4141
return None
4242

test/pytests/test_interactive_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def fake_is_destructive(keywords: list[str], query: str) -> bool:
9292
assert prompt_calls == [
9393
(
9494
("You're about to run a destructive command.\nDo you want to proceed? (y/n)",),
95-
{'type': interactive_utils.BOOLEAN_TYPE},
95+
{'type': interactive_utils.BOOLEAN_TYPE, 'err': True},
9696
)
9797
]
9898

@@ -120,7 +120,7 @@ def fake_is_destructive(keywords: list[str], query: str) -> bool:
120120
assert prompt_calls == [
121121
(
122122
("You're about to run a destructive command.\nDo you want to proceed? (y/n)",),
123-
{'type': interactive_utils.BOOLEAN_TYPE},
123+
{'type': interactive_utils.BOOLEAN_TYPE, 'err': True},
124124
)
125125
]
126126

test/pytests/test_main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,8 @@ def test_help_strings_end_with_periods():
772772
"""Make sure click options have help text that end with a period."""
773773
for param in click_entrypoint.params:
774774
if isinstance(param, click.core.Option):
775+
if param.hidden:
776+
continue
775777
assert hasattr(param, "help")
776778
assert param.help.endswith(".")
777779

test/pytests/test_main_modes_batch.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
@dataclass
2525
class DummyCliArgs:
2626
format: str = 'tsv'
27-
noninteractive: bool = True
27+
warn_batch: bool = False
2828
throttle: float = 0.0
2929
checkpoint: str | TextIOWrapper | None = None
3030
batch: str | None = None
@@ -428,7 +428,7 @@ def test_dispatch_batch_statements_sets_expected_output_format(
428428

429429
def test_dispatch_batch_statements_confirms_destructive_queries_before_running(monkeypatch) -> None:
430430
mycli = DummyMyCli(destructive_warning=True)
431-
cli_args = DummyCliArgs(noninteractive=False)
431+
cli_args = DummyCliArgs(warn_batch=True)
432432
opened_tty = object()
433433

434434
monkeypatch.setattr(batch_mode, 'is_destructive', lambda _keywords, _statement: True)
@@ -444,7 +444,7 @@ def test_dispatch_batch_statements_confirms_destructive_queries_before_running(m
444444

445445
def test_dispatch_batch_statements_skips_query_when_destructive_confirmation_is_rejected(monkeypatch) -> None:
446446
mycli = DummyMyCli(destructive_warning=True)
447-
cli_args = DummyCliArgs(noninteractive=False)
447+
cli_args = DummyCliArgs(warn_batch=True)
448448

449449
monkeypatch.setattr(batch_mode, 'is_destructive', lambda _keywords, _statement: True)
450450
monkeypatch.setattr(batch_mode, 'confirm_destructive_query', lambda _keywords, _statement: False)
@@ -458,7 +458,7 @@ def test_dispatch_batch_statements_skips_query_when_destructive_confirmation_is_
458458

459459
def test_dispatch_batch_statements_raises_when_tty_cannot_be_opened(monkeypatch) -> None:
460460
mycli = DummyMyCli(destructive_warning=True)
461-
cli_args = DummyCliArgs(noninteractive=False)
461+
cli_args = DummyCliArgs(warn_batch=True)
462462

463463
monkeypatch.setattr(batch_mode, 'is_destructive', lambda _keywords, _statement: True)
464464
monkeypatch.setattr(batch_mode, 'open', lambda _path: (_ for _ in ()).throw(OSError('tty unavailable')), raising=False)

test/pytests/test_main_modes_execute.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import builtins
34
from dataclasses import dataclass
45
from types import SimpleNamespace
56
from typing import Any, cast
@@ -14,6 +15,7 @@ class DummyCliArgs:
1415
execute: str | None
1516
format: str = 'tsv'
1617
batch: str | None = None
18+
warn_batch: bool = False
1719
checkpoint: str | None = None
1820

1921

@@ -22,11 +24,21 @@ class DummyFormatter:
2224
format_name: str | None = None
2325

2426

27+
class DummyLogger:
28+
def __init__(self) -> None:
29+
self.warning_calls: list[str] = []
30+
31+
def warning(self, message: str) -> None:
32+
self.warning_calls.append(message)
33+
34+
2535
class DummyMyCli:
2636
def __init__(self, run_query_error: Exception | None = None) -> None:
2737
self.main_formatter = DummyFormatter()
2838
self.run_query_error = run_query_error
2939
self.ran_queries: list[tuple[str, str | None]] = []
40+
self.destructive_keywords = ['drop']
41+
self.logger = DummyLogger()
3042

3143
def run_query(self, query: str, checkpoint: str | None = None) -> None:
3244
if self.run_query_error is not None:
@@ -125,3 +137,61 @@ def test_main_execute_from_cli_reports_query_errors(monkeypatch) -> None:
125137
assert mycli.main_formatter.format_name == 'ascii'
126138
assert mycli.ran_queries == []
127139
assert secho_calls == [('boom', True, 'red')]
140+
141+
142+
def test_main_execute_from_cli_confirms_destructive_query(monkeypatch) -> None:
143+
mycli = DummyMyCli()
144+
tty = object()
145+
confirm_calls: list[tuple[list[str], str]] = []
146+
147+
monkeypatch.setattr(execute_mode, 'sys', fake_sys(stdin_tty=True))
148+
monkeypatch.setattr(execute_mode, 'is_destructive', lambda keywords, query: True)
149+
monkeypatch.setattr(builtins, 'open', lambda path: tty)
150+
151+
def confirm_destructive_query(keywords: list[str], query: str) -> bool:
152+
confirm_calls.append((keywords, query))
153+
return True
154+
155+
monkeypatch.setattr(execute_mode, 'confirm_destructive_query', confirm_destructive_query)
156+
157+
result = main_execute_from_cli(mycli, DummyCliArgs(execute='drop table t', warn_batch=True))
158+
159+
assert result == 0
160+
assert execute_mode.sys.stdin is tty
161+
assert confirm_calls == [(['drop'], 'drop table t')]
162+
assert mycli.ran_queries == [('drop table t', None)]
163+
164+
165+
def test_main_execute_from_cli_returns_error_when_destructive_query_is_rejected(monkeypatch) -> None:
166+
mycli = DummyMyCli()
167+
168+
monkeypatch.setattr(execute_mode, 'sys', fake_sys(stdin_tty=True))
169+
monkeypatch.setattr(execute_mode, 'is_destructive', lambda keywords, query: True)
170+
monkeypatch.setattr(builtins, 'open', lambda path: object())
171+
monkeypatch.setattr(execute_mode, 'confirm_destructive_query', lambda keywords, query: False)
172+
173+
result = main_execute_from_cli(mycli, DummyCliArgs(execute='drop table t', warn_batch=True))
174+
175+
assert result == 1
176+
assert mycli.ran_queries == []
177+
178+
179+
def test_main_execute_from_cli_reports_tty_open_error_for_destructive_query(monkeypatch) -> None:
180+
secho_calls: list[tuple[str, bool, str]] = []
181+
mycli = DummyMyCli()
182+
183+
monkeypatch.setattr(execute_mode, 'sys', fake_sys(stdin_tty=True))
184+
monkeypatch.setattr(execute_mode, 'is_destructive', lambda keywords, query: True)
185+
monkeypatch.setattr(builtins, 'open', lambda path: (_ for _ in ()).throw(OSError('no tty')))
186+
monkeypatch.setattr(
187+
execute_mode.click,
188+
'secho',
189+
lambda message, err, fg: secho_calls.append((message, err, fg)),
190+
)
191+
192+
result = main_execute_from_cli(mycli, DummyCliArgs(execute='drop table t', warn_batch=True))
193+
194+
assert result == 1
195+
assert mycli.logger.warning_calls == ['Unable to open TTY as stdin.']
196+
assert mycli.ran_queries == []
197+
assert secho_calls == [('no tty', True, 'red')]

0 commit comments

Comments
 (0)