Skip to content

Commit ccdcdff

Browse files
committed
move --execute code path out of main.py
fixing a bug in which --execute='' was silently ignored
1 parent ba0435f commit ccdcdff

File tree

4 files changed

+172
-28
lines changed

4 files changed

+172
-28
lines changed

changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Bug Fixes
1616
* Better completions refresh on changing databases or ALTERs.
1717
* Make the return value of `FavoriteQueries.list()` a copy.
1818
* Make multi-line detection and special cases more robust.
19+
* Run empty `--execute` arguments instead of ignoring the flag.
1920

2021

2122
Internal
@@ -33,6 +34,7 @@ Internal
3334
* Refactor suggestion logic into declarative rules.
3435
* Factor the `--batch` execution modes out of `main.py`.
3536
* Move `--checkup` logic to the new `main_modes` with `--batch`.
37+
* Move `--execute` logic to the new `main_modes` with `--batch`.
3638
* Sort coverage report in tox suite.
3739
* Skip more tests when a database connection is not present.
3840

mycli/main.py

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
main_batch_without_progress_bar,
8787
)
8888
from mycli.main_modes.checkup import main_checkup
89+
from mycli.main_modes.execute import main_execute_from_cli
8990
from mycli.packages import special
9091
from mycli.packages.filepaths import dir_path_exists, guess_socket_location
9192
from mycli.packages.hybrid_redirection import get_redirect_components, is_redirect_command
@@ -2660,34 +2661,8 @@ def get_password_from_file(password_file: str | None) -> str | None:
26602661
cli_args.port,
26612662
)
26622663

2663-
# --execute argument
2664-
if cli_args.execute:
2665-
if not sys.stdin.isatty():
2666-
click.secho('Ignoring STDIN since --execute was also given.', err=True, fg='red')
2667-
if cli_args.batch:
2668-
click.secho('Ignoring --batch since --execute was also given.', err=True, fg='red')
2669-
try:
2670-
execute_sql = cli_args.execute
2671-
if cli_args.format == 'csv':
2672-
mycli.main_formatter.format_name = 'csv'
2673-
if execute_sql.endswith(r'\G'):
2674-
execute_sql = execute_sql[:-2]
2675-
elif cli_args.format == 'tsv':
2676-
mycli.main_formatter.format_name = 'tsv'
2677-
if execute_sql.endswith(r'\G'):
2678-
execute_sql = execute_sql[:-2]
2679-
elif cli_args.format == 'table':
2680-
mycli.main_formatter.format_name = 'ascii'
2681-
if execute_sql.endswith(r'\G'):
2682-
execute_sql = execute_sql[:-2]
2683-
else:
2684-
mycli.main_formatter.format_name = 'tsv'
2685-
2686-
mycli.run_query(execute_sql, checkpoint=cli_args.checkpoint)
2687-
sys.exit(0)
2688-
except Exception as e:
2689-
click.secho(str(e), err=True, fg="red")
2690-
sys.exit(1)
2664+
if cli_args.execute is not None:
2665+
sys.exit(main_execute_from_cli(mycli, cli_args))
26912666

26922667
if cli_args.batch and cli_args.batch != '-' and cli_args.progress and sys.stderr.isatty():
26932668
sys.exit(main_batch_with_progress_bar(mycli, cli_args))

mycli/main_modes/execute.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from __future__ import annotations
2+
3+
import sys
4+
from typing import TYPE_CHECKING
5+
6+
import click
7+
8+
if TYPE_CHECKING:
9+
from mycli.main import CliArgs, MyCli
10+
11+
12+
def main_execute_from_cli(mycli: 'MyCli', cli_args: 'CliArgs') -> int:
13+
if cli_args.execute is None:
14+
return 1
15+
if not sys.stdin.isatty():
16+
click.secho('Ignoring STDIN since --execute was also given.', err=True, fg='red')
17+
if cli_args.batch:
18+
click.secho('Ignoring --batch since --execute was also given.', err=True, fg='red')
19+
try:
20+
execute_sql = cli_args.execute
21+
if cli_args.format == 'csv':
22+
mycli.main_formatter.format_name = 'csv'
23+
if execute_sql.endswith(r'\G'):
24+
execute_sql = execute_sql[:-2]
25+
elif cli_args.format == 'tsv':
26+
mycli.main_formatter.format_name = 'tsv'
27+
if execute_sql.endswith(r'\G'):
28+
execute_sql = execute_sql[:-2]
29+
elif cli_args.format == 'table':
30+
mycli.main_formatter.format_name = 'ascii'
31+
if execute_sql.endswith(r'\G'):
32+
execute_sql = execute_sql[:-2]
33+
else:
34+
mycli.main_formatter.format_name = 'tsv'
35+
36+
mycli.run_query(execute_sql, checkpoint=cli_args.checkpoint)
37+
return 0
38+
except Exception as e:
39+
click.secho(str(e), err=True, fg="red")
40+
return 1
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from types import SimpleNamespace
5+
from typing import Any, cast
6+
7+
import pytest
8+
9+
import mycli.main_modes.execute as execute_mode
10+
11+
12+
@dataclass
13+
class DummyCliArgs:
14+
execute: str | None
15+
format: str = 'tsv'
16+
batch: str | None = None
17+
checkpoint: str | None = None
18+
19+
20+
@dataclass
21+
class DummyFormatter:
22+
format_name: str | None = None
23+
24+
25+
class DummyMyCli:
26+
def __init__(self, run_query_error: Exception | None = None) -> None:
27+
self.main_formatter = DummyFormatter()
28+
self.run_query_error = run_query_error
29+
self.ran_queries: list[tuple[str, str | None]] = []
30+
31+
def run_query(self, query: str, checkpoint: str | None = None) -> None:
32+
if self.run_query_error is not None:
33+
raise self.run_query_error
34+
self.ran_queries.append((query, checkpoint))
35+
36+
37+
def main_execute_from_cli(mycli: DummyMyCli, cli_args: DummyCliArgs) -> int:
38+
return execute_mode.main_execute_from_cli(cast(Any, mycli), cast(Any, cli_args))
39+
40+
41+
def fake_sys(stdin_tty: bool) -> SimpleNamespace:
42+
return SimpleNamespace(stdin=SimpleNamespace(isatty=lambda: stdin_tty))
43+
44+
45+
def test_main_execute_from_cli_returns_error_when_execute_is_missing() -> None:
46+
assert main_execute_from_cli(DummyMyCli(), DummyCliArgs(execute=None)) == 1
47+
48+
49+
@pytest.mark.parametrize(
50+
('format_name', 'original_sql', 'expected_format', 'expected_sql'),
51+
(
52+
('csv', r'select 1\G', 'csv', 'select 1'),
53+
('tsv', r'select 2\G', 'tsv', 'select 2'),
54+
('table', r'select 3\G', 'ascii', 'select 3'),
55+
('vertical', r'select 4\G', 'tsv', r'select 4\G'),
56+
),
57+
)
58+
def test_main_execute_from_cli_sets_format_and_runs_query(
59+
monkeypatch,
60+
format_name: str,
61+
original_sql: str,
62+
expected_format: str,
63+
expected_sql: str,
64+
) -> None:
65+
secho_calls: list[tuple[str, bool, str]] = []
66+
mycli = DummyMyCli()
67+
cli_args = DummyCliArgs(
68+
execute=original_sql,
69+
format=format_name,
70+
batch='batch.sql',
71+
checkpoint='cp',
72+
)
73+
74+
monkeypatch.setattr(execute_mode, 'sys', fake_sys(stdin_tty=False))
75+
monkeypatch.setattr(
76+
execute_mode.click,
77+
'secho',
78+
lambda message, err, fg: secho_calls.append((message, err, fg)),
79+
)
80+
81+
result = main_execute_from_cli(mycli, cli_args)
82+
83+
assert result == 0
84+
assert mycli.main_formatter.format_name == expected_format
85+
assert mycli.ran_queries == [(expected_sql, 'cp')]
86+
assert secho_calls == [
87+
('Ignoring STDIN since --execute was also given.', True, 'red'),
88+
('Ignoring --batch since --execute was also given.', True, 'red'),
89+
]
90+
91+
92+
def test_main_execute_from_cli_does_not_warn_when_stdin_is_tty_and_batch_is_unset(monkeypatch) -> None:
93+
secho_calls: list[tuple[str, bool, str]] = []
94+
mycli = DummyMyCli()
95+
96+
monkeypatch.setattr(execute_mode, 'sys', fake_sys(stdin_tty=True))
97+
monkeypatch.setattr(
98+
execute_mode.click,
99+
'secho',
100+
lambda message, err, fg: secho_calls.append((message, err, fg)),
101+
)
102+
103+
result = main_execute_from_cli(mycli, DummyCliArgs(execute='select 1', format='csv'))
104+
105+
assert result == 0
106+
assert mycli.main_formatter.format_name == 'csv'
107+
assert mycli.ran_queries == [('select 1', None)]
108+
assert secho_calls == []
109+
110+
111+
def test_main_execute_from_cli_reports_query_errors(monkeypatch) -> None:
112+
secho_calls: list[tuple[str, bool, str]] = []
113+
mycli = DummyMyCli(run_query_error=RuntimeError('boom'))
114+
115+
monkeypatch.setattr(execute_mode, 'sys', fake_sys(stdin_tty=True))
116+
monkeypatch.setattr(
117+
execute_mode.click,
118+
'secho',
119+
lambda message, err, fg: secho_calls.append((message, err, fg)),
120+
)
121+
122+
result = main_execute_from_cli(mycli, DummyCliArgs(execute='select 1', format='table'))
123+
124+
assert result == 1
125+
assert mycli.main_formatter.format_name == 'ascii'
126+
assert mycli.ran_queries == []
127+
assert secho_calls == [('boom', True, 'red')]

0 commit comments

Comments
 (0)