Skip to content

Commit a45acf5

Browse files
committed
Merge remote-tracking branch 'origin' into default-socket-location
2 parents df44b06 + 84dcba2 commit a45acf5

File tree

10 files changed

+154
-22
lines changed

10 files changed

+154
-22
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
A command line client for MySQL that can do auto-completion and syntax highlighting.
88

9-
HomePage: [http://mycli.net](http://mycli.net)
9+
HomePage: [http://mycli.net](http://mycli.net)
1010
Documentation: [http://mycli.net/docs](http://mycli.net/docs)
1111

1212
![Completion](screenshots/tables.png)
@@ -63,6 +63,8 @@ $ sudo apt-get install mycli # Only on debian or ubuntu
6363
--ssh-password TEXT Password to connect to ssh server.
6464
--ssh-key-filename TEXT Private key filename (identify file) for the
6565
ssh connection.
66+
--ssh-config-path TEXT Path to ssh configuation.
67+
--ssh-config-host TEXT Host for ssh server in ssh configuations (requires paramiko).
6668
--ssl-ca PATH CA file in PEM format.
6769
--ssl-capath TEXT CA directory.
6870
--ssl-cert PATH X509 cert in PEM format.
@@ -78,6 +80,7 @@ $ sudo apt-get install mycli # Only on debian or ubuntu
7880
section of myclirc file.
7981
--list-dsn list of DSN configured into the [alias_dsn]
8082
section of myclirc file.
83+
--list-ssh-config list ssh configurations in the ssh config (requires paramiko).
8184
-R, --prompt TEXT Prompt format (Default: "\t \u@\h:\d> ").
8285
-l, --logfile FILENAME Log every query and its results to a file.
8386
--defaults-group-suffix TEXT Read MySQL config groups with the specified

changelog.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1+
TBD
2+
===
3+
4+
Features:
5+
---------
6+
* Add an option `--ssh-config-host` to read ssh configuration from OpenSSH configuration file.
7+
* Add an option `--list-ssh-config` to list ssh configurations.
8+
* Add an option `--ssh-config-path` to choose ssh configuration path.
9+
10+
111
1.21.1
212
======
313

14+
415
Bug Fixes:
516
----------
617

mycli/AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Contributors:
6868
* Mike Palandra
6969
* Georgy Frolov
7070
* Jonathan Lloyd
71+
* Nathan Huang
7172
* Jakub Boukal
7273
* Takeshi D. Itoh
7374
* laixintao

mycli/packages/tabular_output/sql_format.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99
preprocessors = ()
1010

1111

12+
def escape_for_sql_statement(value):
13+
if isinstance(value, bytes):
14+
return f"X'{value.hex()}'"
15+
else:
16+
return formatter.mycli.sqlexecute.conn.escape(value)
17+
18+
1219
def adapter(data, headers, table_format=None, **kwargs):
1320
tables = extract_tables(formatter.query)
1421
if len(tables) > 0:
@@ -19,13 +26,13 @@ def adapter(data, headers, table_format=None, **kwargs):
1926
table_name = table[1]
2027
else:
2128
table_name = "`DUAL`"
22-
escape = formatter.mycli.sqlexecute.conn.escape
2329
if table_format == 'sql-insert':
2430
h = "`, `".join(headers)
2531
yield "INSERT INTO {} (`{}`) VALUES".format(table_name, h)
2632
prefix = " "
2733
for d in data:
28-
values = ", ".join(escape(v) for i, v in enumerate(d))
34+
values = ", ".join(escape_for_sql_statement(v)
35+
for i, v in enumerate(d))
2936
yield "{}({})".format(prefix, values)
3037
if prefix == " ":
3138
prefix = ", "
@@ -39,11 +46,12 @@ def adapter(data, headers, table_format=None, **kwargs):
3946
yield "UPDATE {} SET".format(table_name)
4047
prefix = " "
4148
for i, v in enumerate(d[keys:], keys):
42-
yield "{}`{}` = {}".format(prefix, headers[i], escape(v))
49+
yield "{}`{}` = {}".format(prefix, headers[i], escape_for_sql_statement(v))
4350
if prefix == " ":
4451
prefix = ", "
4552
f = "`{}` = {}"
46-
where = (f.format(headers[i], escape(d[i])) for i in range(keys))
53+
where = (f.format(headers[i], escape_for_sql_statement(
54+
d[i])) for i in range(keys))
4755
yield "WHERE {};".format(" AND ".join(where))
4856

4957

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ autopep8==1.3.3
1111
colorama==0.4.1
1212
git+https://github.com/hayd/pep8radius.git # --error-status option not released
1313
click>=7.0
14+
paramiko==2.7.1

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
install_requirements = [
2020
'click >= 7.0',
2121
'Pygments >= 1.6',
22-
'prompt_toolkit>=2.0.6,<3.0.0',
22+
'prompt_toolkit>=3.0.0,<4.0.0',
2323
'PyMySQL >= 0.9.2',
2424
'sqlparse>=0.3.0,<0.4.0',
2525
'configobj >= 5.0.5',

test/features/environment.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def before_all(context):
1717
os.environ['COLUMNS'] = "100"
1818
os.environ['EDITOR'] = 'ex'
1919
os.environ['LC_ALL'] = 'en_US.utf8'
20+
os.environ['PROMPT_TOOLKIT_NO_CPR'] = '1'
2021

2122
test_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
2223
login_path_file = os.path.join(test_dir, 'mylogin.cnf')

test/test_main.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,24 @@ def test_list_dsn():
283283
assert result.output == "test : mysql://test/test\n"
284284

285285

286+
def test_list_ssh_config():
287+
runner = CliRunner()
288+
with NamedTemporaryFile(mode="w") as ssh_config:
289+
ssh_config.write(dedent("""\
290+
Host test
291+
Hostname test.example.com
292+
User joe
293+
Port 22222
294+
IdentityFile ~/.ssh/gateway
295+
"""))
296+
ssh_config.flush()
297+
args = ['--list-ssh-config', '--ssh-config-path', ssh_config.name]
298+
result = runner.invoke(cli, args=args)
299+
assert "test\n" in result.output
300+
result = runner.invoke(cli, args=args + ['--verbose'])
301+
assert "test : test.example.com\n" in result.output
302+
303+
286304
def test_dsn(monkeypatch):
287305
# Setup classes to mock mycli.main.MyCli
288306
class Formatter:
@@ -395,3 +413,82 @@ def run_query(self, query, new_line=True):
395413
MockMyCli.connect_args["host"] == "dsn_host" and \
396414
MockMyCli.connect_args["port"] == 6 and \
397415
MockMyCli.connect_args["database"] == "dsn_database"
416+
417+
418+
def test_ssh_config(monkeypatch):
419+
# Setup classes to mock mycli.main.MyCli
420+
class Formatter:
421+
format_name = None
422+
423+
class Logger:
424+
def debug(self, *args, **args_dict):
425+
pass
426+
427+
def warning(self, *args, **args_dict):
428+
pass
429+
430+
class MockMyCli:
431+
config = {'alias_dsn': {}}
432+
433+
def __init__(self, **args):
434+
self.logger = Logger()
435+
self.destructive_warning = False
436+
self.formatter = Formatter()
437+
438+
def connect(self, **args):
439+
MockMyCli.connect_args = args
440+
441+
def run_query(self, query, new_line=True):
442+
pass
443+
444+
import mycli.main
445+
monkeypatch.setattr(mycli.main, 'MyCli', MockMyCli)
446+
runner = CliRunner()
447+
448+
# Setup temporary configuration
449+
with NamedTemporaryFile(mode="w") as ssh_config:
450+
ssh_config.write(dedent("""\
451+
Host test
452+
Hostname test.example.com
453+
User joe
454+
Port 22222
455+
IdentityFile ~/.ssh/gateway
456+
"""))
457+
ssh_config.flush()
458+
459+
# When a user supplies a ssh config.
460+
result = runner.invoke(mycli.main.cli, args=[
461+
"--ssh-config-path",
462+
ssh_config.name,
463+
"--ssh-config-host",
464+
"test"
465+
])
466+
assert result.exit_code == 0, result.output + \
467+
" " + str(result.exception)
468+
assert \
469+
MockMyCli.connect_args["ssh_user"] == "joe" and \
470+
MockMyCli.connect_args["ssh_host"] == "test.example.com" and \
471+
MockMyCli.connect_args["ssh_port"] == 22222 and \
472+
MockMyCli.connect_args["ssh_key_filename"] == os.getenv(
473+
"HOME") + "/.ssh/gateway"
474+
475+
# When a user supplies a ssh config host as argument to mycli,
476+
# and used command line arguments, use the command line
477+
# arguments.
478+
result = runner.invoke(mycli.main.cli, args=[
479+
"--ssh-config-path",
480+
ssh_config.name,
481+
"--ssh-config-host",
482+
"test",
483+
"--ssh-user", "arg_user",
484+
"--ssh-host", "arg_host",
485+
"--ssh-port", "3",
486+
"--ssh-key-filename", "/path/to/key"
487+
])
488+
assert result.exit_code == 0, result.output + \
489+
" " + str(result.exception)
490+
assert \
491+
MockMyCli.connect_args["ssh_user"] == "arg_user" and \
492+
MockMyCli.connect_args["ssh_host"] == "arg_host" and \
493+
MockMyCli.connect_args["ssh_port"] == 3 and \
494+
MockMyCli.connect_args["ssh_key_filename"] == "/path/to/key"

test/test_tabular_output.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,21 @@ def mycli():
2323
@dbtest
2424
def test_sql_output(mycli):
2525
"""Test the sql output adapter."""
26-
headers = ['letters', 'number', 'optional', 'float']
26+
headers = ['letters', 'number', 'optional', 'float', 'binary']
2727

2828
class FakeCursor(object):
2929
def __init__(self):
30-
self.data = [('abc', 1, None, 10.0), ('d', 456, '1', 0.5)]
31-
self.description = [(None, FIELD_TYPE.VARCHAR), (None, FIELD_TYPE.LONG),
32-
(None, FIELD_TYPE.LONG), (None, FIELD_TYPE.FLOAT)]
30+
self.data = [
31+
('abc', 1, None, 10.0, b'\xAA'),
32+
('d', 456, '1', 0.5, b'\xAA\xBB')
33+
]
34+
self.description = [
35+
(None, FIELD_TYPE.VARCHAR),
36+
(None, FIELD_TYPE.LONG),
37+
(None, FIELD_TYPE.LONG),
38+
(None, FIELD_TYPE.FLOAT),
39+
(None, FIELD_TYPE.BLOB)
40+
]
3341

3442
def __iter__(self):
3543
return self
@@ -40,8 +48,6 @@ def __next__(self):
4048
else:
4149
raise StopIteration()
4250

43-
next = __next__ # Python 2
44-
4551
def description(self):
4652
return self.description
4753

@@ -55,11 +61,13 @@ def description(self):
5561
`number` = 1
5662
, `optional` = NULL
5763
, `float` = 10
64+
, `binary` = X'aa'
5865
WHERE `letters` = 'abc';
5966
UPDATE `DUAL` SET
6067
`number` = 456
6168
, `optional` = '1'
6269
, `float` = 0.5
70+
, `binary` = X'aabb'
6371
WHERE `letters` = 'd';''')
6472
# Test sql-update-2 output format
6573
assert list(mycli.change_table_format("sql-update-2")) == \
@@ -70,38 +78,40 @@ def description(self):
7078
UPDATE `DUAL` SET
7179
`optional` = NULL
7280
, `float` = 10
81+
, `binary` = X'aa'
7382
WHERE `letters` = 'abc' AND `number` = 1;
7483
UPDATE `DUAL` SET
7584
`optional` = '1'
7685
, `float` = 0.5
86+
, `binary` = X'aabb'
7787
WHERE `letters` = 'd' AND `number` = 456;''')
7888
# Test sql-insert output format (without table name)
7989
assert list(mycli.change_table_format("sql-insert")) == \
8090
[(None, None, None, 'Changed table format to sql-insert')]
8191
mycli.formatter.query = ""
8292
output = mycli.format_output(None, FakeCursor(), headers)
8393
assert "\n".join(output) == dedent('''\
84-
INSERT INTO `DUAL` (`letters`, `number`, `optional`, `float`) VALUES
85-
('abc', 1, NULL, 10)
86-
, ('d', 456, '1', 0.5)
94+
INSERT INTO `DUAL` (`letters`, `number`, `optional`, `float`, `binary`) VALUES
95+
('abc', 1, NULL, 10, X'aa')
96+
, ('d', 456, '1', 0.5, X'aabb')
8797
;''')
8898
# Test sql-insert output format (with table name)
8999
assert list(mycli.change_table_format("sql-insert")) == \
90100
[(None, None, None, 'Changed table format to sql-insert')]
91101
mycli.formatter.query = "SELECT * FROM `table`"
92102
output = mycli.format_output(None, FakeCursor(), headers)
93103
assert "\n".join(output) == dedent('''\
94-
INSERT INTO `table` (`letters`, `number`, `optional`, `float`) VALUES
95-
('abc', 1, NULL, 10)
96-
, ('d', 456, '1', 0.5)
104+
INSERT INTO `table` (`letters`, `number`, `optional`, `float`, `binary`) VALUES
105+
('abc', 1, NULL, 10, X'aa')
106+
, ('d', 456, '1', 0.5, X'aabb')
97107
;''')
98108
# Test sql-insert output format (with database + table name)
99109
assert list(mycli.change_table_format("sql-insert")) == \
100110
[(None, None, None, 'Changed table format to sql-insert')]
101111
mycli.formatter.query = "SELECT * FROM `database`.`table`"
102112
output = mycli.format_output(None, FakeCursor(), headers)
103113
assert "\n".join(output) == dedent('''\
104-
INSERT INTO `database`.`table` (`letters`, `number`, `optional`, `float`) VALUES
105-
('abc', 1, NULL, 10)
106-
, ('d', 456, '1', 0.5)
114+
INSERT INTO `database`.`table` (`letters`, `number`, `optional`, `float`, `binary`) VALUES
115+
('abc', 1, NULL, 10, X'aa')
116+
, ('d', 456, '1', 0.5, X'aabb')
107117
;''')

test/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
PASSWORD = os.getenv('PYTEST_PASSWORD')
1313
USER = os.getenv('PYTEST_USER', 'root')
1414
HOST = os.getenv('PYTEST_HOST', 'localhost')
15-
PORT = os.getenv('PYTEST_PORT', 3306)
15+
PORT = int(os.getenv('PYTEST_PORT', 3306))
1616
CHARSET = os.getenv('PYTEST_CHARSET', 'utf8')
1717
SSH_USER = os.getenv('PYTEST_SSH_USER', None)
1818
SSH_HOST = os.getenv('PYTEST_SSH_HOST', None)

0 commit comments

Comments
 (0)