Skip to content

Commit 346949f

Browse files
committed
move --list-ssh-config out of main.py
The added tests are not 100% equivalent to the removed tests, but the execution path is also deprecated.
1 parent ba0435f commit 346949f

7 files changed

Lines changed: 217 additions & 152 deletions

File tree

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Internal
3333
* Refactor suggestion logic into declarative rules.
3434
* Factor the `--batch` execution modes out of `main.py`.
3535
* Move `--checkup` logic to the new `main_modes` with `--batch`.
36+
* Move `--list-ssh-config` logic to the new `main_modes` with `--batch`.
3637
* Sort coverage report in tox suite.
3738
* Skip more tests when a database connection is not present.
3839

mycli/main.py

Lines changed: 3 additions & 36 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.list_ssh_config import main_list_ssh_config
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
@@ -96,16 +97,12 @@
9697
from mycli.packages.special.main import ArgType
9798
from mycli.packages.special.utils import format_uptime, get_ssl_version, get_uptime, get_warning_count
9899
from mycli.packages.sqlresult import SQLResult
100+
from mycli.packages.ssh_utils import read_ssh_config
99101
from mycli.packages.string_utils import sanitize_terminal_title
100102
from mycli.packages.tabular_output import sql_format
101103
from mycli.sqlcompleter import SQLCompleter
102104
from mycli.sqlexecute import FIELD_TYPES, SQLExecute
103105

104-
try:
105-
import paramiko
106-
except ImportError:
107-
from mycli.packages.paramiko_stub import paramiko # type: ignore[no-redef]
108-
109106
sqlparse.engine.grouping.MAX_GROUPING_DEPTH = None # type: ignore[assignment]
110107
sqlparse.engine.grouping.MAX_GROUPING_TOKENS = None # type: ignore[assignment]
111108

@@ -2327,19 +2324,7 @@ def get_password_from_file(password_file: str | None) -> str | None:
23272324
sys.exit(0)
23282325

23292326
if cli_args.list_ssh_config:
2330-
ssh_config = read_ssh_config(cli_args.ssh_config_path)
2331-
try:
2332-
host_entries = ssh_config.get_hostnames()
2333-
except KeyError:
2334-
click.secho('Error reading ssh config', err=True, fg="red")
2335-
sys.exit(1)
2336-
for host_entry in host_entries:
2337-
if cli_args.verbose:
2338-
host_config = ssh_config.lookup(host_entry)
2339-
click.secho(f"{host_entry} : {host_config.get('hostname')}")
2340-
else:
2341-
click.secho(host_entry)
2342-
sys.exit(0)
2327+
sys.exit(main_list_ssh_config(mycli, cli_args))
23432328

23442329
if 'MYSQL_UNIX_PORT' in os.environ:
23452330
# deprecated 2026-03
@@ -2798,24 +2783,6 @@ def edit_and_execute(event: KeyPressEvent) -> None:
27982783
buff.open_in_editor(validate_and_handle=False)
27992784

28002785

2801-
def read_ssh_config(ssh_config_path: str):
2802-
ssh_config = paramiko.config.SSHConfig()
2803-
try:
2804-
with open(ssh_config_path) as f:
2805-
ssh_config.parse(f)
2806-
except FileNotFoundError as e:
2807-
click.secho(str(e), err=True, fg="red")
2808-
sys.exit(1)
2809-
# Paramiko prior to version 2.7 raises Exception on parse errors.
2810-
# In 2.7 it has become paramiko.ssh_exception.SSHException,
2811-
# but let's catch everything for compatibility
2812-
except Exception as err:
2813-
click.secho(f"Could not parse SSH configuration file {ssh_config_path}:\n{err} ", err=True, fg="red")
2814-
sys.exit(1)
2815-
else:
2816-
return ssh_config
2817-
2818-
28192786
def filtered_sys_argv() -> list[str]:
28202787
args = sys.argv[1:]
28212788
if args == ['-h']:
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
import click
6+
7+
from mycli.packages.ssh_utils import read_ssh_config
8+
9+
if TYPE_CHECKING:
10+
from mycli.main import CliArgs, MyCli
11+
12+
13+
def main_list_ssh_config(mycli: 'MyCli', cli_args: 'CliArgs') -> int:
14+
ssh_config = read_ssh_config(cli_args.ssh_config_path)
15+
try:
16+
host_entries = ssh_config.get_hostnames()
17+
except KeyError:
18+
click.secho('Error reading ssh config', err=True, fg="red")
19+
return 1
20+
for host_entry in host_entries:
21+
if cli_args.verbose:
22+
host_config = ssh_config.lookup(host_entry)
23+
click.secho(f"{host_entry} : {host_config.get('hostname')}")
24+
else:
25+
click.secho(host_entry)
26+
return 0

mycli/packages/ssh_utils.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import sys
2+
3+
import click
4+
5+
try:
6+
import paramiko
7+
except ImportError:
8+
from mycli.packages.paramiko_stub import paramiko # type: ignore[no-redef]
9+
10+
11+
# it isn't cool that this utility function can exit(), but it is slated to be removed anyway
12+
def read_ssh_config(ssh_config_path: str):
13+
ssh_config = paramiko.config.SSHConfig()
14+
try:
15+
with open(ssh_config_path) as f:
16+
ssh_config.parse(f)
17+
except FileNotFoundError as e:
18+
click.secho(str(e), err=True, fg="red")
19+
sys.exit(1)
20+
# Paramiko prior to version 2.7 raises Exception on parse errors.
21+
# In 2.7 it has become paramiko.ssh_exception.SSHException,
22+
# but let's catch everything for compatibility
23+
except Exception as err:
24+
click.secho(f"Could not parse SSH configuration file {ssh_config_path}:\n{err} ", err=True, fg="red")
25+
sys.exit(1)
26+
else:
27+
return ssh_config
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from typing import Any, cast
5+
6+
import mycli.main_modes.list_ssh_config as list_ssh_config_mode
7+
8+
9+
@dataclass
10+
class DummyCliArgs:
11+
ssh_config_path: str = 'ssh_config'
12+
verbose: bool = False
13+
14+
15+
class DummySSHConfig:
16+
def __init__(self, hostnames: list[str] | Exception, lookups: dict[str, dict[str, str]] | None = None) -> None:
17+
self.hostnames = hostnames
18+
self.lookups = lookups or {}
19+
20+
def get_hostnames(self) -> list[str]:
21+
if isinstance(self.hostnames, Exception):
22+
raise self.hostnames
23+
return self.hostnames
24+
25+
def lookup(self, hostname: str) -> dict[str, str]:
26+
return self.lookups[hostname]
27+
28+
29+
def main_list_ssh_config(cli_args: DummyCliArgs) -> int:
30+
return list_ssh_config_mode.main_list_ssh_config(cast(Any, object()), cast(Any, cli_args))
31+
32+
33+
def test_main_list_ssh_config_lists_hostnames(monkeypatch) -> None:
34+
secho_calls: list[tuple[str, bool | None, str | None]] = []
35+
ssh_config = DummySSHConfig(['prod', 'staging'])
36+
37+
monkeypatch.setattr(list_ssh_config_mode, 'read_ssh_config', lambda _path: ssh_config)
38+
monkeypatch.setattr(
39+
list_ssh_config_mode.click,
40+
'secho',
41+
lambda message, err=None, fg=None: secho_calls.append((message, err, fg)),
42+
)
43+
44+
result = main_list_ssh_config(DummyCliArgs(verbose=False))
45+
46+
assert result == 0
47+
assert secho_calls == [
48+
('prod', None, None),
49+
('staging', None, None),
50+
]
51+
52+
53+
def test_main_list_ssh_config_lists_verbose_host_details(monkeypatch) -> None:
54+
secho_calls: list[tuple[str, bool | None, str | None]] = []
55+
ssh_config = DummySSHConfig(
56+
['prod'],
57+
lookups={'prod': {'hostname': 'db.example.com'}},
58+
)
59+
60+
monkeypatch.setattr(list_ssh_config_mode, 'read_ssh_config', lambda _path: ssh_config)
61+
monkeypatch.setattr(
62+
list_ssh_config_mode.click,
63+
'secho',
64+
lambda message, err=None, fg=None: secho_calls.append((message, err, fg)),
65+
)
66+
67+
result = main_list_ssh_config(DummyCliArgs(verbose=True))
68+
69+
assert result == 0
70+
assert secho_calls == [('prod : db.example.com', None, None)]
71+
72+
73+
def test_main_list_ssh_config_reports_host_lookup_errors(monkeypatch) -> None:
74+
secho_calls: list[tuple[str, bool | None, str | None]] = []
75+
ssh_config = DummySSHConfig(KeyError('bad ssh config'))
76+
77+
monkeypatch.setattr(list_ssh_config_mode, 'read_ssh_config', lambda _path: ssh_config)
78+
monkeypatch.setattr(
79+
list_ssh_config_mode.click,
80+
'secho',
81+
lambda message, err=None, fg=None: secho_calls.append((message, err, fg)),
82+
)
83+
84+
result = main_list_ssh_config(DummyCliArgs())
85+
86+
assert result == 1
87+
assert secho_calls == [('Error reading ssh config', True, 'red')]

0 commit comments

Comments
 (0)