Skip to content

Commit ae896ad

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 36738e2 commit ae896ad

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
@@ -38,6 +38,7 @@ Internal
3838
* Move `--checkup` logic to the new `main_modes` with `--batch`.
3939
* Move `--execute` logic to the new `main_modes` with `--batch`.
4040
* Move `--list-dsn` logic to the new `main_modes` with `--batch`.
41+
* Move `--list-ssh-config` logic to the new `main_modes` with `--batch`.
4142
* Sort coverage report in tox suite.
4243
* Skip more tests when a database connection is not present.
4344

mycli/main.py

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
from mycli.main_modes.checkup import main_checkup
8989
from mycli.main_modes.execute import main_execute_from_cli
9090
from mycli.main_modes.list_dsn import main_list_dsn
91+
from mycli.main_modes.list_ssh_config import main_list_ssh_config
9192
from mycli.packages import special
9293
from mycli.packages.filepaths import dir_path_exists, guess_socket_location
9394
from mycli.packages.hybrid_redirection import get_redirect_components, is_redirect_command
@@ -98,16 +99,12 @@
9899
from mycli.packages.special.main import ArgType
99100
from mycli.packages.special.utils import format_uptime, get_ssl_version, get_uptime, get_warning_count
100101
from mycli.packages.sqlresult import SQLResult
102+
from mycli.packages.ssh_utils import read_ssh_config
101103
from mycli.packages.string_utils import sanitize_terminal_title
102104
from mycli.packages.tabular_output import sql_format
103105
from mycli.sqlcompleter import SQLCompleter
104106
from mycli.sqlexecute import FIELD_TYPES, SQLExecute
105107

106-
try:
107-
import paramiko
108-
except ImportError:
109-
from mycli.packages.paramiko_stub import paramiko # type: ignore[no-redef]
110-
111108
sqlparse.engine.grouping.MAX_GROUPING_DEPTH = None # type: ignore[assignment]
112109
sqlparse.engine.grouping.MAX_GROUPING_TOKENS = None # type: ignore[assignment]
113110

@@ -2316,19 +2313,7 @@ def get_password_from_file(password_file: str | None) -> str | None:
23162313
sys.exit(main_list_dsn(mycli, cli_args))
23172314

23182315
if cli_args.list_ssh_config:
2319-
ssh_config = read_ssh_config(cli_args.ssh_config_path)
2320-
try:
2321-
host_entries = ssh_config.get_hostnames()
2322-
except KeyError:
2323-
click.secho('Error reading ssh config', err=True, fg="red")
2324-
sys.exit(1)
2325-
for host_entry in host_entries:
2326-
if cli_args.verbose:
2327-
host_config = ssh_config.lookup(host_entry)
2328-
click.secho(f"{host_entry} : {host_config.get('hostname')}")
2329-
else:
2330-
click.secho(host_entry)
2331-
sys.exit(0)
2316+
sys.exit(main_list_ssh_config(mycli, cli_args))
23322317

23332318
if 'MYSQL_UNIX_PORT' in os.environ:
23342319
# deprecated 2026-03
@@ -2761,24 +2746,6 @@ def edit_and_execute(event: KeyPressEvent) -> None:
27612746
buff.open_in_editor(validate_and_handle=False)
27622747

27632748

2764-
def read_ssh_config(ssh_config_path: str):
2765-
ssh_config = paramiko.config.SSHConfig()
2766-
try:
2767-
with open(ssh_config_path) as f:
2768-
ssh_config.parse(f)
2769-
except FileNotFoundError as e:
2770-
click.secho(str(e), err=True, fg="red")
2771-
sys.exit(1)
2772-
# Paramiko prior to version 2.7 raises Exception on parse errors.
2773-
# In 2.7 it has become paramiko.ssh_exception.SSHException,
2774-
# but let's catch everything for compatibility
2775-
except Exception as err:
2776-
click.secho(f"Could not parse SSH configuration file {ssh_config_path}:\n{err} ", err=True, fg="red")
2777-
sys.exit(1)
2778-
else:
2779-
return ssh_config
2780-
2781-
27822749
def filtered_sys_argv() -> list[str]:
27832750
args = sys.argv[1:]
27842751
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)