Skip to content

Commit d7041fb

Browse files
committed
make batch execution noninteractive by default
* replace --noninteractive flag with --batch-warn, with an inverted meaning * when --batch-warn is set, it overrides myclirc's destructive_warning setting * let --execute work exactly like --batch * send destructive-query confirmation prompts to the standard error, since users running batch scripts will often have the standard output redirected This is a breaking change, not least because the --noninteractive flag no longer has an effect, and disappears from the docs. But passing it causes a Click warning referring the user to --batch-warn. Preparation for release 2.0.
1 parent 7063a3f commit d7041fb

9 files changed

Lines changed: 111 additions & 15 deletions

File tree

changelog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
Upcoming (TBD)
22
==============
33

4+
Breaking Changes
5+
---------
6+
* Make `--batch` and `--execute` non-interactive by default.
7+
8+
49
Internal
510
---------
611
* Improve test coverage for DSN variable expansion.

mycli/main.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ class CliArgs:
254254
clickdc=None,
255255
help='Warn before running a destructive query.',
256256
)
257+
batch_warn: bool = clickdc.option(
258+
is_flag=True,
259+
help='Warn before running a destructive query when executing a script.',
260+
)
257261
local_infile: bool | None = clickdc.option(
258262
type=bool,
259263
is_flag=False,
@@ -289,9 +293,11 @@ class CliArgs:
289293
type=str,
290294
help='SQL script to execute in batch mode.',
291295
)
296+
# deprecated 2026-06-20
292297
noninteractive: bool = clickdc.option(
293298
is_flag=True,
294-
help="Don't prompt during batch input. Recommended.",
299+
hidden=True,
300+
deprecated='See --batch-warn.',
295301
)
296302
format: str | None = clickdc.option(
297303
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.batch_warn 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.batch_warn 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
@@ -828,6 +828,8 @@ def test_help_strings_end_with_periods():
828828
"""Make sure click options have help text that end with a period."""
829829
for param in click_entrypoint.params:
830830
if isinstance(param, click.core.Option):
831+
if param.hidden:
832+
continue
831833
assert hasattr(param, "help")
832834
assert param.help.endswith(".")
833835

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+
batch_warn: 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(batch_warn=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(batch_warn=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(batch_warn=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+
batch_warn: 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', batch_warn=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', batch_warn=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', batch_warn=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)