From 4390e3839d42734ce618609084ac5b69b7f4a7c5 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Sun, 11 Aug 2019 23:56:45 -0500 Subject: [PATCH 001/202] Do not use ConfigObj's list_values. (#764) --- mycli/config.py | 34 ++++++++++++++++++++----- mycli/main.py | 12 +++------ test/test_config.py | 62 ++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 91 insertions(+), 17 deletions(-) diff --git a/mycli/config.py b/mycli/config.py index 43b33394..b1ec4f42 100644 --- a/mycli/config.py +++ b/mycli/config.py @@ -29,14 +29,23 @@ def log(logger, level, message): print(message, file=sys.stderr) -def read_config_file(f): - """Read a config file.""" +def read_config_file(f, list_values=True): + """Read a config file. + + *list_values* set to `True` is the default behavior of ConfigObj. + Disabling it causes values to not be parsed for lists, + (e.g. 'a,b,c' -> ['a', 'b', 'c']. Additionally, the config values are + not unquoted. We are disabling list_values when reading MySQL config files + so we can correctly interpret commas in passwords. + + """ if isinstance(f, basestring): f = os.path.expanduser(f) try: - config = ConfigObj(f, interpolation=False, encoding='utf8') + config = ConfigObj(f, interpolation=False, encoding='utf8', + list_values=list_values) except ConfigObjError as e: log(logger, logging.ERROR, "Unable to parse line {0} of config file " "'{1}'.".format(e.line_number, f)) @@ -50,13 +59,13 @@ def read_config_file(f): return config -def read_config_files(files): +def read_config_files(files, list_values=True): """Read and merge a list of config files.""" - config = ConfigObj() + config = ConfigObj(list_values=list_values) for _file in files: - _config = read_config_file(_file) + _config = read_config_file(_file, list_values=list_values) if bool(_config) is True: config.merge(_config) config.filename = _config.filename @@ -199,6 +208,19 @@ def str_to_bool(s): raise ValueError('not a recognized boolean value: %s'.format(s)) +def strip_matching_quotes(s): + """Remove matching, surrounding quotes from a string. + + This is the same logic that ConfigObj uses when parsing config + values. + + """ + if (isinstance(s, basestring) and len(s) >= 2 and + s[0] == s[-1] and s[0] in ('"', "'")): + s = s[1:-1] + return s + + def _get_decryptor(key): """Get the AES decryptor.""" c = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend()) diff --git a/mycli/main.py b/mycli/main.py index 968a6b0d..29e26c30 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -42,7 +42,8 @@ from .clibuffer import cli_is_multiline from .completion_refresher import CompletionRefresher from .config import (write_default_config, get_mylogin_cnf_path, - open_mylogin_cnf, read_config_files, str_to_bool) + open_mylogin_cnf, read_config_files, str_to_bool, + strip_matching_quotes) from .key_bindings import mycli_bindings from .encodingutils import utf8tounicode, text_type from .lexer import MyCliLexer @@ -308,7 +309,7 @@ def read_my_cnf_files(self, files, keys): :param keys: list of keys to retrieve :returns: tuple, with None for missing keys. """ - cnf = read_config_files(files) + cnf = read_config_files(files, list_values=False) sections = ['client'] if self.login_path and self.login_path != 'client': @@ -321,12 +322,7 @@ def get(key): result = None for sect in cnf: if sect in sections and key in cnf[sect]: - result = cnf[sect][key] - # HACK: if result is a list, then ConfigObj() probably decoded from - # string by splitting on comma, so reconstruct string by joining on - # comma. - if isinstance(result, list): - result = ','.join(result) + result = strip_matching_quotes(cnf[sect][key]) return result return {x: get(x) for x in keys} diff --git a/test/test_config.py b/test/test_config.py index 81a9ee4f..7f2b2442 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -1,5 +1,5 @@ """Unit tests for the mycli.config module.""" -from io import BytesIO, TextIOWrapper +from io import BytesIO, StringIO, TextIOWrapper import os import struct import sys @@ -7,7 +7,8 @@ import pytest from mycli.config import (get_mylogin_cnf_path, open_mylogin_cnf, - read_and_decrypt_mylogin_cnf, str_to_bool) + read_and_decrypt_mylogin_cnf, read_config_file, + str_to_bool, strip_matching_quotes) LOGIN_PATH_FILE = os.path.abspath(os.path.join(os.path.dirname(__file__), 'mylogin.cnf')) @@ -20,7 +21,6 @@ def open_bmylogin_cnf(name): buf.write(f.read()) return buf - def test_read_mylogin_cnf(): """Tests that a login path file can be read and decrypted.""" mylogin_cnf = open_mylogin_cnf(LOGIN_PATH_FILE) @@ -138,3 +138,59 @@ def test_str_to_bool(): with pytest.raises(TypeError): str_to_bool(None) + + +def test_read_config_file_list_values_default(): + """Test that reading a config file uses list_values by default.""" + + f = StringIO(u"[main]\nweather='cloudy with a chance of meatballs'\n") + config = read_config_file(f) + + assert config['main']['weather'] == u"cloudy with a chance of meatballs" + + +def test_read_config_file_list_values_off(): + """Test that you can disable list_values when reading a config file.""" + + f = StringIO(u"[main]\nweather='cloudy with a chance of meatballs'\n") + config = read_config_file(f, list_values=False) + + assert config['main']['weather'] == u"'cloudy with a chance of meatballs'" + + +def test_strip_quotes_with_matching_quotes(): + """Test that a string with matching quotes is unquoted.""" + + s = "May the force be with you." + assert s == strip_matching_quotes('"{}"'.format(s)) + assert s == strip_matching_quotes("'{}'".format(s)) + + +def test_strip_quotes_with_unmatching_quotes(): + """Test that a string with unmatching quotes is not unquoted.""" + + s = "May the force be with you." + assert '"' + s == strip_matching_quotes('"{}'.format(s)) + assert s + "'" == strip_matching_quotes("{}'".format(s)) + + +def test_strip_quotes_with_empty_string(): + """Test that an empty string is handled during unquoting.""" + + assert '' == strip_matching_quotes('') + + +def test_strip_quotes_with_none(): + """Test that None is handled during unquoting.""" + + assert None is strip_matching_quotes(None) + + +def test_strip_quotes_with_quotes(): + """Test that strings with quotes in them are handled during unquoting.""" + + s1 = 'Darth Vader said, "Luke, I am your father."' + assert s1 == strip_matching_quotes(s1) + + s2 = '"Darth Vader said, "Luke, I am your father.""' + assert s2[1:-1] == strip_matching_quotes(s2) From e429a3165357f738962485d230cf93abf8a324ea Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Mon, 12 Aug 2019 14:25:29 -0500 Subject: [PATCH 002/202] Prep for v1.20.0 release. (#765) --- changelog.md | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/changelog.md b/changelog.md index 039aa301..f3b3f502 100644 --- a/changelog.md +++ b/changelog.md @@ -1,11 +1,12 @@ -TBD -==== +1.20.0 +====== Features: ---------- * Auto find alias dsn when `://` not in `database` (Thanks: [QiaoHou Peng]). * Mention URL encoding as escaping technique for special characters in connection DSN (Thanks: [Aljosha Papsch]). * Pressing Alt-Enter will introduce a line break. This is a way to break up the query into multiple lines without switching to multi-line mode. (Thanks: [Amjith Ramanujam]). +* Use a generator to stream the output to the pager (Thanks: [Dick Marinus]). Bug Fixes: ---------- @@ -17,16 +18,10 @@ Bug Fixes: * Update `setup.py` to no longer require `sqlparse` to be less than 0.3.0 as that just came out and there are no notable changes. ([VVelox]) * workaround for ConfigObj parsing strings containing "," as lists (Thanks: [Mike Palandra]) -Features: ---------- - -* Use a generator to stream the output to the pager (Thanks: [Dick Marinus]). - Internal: --------- * fix unhashable FormattedText from prompt toolkit in unit tests (Thanks: [Dick Marinus]). - 1.19.0 ====== From 36916e8115050853a35969b478fe13512d5c373b Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Mon, 12 Aug 2019 17:18:13 -0500 Subject: [PATCH 003/202] Release version 1.20.0. --- mycli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/__init__.py b/mycli/__init__.py index 8ac48f09..bec07380 100644 --- a/mycli/__init__.py +++ b/mycli/__init__.py @@ -1 +1 @@ -__version__ = '1.19.0' +__version__ = '1.20.0' From fe65b642d0e0d1d069953ce6607b6bdd796556ca Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Tue, 20 Aug 2019 07:49:21 -0500 Subject: [PATCH 004/202] Do not override login path with DSN. --- mycli/main.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index 29e26c30..9570bb2d 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1085,21 +1085,22 @@ def cli(database, user, host, port, socket, password, dbname, dsn_uri = None - if database and '://' not in database and not any([user, password, host, port]): - dsn = database - database = '' + # Treat the database argument as a DSN alias if we're missing + # other connection information. + if (mycli.config['alias_dsn'] and database and '://' not in database + and not any([user, password, host, port, login_path])): + dsn, database = database, '' if database and '://' in database: - dsn_uri = database - database = '' + dsn_uri, database = database, '' - if dsn is not '': + if dsn: try: dsn_uri = mycli.config['alias_dsn'][dsn] - except KeyError as err: - click.secho('Invalid DSNs found in the config file. ' - 'Please check the "[alias_dsn]" section in myclirc.', - err=True, fg='red') + except KeyError: + click.secho('Could not find the specified DSN in the config file. ' + 'Please check the "[alias_dsn]" section in your ' + 'myclirc.', err=True, fg='red') exit(1) if dsn_uri: From ffe148adbc53315be3d89e0cbfe804157a6d3961 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Tue, 20 Aug 2019 07:51:17 -0500 Subject: [PATCH 005/202] Add login path fix to changelog. --- changelog.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/changelog.md b/changelog.md index f3b3f502..43ac28dc 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,11 @@ +1.20.1 +====== + +Bug Fixes: +---------- + +* Fix an error when using login paths with an explicit database name (Thanks: [Thomas Roten]). + 1.20.0 ====== From 160533e8cce47cf9d965b7b860fe22291b9a16e5 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Wed, 21 Aug 2019 07:02:16 -0500 Subject: [PATCH 006/202] Releasing version 1.20.1. --- mycli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/__init__.py b/mycli/__init__.py index bec07380..d1f2def7 100644 --- a/mycli/__init__.py +++ b/mycli/__init__.py @@ -1 +1 @@ -__version__ = '1.20.0' +__version__ = '1.20.1' From b21c9cebe8d73d99abb7aa1e738aaa54cd415f21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D0=BE=D1=80=D0=B3=D0=B8=D0=B9=20=D0=A4=D1=80?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D0=B2?= Date: Mon, 30 Sep 2019 10:13:51 +0300 Subject: [PATCH 007/202] Added an option to include the DSN alias name into the prompt --- mycli/main.py | 4 ++++ mycli/myclirc | 1 + 2 files changed, 5 insertions(+) diff --git a/mycli/main.py b/mycli/main.py index 9570bb2d..f09b2753 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -121,6 +121,7 @@ def __init__(self, sqlexecute=None, prompt=None, special.set_favorite_queries(self.config) + self.dsn_alias = None self.formatter = TabularOutputFormatter( format_name=c['main']['table_format']) sql_format.register_new_formatter(self.formatter) @@ -885,6 +886,7 @@ def get_prompt(self, string): string = string.replace('\\r', now.strftime('%I')) string = string.replace('\\s', now.strftime('%S')) string = string.replace('\\p', str(sqlexecute.port)) + string = string.replace('\\A', self.dsn_alias or '(none)') string = string.replace('\\_', ' ') return string @@ -1102,6 +1104,8 @@ def cli(database, user, host, port, socket, password, dbname, 'Please check the "[alias_dsn]" section in your ' 'myclirc.', err=True, fg='red') exit(1) + else: + mycli.dsn_alias = dsn if dsn_uri: uri = urlparse(dsn_uri) diff --git a/mycli/myclirc b/mycli/myclirc index 571ffb08..31270d9d 100644 --- a/mycli/myclirc +++ b/mycli/myclirc @@ -63,6 +63,7 @@ wider_completion_menu = False # \r - The current time, standard 12-hour time (1–12) # \s - Seconds of the current time # \t - Product type (Percona, MySQL, MariaDB) +# \A - DSN alias name (from the [alias_dsn] section) # \u - Username prompt = '\t \u@\h:\d> ' prompt_continuation = '-> ' From e2001c7c9cc90789d1b90efbf599a257f7476920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D0=BE=D1=80=D0=B3=D0=B8=D0=B9=20=D0=A4=D1=80?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D0=B2?= Date: Mon, 30 Sep 2019 10:25:41 +0300 Subject: [PATCH 008/202] updated changelog --- changelog.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/changelog.md b/changelog.md index 43ac28dc..e479eb76 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,11 @@ +TBD +=== + +Features: +--------- +* Added DSN alias name as a format specifier to the prompt (Thanks: [Georgy Frolov]). + + 1.20.1 ====== @@ -6,6 +14,7 @@ Bug Fixes: * Fix an error when using login paths with an explicit database name (Thanks: [Thomas Roten]). + 1.20.0 ====== From 1764bb3905685ede896b83305036ff6f93d510ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D0=BE=D1=80=D0=B3=D0=B8=D0=B9=20=D0=A4=D1=80?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D0=B2?= Date: Mon, 30 Sep 2019 10:26:43 +0300 Subject: [PATCH 009/202] updated AUTHORS --- mycli/AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/mycli/AUTHORS b/mycli/AUTHORS index ae1fb08a..2e28ccad 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -66,6 +66,7 @@ Contributors: * Aljosha Papsch * Zane C. Bowers-Hadley * Mike Palandra + * Georgy Frolov Creator: -------- From cbd528a83c22b90cc06b16960d011103e197e207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D0=BE=D1=80=D0=B3=D0=B8=D0=B9=20=D0=A4=D1=80?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D0=B2?= Date: Mon, 30 Sep 2019 17:51:52 +0300 Subject: [PATCH 010/202] added .vscode to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e1d59532..b13429e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea/ +.vscode/ /build /dist /mycli.egg-info From 48484ece134b86ac22191819c58897c9aebdfbe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D0=BE=D1=80=D0=B3=D0=B8=D0=B9=20=D0=A4=D1=80?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D0=B2?= Date: Wed, 2 Oct 2019 17:18:51 +0300 Subject: [PATCH 011/202] retain ANSI escapes on the first line of output --- mycli/main.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index 9570bb2d..ede65a18 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -936,15 +936,15 @@ def get_col_type(col): formatted = formatted.splitlines() formatted = iter(formatted) - first_line = strip_ansi(next(formatted)) - formatted = itertools.chain([first_line], formatted) - - if (not expanded and max_width and headers and cur and - len(first_line) > max_width): - formatted = self.formatter.format_output( - cur, headers, format_name='vertical', column_types=column_types, **output_kwargs) - if isinstance(formatted, (text_type)): - formatted = iter(formatted.splitlines()) + if (not expanded and max_width and headers and cur): + first_line = next(formatted) + if len(strip_ansi(first_line)) > max_width: + formatted = self.formatter.format_output( + cur, headers, format_name='vertical', column_types=column_types, **output_kwargs) + if isinstance(formatted, (text_type)): + formatted = iter(formatted.splitlines()) + else: + formatted = itertools.chain([first_line], formatted) output = itertools.chain(output, formatted) From 8ac245d074d11de3080aeb82a2fcb07bcad76c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B5=D0=BE=D1=80=D0=B3=D0=B8=D0=B9=20=D0=A4=D1=80?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D0=B2?= Date: Mon, 30 Sep 2019 17:56:20 +0300 Subject: [PATCH 012/202] added RENAME TABLE to the lists of mutating and destructive queries --- mycli/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index 9570bb2d..27a6e0cf 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -631,7 +631,7 @@ def one_iteration(text=None): start = time() result_count += 1 - mutating = mutating or is_mutating(status) + mutating = mutating or destroy or is_mutating(status) special.unset_once_if_written() except EOFError as e: raise e @@ -1204,7 +1204,7 @@ def need_completion_refresh(queries): try: first_token = query.split()[0] if first_token.lower() in ('alter', 'create', 'use', '\\r', - '\\u', 'connect', 'drop'): + '\\u', 'connect', 'drop', 'rename'): return True except Exception: return False @@ -1253,7 +1253,7 @@ def is_mutating(status): return False mutating = set(['insert', 'update', 'delete', 'alter', 'create', 'drop', - 'replace', 'truncate', 'load']) + 'replace', 'truncate', 'load', 'rename']) return status.split(None, 1)[0].lower() in mutating def is_select(status): From ffc6c41a0eae58095ea3713c1d9363a8d1c3356a Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sat, 5 Oct 2019 00:13:59 +0300 Subject: [PATCH 013/202] updated click version in requirements.dev --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 5cbc1577..1c1333d2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,4 +9,4 @@ coverage==4.3.4 codecov==2.0.9 autopep8==1.3.3 git+https://github.com/hayd/pep8radius.git # --error-status option not released -click==6.7 +click>=7.0 From 4be17eb8887d4eaaca4376d78d1fbac025c1592c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20W=C3=BCnschel?= Date: Wed, 9 Oct 2019 23:09:31 +0200 Subject: [PATCH 014/202] parseutils: Consider update-statement without where-clause as destructive --- changelog.md | 1 + mycli/packages/parseutils.py | 20 +++++++++++++++++++- test/test_parseutils.py | 31 ++++++++++++++++++++++++++++++- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index e479eb76..167ea93f 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,7 @@ TBD Features: --------- * Added DSN alias name as a format specifier to the prompt (Thanks: [Georgy Frolov]). +* Mark `update` without `where`-clause as destructive query (Thanks: [Klaus Wünschel]). 1.20.1 diff --git a/mycli/packages/parseutils.py b/mycli/packages/parseutils.py index 3e0f2e70..8a39fc67 100644 --- a/mycli/packages/parseutils.py +++ b/mycli/packages/parseutils.py @@ -203,10 +203,28 @@ def queries_start_with(queries, prefixes): return False +def query_has_where_clause(query): + """Check if the query contains a where-clause.""" + return any( + isinstance(token, sqlparse.sql.Where) + for token_list in sqlparse.parse(query) + for token in token_list + ) + + def is_destructive(queries): """Returns if any of the queries in *queries* is destructive.""" keywords = ('drop', 'shutdown', 'delete', 'truncate', 'alter') - return queries_start_with(queries, keywords) + for query in sqlparse.split(queries): + if query: + if query_starts_with(query, keywords) is True: + return True + elif query_starts_with( + query, ['update'] + ) is True and not query_has_where_clause(query): + return True + + return False def is_open_quote(sql): diff --git a/test/test_parseutils.py b/test/test_parseutils.py index f11dcdb4..1cca9145 100644 --- a/test/test_parseutils.py +++ b/test/test_parseutils.py @@ -1,6 +1,6 @@ import pytest from mycli.packages.parseutils import ( - extract_tables, query_starts_with, queries_start_with, is_destructive + extract_tables, query_starts_with, queries_start_with, is_destructive, query_has_where_clause ) @@ -135,3 +135,32 @@ def test_is_destructive(): 'drop database foo;' ) assert is_destructive(sql) is True + + +def test_is_destructive_update_with_where_clause(): + sql = ( + 'use test;\n' + 'show databases;\n' + 'UPDATE test SET x = 1 WHERE id = 1;' + ) + assert is_destructive(sql) is False + + +def test_is_destructive_update_without_where_clause(): + sql = ( + 'use test;\n' + 'show databases;\n' + 'UPDATE test SET x = 1;' + ) + assert is_destructive(sql) is True + + +@pytest.mark.parametrize( + ('sql', 'has_where_clause'), + [ + ('update test set dummy = 1;', False), + ('update test set dummy = 1 where id = 1);', True), + ], +) +def test_query_has_where_clause(sql, has_where_clause): + assert query_has_where_clause(sql) is has_where_clause From 5bce6d1b806d937f5db10bb8a9e455f4aa0b426f Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sat, 5 Oct 2019 03:04:08 +0300 Subject: [PATCH 015/202] implemented the DELIMITER command --- changelog.md | 3 + mycli/clibuffer.py | 36 +++++++--- mycli/clitoolbar.py | 8 ++- mycli/packages/special/__init__.py | 2 + mycli/packages/special/delimitercommand.py | 83 ++++++++++++++++++++++ mycli/packages/special/favoritequeries.py | 2 +- mycli/packages/special/iocommands.py | 8 +++ mycli/sqlcompleter.py | 2 +- mycli/sqlexecute.py | 8 +-- test/features/iocommands.feature | 21 +++++- test/features/steps/crud_database.py | 2 +- test/features/steps/iocommands.py | 56 ++++++++++----- test/test_parseutils.py | 2 +- test/test_special_iocommands.py | 43 +++++++++++ 14 files changed, 239 insertions(+), 37 deletions(-) create mode 100644 mycli/packages/special/delimitercommand.py diff --git a/changelog.md b/changelog.md index e479eb76..97e07509 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,8 @@ Features: --------- * Added DSN alias name as a format specifier to the prompt (Thanks: [Georgy Frolov]). +* Added DELIMITER command (Thanks: [Georgy Frolov]) + 1.20.1 ====== @@ -723,3 +725,4 @@ Bug Fixes: [Dick Marinus]: https://github.com/meeuw [François Pietka]: https://github.com/fpietka [Frederic Aoustin]: https://github.com/fraoustin +[Georgy Frolov]: https://github.com/pasenor \ No newline at end of file diff --git a/mycli/clibuffer.py b/mycli/clibuffer.py index f6cc737a..d3f1af4b 100644 --- a/mycli/clibuffer.py +++ b/mycli/clibuffer.py @@ -4,6 +4,7 @@ from prompt_toolkit.filters import Condition from prompt_toolkit.application import get_app from .packages.parseutils import is_open_quote +from .packages import special def cli_is_multiline(mycli): @@ -17,6 +18,7 @@ def cond(): return not _multiline_exception(doc.text) return cond + def _multiline_exception(text): orig = text text = text.strip() @@ -27,12 +29,28 @@ def _multiline_exception(text): if text.startswith('\\fs'): return orig.endswith('\n') - return (text.startswith('\\') or # Special Command - text.endswith(';') or # Ended with a semi-colon - text.endswith('\\g') or # Ended with \g - text.endswith('\\G') or # Ended with \G - (text == 'exit') or # Exit doesn't need semi-colon - (text == 'quit') or # Quit doesn't need semi-colon - (text == ':q') or # To all the vim fans out there - (text == '') # Just a plain enter without any text - ) + return ( + # Special Command + text.startswith('\\') or + + # Delimiter declaration + text.lower().startswith('delimiter') or + + # Ended with the current delimiter (usually a semi-column) + text.endswith(special.delimiter.current) or + + text.endswith('\\g') or + text.endswith('\\G') or + + # Exit doesn't need semi-column` + (text == 'exit') or + + # Quit doesn't need semi-column + (text == 'quit') or + + # To all teh vim fans out there + (text == ':q') or + + # just a plain enter without any text + (text == '') + ) diff --git a/mycli/clitoolbar.py b/mycli/clitoolbar.py index 89e6afa0..03e39cf2 100644 --- a/mycli/clitoolbar.py +++ b/mycli/clitoolbar.py @@ -3,6 +3,7 @@ from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.application import get_app from prompt_toolkit.enums import EditingMode +from .packages import special def create_toolbar_tokens_func(mycli, show_fish_help): @@ -12,8 +13,13 @@ def get_toolbar_tokens(): result.append(('class:bottom-toolbar', ' ')) if mycli.multi_line: + delimiter = special.delimiter.current result.append( - ('class:bottom-toolbar', ' (Semi-colon [;] will end the line) ')) + ( + 'class:bottom-toolbar', + ' ({} [{}] will end the line) '.format( + 'Semi-colon' if delimiter == ';' else 'Delimiter', delimiter) + )) if mycli.multi_line: result.append(('class:bottom-toolbar.on', '[F3] Multiline: ON ')) diff --git a/mycli/packages/special/__init__.py b/mycli/packages/special/__init__.py index 92bcca6d..f4d105e0 100644 --- a/mycli/packages/special/__init__.py +++ b/mycli/packages/special/__init__.py @@ -8,3 +8,5 @@ def export(defn): from . import dbcommands from . import iocommands + +delimiter = iocommands.delimiter_command diff --git a/mycli/packages/special/delimitercommand.py b/mycli/packages/special/delimitercommand.py new file mode 100644 index 00000000..a7dc7979 --- /dev/null +++ b/mycli/packages/special/delimitercommand.py @@ -0,0 +1,83 @@ +# coding: utf-8 +from __future__ import unicode_literals + +import re +import sqlparse + + +class DelimiterCommand(object): + def __init__(self): + self._delimiter = ';' + + def _split(self, sql): + """Temporary workaround until sqlparse.split() learns about custom + delimiters.""" + + placeholder = "\ufffc" # unicode object replacement character + + if self._delimiter == ';': + return sqlparse.split(sql) + + # We must find a string that original sql does not contain. + # Most likely, our placeholder is enough, but if not, keep looking + while placeholder in sql: + placeholder += placeholder[0] + sql = sql.replace(';', placeholder) + sql = sql.replace(self._delimiter, ';') + + split = sqlparse.split(sql) + + return [ + stmt.replace(';', self._delimiter).replace(placeholder, ';') + for stmt in split + ] + + def queries_iter(self, input): + """Iterate over queries in the input string.""" + + queries = self._split(input) + while queries: + for sql in queries: + delimiter = self._delimiter + sql = queries.pop(0) + if sql.endswith(delimiter): + trailing_delimiter = True + sql = sql.strip(delimiter) + else: + trailing_delimiter = False + + yield sql + + # if the delimiter was changed by the last command, + # re-split everything, and if we previously stripped + # the delimiter, append it to the end + if self._delimiter != delimiter: + combined_statement = ' '.join([sql] + queries) + if trailing_delimiter: + combined_statement += delimiter + queries = self._split(combined_statement)[1:] + + def set(self, arg, **_): + """Change delimiter. + + Since `arg` is everything that follows the DELIMITER token + after sqlparse (it may include other statements separated by + the new delimiter), we want to set the delimiter to the first + word of it. + + """ + match = arg and re.search(r'[^\s]+', arg) + if not match: + message = 'Missing required argument, delimiter' + return [(None, None, None, message)] + + delimiter = match.group() + if delimiter.lower() == 'delimiter': + return [(None, None, None, 'Invalid delimiter "delimiter"')] + + self._delimiter = delimiter + return [(None, None, None, "Changed delimiter to {}".format(delimiter))] + + @property + def current(self): + return self._delimiter diff --git a/mycli/packages/special/favoritequeries.py b/mycli/packages/special/favoritequeries.py index ed47127f..55dd8cad 100644 --- a/mycli/packages/special/favoritequeries.py +++ b/mycli/packages/special/favoritequeries.py @@ -26,7 +26,7 @@ class FavoriteQueries(object): ╒════════╤════════╕ │ a │ b │ ╞════════╪════════╡ - │ 日本語 │ 日本語 │ + │ 日本語 │ 日本語 │ ╘════════╧════════╛ # Delete a favorite query. diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index 166e457c..84c668eb 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -15,6 +15,7 @@ from . import export from .main import special_command, NO_QUERY, PARSED_QUERY from .favoritequeries import FavoriteQueries +from .delimitercommand import DelimiterCommand from .utils import handle_cd_command from mycli.packages.prompt_utils import confirm_destructive_query @@ -24,6 +25,8 @@ tee_file = None once_file = written_to_once_file = None favoritequeries = FavoriteQueries(ConfigObj()) +delimiter_command = DelimiterCommand() + @export def set_timing_enabled(val): @@ -437,3 +440,8 @@ def watch_query(arg, **kwargs): return finally: set_pager_enabled(old_pager_enabled) + + +@special_command('delimiter', None, 'Change SQL delimiter.') +def set_delimiter(arg, **_): + return delimiter_command.set(arg) diff --git a/mycli/sqlcompleter.py b/mycli/sqlcompleter.py index 1e11c9c3..2f932338 100644 --- a/mycli/sqlcompleter.py +++ b/mycli/sqlcompleter.py @@ -21,7 +21,7 @@ class SQLCompleter(Completer): 'CHARACTER SET', 'CHECK', 'COLLATE', 'COLUMN', 'COMMENT', 'COMMIT', 'CONSTRAINT', 'CREATE', 'CURRENT', 'CURRENT_TIMESTAMP', 'DATABASE', 'DATE', 'DECIMAL', 'DEFAULT', - 'DELETE FROM', 'DELIMITER', 'DESC', 'DESCRIBE', 'DROP', + 'DELETE FROM', 'DESC', 'DESCRIBE', 'DROP', 'ELSE', 'END', 'ENGINE', 'ESCAPE', 'EXISTS', 'FILE', 'FLOAT', 'FOR', 'FOREIGN KEY', 'FORMAT', 'FROM', 'FULL', 'FUNCTION', 'GRANT', 'GROUP BY', 'HAVING', 'HOST', 'IDENTIFIED', 'IN', diff --git a/mycli/sqlexecute.py b/mycli/sqlexecute.py index 61ba6848..fa79af9c 100644 --- a/mycli/sqlexecute.py +++ b/mycli/sqlexecute.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import logging import pymysql import sqlparse @@ -166,12 +168,9 @@ def run(self, statement): if statement.startswith('\\fs'): components = [statement] else: - components = sqlparse.split(statement) + components = special.delimiter.queries_iter(statement) for sql in components: - # Remove spaces, eol and semi-colons. - sql = sql.rstrip(';') - # \G is treated specially since we have to set the expanded output. if sql.endswith('\\G'): special.set_expanded_output(True) @@ -194,6 +193,7 @@ def run(self, statement): if not cur.nextset() or (not cur.rowcount and cur.description is None): break + def get_result(self, cursor): """Get the current result's data from the cursor.""" title = headers = None diff --git a/test/features/iocommands.feature b/test/features/iocommands.feature index 38efbbb0..246a44a3 100644 --- a/test/features/iocommands.feature +++ b/test/features/iocommands.feature @@ -2,16 +2,31 @@ Feature: I/O commands Scenario: edit sql in file with external editor When we start external editor providing a file name - and we type sql in the editor + and we type "select * from abc" in the editor and we exit the editor then we see dbcli prompt - and we see the sql in prompt + and we see "select * from abc" in prompt Scenario: tee output from query When we tee output and we wait for prompt - and we query "select 123456" + and we select "select 123456" and we wait for prompt and we notee output and we wait for prompt then we see 123456 in tee output + + Scenario: set delimiter + When we query "delimiter $" + then delimiter is set to "$" + + Scenario: set delimiter twice + When we query "delimiter $" + and we query "delimiter ]]" + then delimiter is set to "]]" + + Scenario: set delimiter and query on same line + When we query "select 123; delimiter $ select 456 $ delimiter %" + then we see result "123" + and we see result "456" + and delimiter is set to "%" diff --git a/test/features/steps/crud_database.py b/test/features/steps/crud_database.py index 046e829d..ef7f3f62 100644 --- a/test/features/steps/crud_database.py +++ b/test/features/steps/crud_database.py @@ -36,7 +36,7 @@ def step_db_connect_test(context): """Send connect to database.""" db_name = context.conf['dbname'] context.currentdb = db_name - context.cli.sendline('use {0}'.format(db_name)) + context.cli.sendline('use {0};'.format(db_name)) @when('we connect to tmp database') diff --git a/test/features/steps/iocommands.py b/test/features/steps/iocommands.py index 206ca802..2f3efb31 100644 --- a/test/features/steps/iocommands.py +++ b/test/features/steps/iocommands.py @@ -21,10 +21,10 @@ def step_edit_file(context): wrappers.expect_exact(context, '\r\n:', timeout=2) -@when('we type sql in the editor') -def step_edit_type_sql(context): +@when('we type "{query}" in the editor') +def step_edit_type_sql(context, query): context.cli.sendline('i') - context.cli.sendline('select * from abc') + context.cli.sendline(query) context.cli.sendline('.') wrappers.expect_exact(context, '\r\n:', timeout=2) @@ -35,9 +35,9 @@ def step_edit_quit(context): wrappers.expect_exact(context, "written", timeout=2) -@then('we see the sql in prompt') -def step_edit_done_sql(context): - for match in 'select * from abc'.split(' '): +@then('we see "{query}" in prompt') +def step_edit_done_sql(context, query): + for match in query.split(' '): wrappers.expect_exact(context, match, timeout=5) # Cleanup the command line. context.cli.sendcontrol('c') @@ -56,20 +56,35 @@ def step_tee_ouptut(context): os.path.basename(context.tee_file_name))) -@when(u'we query "select 123456"') -def step_query_select_123456(context): - context.cli.sendline('select 123456') - wrappers.expect_pager(context, dedent("""\ - +--------+\r - | 123456 |\r - +--------+\r - | 123456 |\r - +--------+\r +@when(u'we select "select {param}"') +def step_query_select_number(context, param): + context.cli.sendline(u'select {}'.format(param)) + wrappers.expect_pager(context, dedent(u"""\ + +{dashes}+\r + | {param} |\r + +{dashes}+\r + | {param} |\r + +{dashes}+\r \r - """), timeout=5) + """.format(param=param, dashes='-' * (len(param) + 2)) + ), timeout=5) wrappers.expect_exact(context, '1 row in set', timeout=2) +@then(u'we see result "{result}"') +def step_see_result(context, result): + wrappers.expect_exact( + context, + u"| {} |".format(result), + timeout=2 + ) + + +@when(u'we query "{query}"') +def step_query(context, query): + context.cli.sendline(query) + + @when(u'we notee output') def step_notee_output(context): context.cli.sendline('notee') @@ -81,3 +96,12 @@ def step_see_123456_in_ouput(context): assert '123456' in f.read() if os.path.exists(context.tee_file_name): os.remove(context.tee_file_name) + + +@then(u'delimiter is set to "{delimiter}"') +def delimiter_is_set(context, delimiter): + wrappers.expect_exact( + context, + u'Changed delimiter to {}'.format(delimiter), + timeout=2 + ) diff --git a/test/test_parseutils.py b/test/test_parseutils.py index f11dcdb4..9fd5daf1 100644 --- a/test/test_parseutils.py +++ b/test/test_parseutils.py @@ -1,6 +1,6 @@ import pytest from mycli.packages.parseutils import ( - extract_tables, query_starts_with, queries_start_with, is_destructive + extract_tables, query_starts_with, queries_start_with, is_destructive, ) diff --git a/test/test_special_iocommands.py b/test/test_special_iocommands.py index c7f802b1..7ca33839 100644 --- a/test/test_special_iocommands.py +++ b/test/test_special_iocommands.py @@ -1,4 +1,6 @@ # coding: utf-8 +from __future__ import unicode_literals + import os import stat import tempfile @@ -232,3 +234,44 @@ def test_asserts(gen): cur=cur)) test_asserts(watch_query('-c {0!s} select 1;'.format(seconds), cur=cur)) + + +def test_split_sql_by_delimiter(): + delimiter = mycli.packages.special.delimiter + for delimiter_str in (';', '$', '😀'): + delimiter.set(delimiter_str) + sql_input = "select 1{} select \ufffc2".format(delimiter_str) + queries = ( + "select 1", + "select \ufffc2" + ) + for query, parsed_query in zip( + queries, delimiter.queries_iter(sql_input)): + assert(query == parsed_query) + + +def test_switch_delimiter_within_query(): + delimiter = mycli.packages.special.delimiter + delimiter.set(';') + sql_input = "select 1; delimiter $$ select 2 $$ select 3 $$" + queries = ( + "select 1", + "delimiter $$ select 2 $$ select 3 $$", + "select 2", + "select 3" + ) + for query, parsed_query in zip( + queries, delimiter.queries_iter(sql_input)): + assert(query == parsed_query) + + +def test_set_delimiter(): + delimiter = mycli.packages.special.delimiter + for delim in ('foo', 'bar'): + delimiter.set(delim) + assert delimiter.current == delim + + +def teardown_function(): + delimiter = mycli.packages.special.delimiter + delimiter.set(';') From 1f99087725e5168c18971561e62a4902cb6593fb Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Thu, 10 Oct 2019 12:08:54 +0300 Subject: [PATCH 016/202] Hid delimiter_command variable inside packages.special module --- mycli/clibuffer.py | 2 +- mycli/clitoolbar.py | 2 +- mycli/packages/special/__init__.py | 2 -- mycli/packages/special/iocommands.py | 12 ++++++++++++ mycli/sqlexecute.py | 2 +- test/test_special_iocommands.py | 20 +++++++++----------- 6 files changed, 24 insertions(+), 16 deletions(-) diff --git a/mycli/clibuffer.py b/mycli/clibuffer.py index d3f1af4b..3e720c6d 100644 --- a/mycli/clibuffer.py +++ b/mycli/clibuffer.py @@ -37,7 +37,7 @@ def _multiline_exception(text): text.lower().startswith('delimiter') or # Ended with the current delimiter (usually a semi-column) - text.endswith(special.delimiter.current) or + text.endswith(special.get_current_delimiter()) or text.endswith('\\g') or text.endswith('\\G') or diff --git a/mycli/clitoolbar.py b/mycli/clitoolbar.py index 03e39cf2..cf73b97d 100644 --- a/mycli/clitoolbar.py +++ b/mycli/clitoolbar.py @@ -13,7 +13,7 @@ def get_toolbar_tokens(): result.append(('class:bottom-toolbar', ' ')) if mycli.multi_line: - delimiter = special.delimiter.current + delimiter = special.get_current_delimiter() result.append( ( 'class:bottom-toolbar', diff --git a/mycli/packages/special/__init__.py b/mycli/packages/special/__init__.py index f4d105e0..92bcca6d 100644 --- a/mycli/packages/special/__init__.py +++ b/mycli/packages/special/__init__.py @@ -8,5 +8,3 @@ def export(defn): from . import dbcommands from . import iocommands - -delimiter = iocommands.delimiter_command diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index 84c668eb..4074772b 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -442,6 +442,18 @@ def watch_query(arg, **kwargs): set_pager_enabled(old_pager_enabled) +@export @special_command('delimiter', None, 'Change SQL delimiter.') def set_delimiter(arg, **_): return delimiter_command.set(arg) + + +@export +def get_current_delimiter(): + return delimiter_command.current + + +@export +def split_queries(input): + for query in delimiter_command.queries_iter(input): + yield query diff --git a/mycli/sqlexecute.py b/mycli/sqlexecute.py index fa79af9c..85ea39ff 100644 --- a/mycli/sqlexecute.py +++ b/mycli/sqlexecute.py @@ -168,7 +168,7 @@ def run(self, statement): if statement.startswith('\\fs'): components = [statement] else: - components = special.delimiter.queries_iter(statement) + components = special.split_queries(statement) for sql in components: # \G is treated specially since we have to set the expanded output. diff --git a/test/test_special_iocommands.py b/test/test_special_iocommands.py index 7ca33839..2f97742d 100644 --- a/test/test_special_iocommands.py +++ b/test/test_special_iocommands.py @@ -237,22 +237,20 @@ def test_asserts(gen): def test_split_sql_by_delimiter(): - delimiter = mycli.packages.special.delimiter for delimiter_str in (';', '$', '😀'): - delimiter.set(delimiter_str) + mycli.packages.special.set_delimiter(delimiter_str) sql_input = "select 1{} select \ufffc2".format(delimiter_str) queries = ( "select 1", "select \ufffc2" ) for query, parsed_query in zip( - queries, delimiter.queries_iter(sql_input)): + queries, mycli.packages.special.split_queries(sql_input)): assert(query == parsed_query) def test_switch_delimiter_within_query(): - delimiter = mycli.packages.special.delimiter - delimiter.set(';') + mycli.packages.special.set_delimiter(';') sql_input = "select 1; delimiter $$ select 2 $$ select 3 $$" queries = ( "select 1", @@ -261,17 +259,17 @@ def test_switch_delimiter_within_query(): "select 3" ) for query, parsed_query in zip( - queries, delimiter.queries_iter(sql_input)): + queries, + mycli.packages.special.split_queries(sql_input)): assert(query == parsed_query) def test_set_delimiter(): - delimiter = mycli.packages.special.delimiter + for delim in ('foo', 'bar'): - delimiter.set(delim) - assert delimiter.current == delim + mycli.packages.special.set_delimiter(delim) + assert mycli.packages.special.get_current_delimiter() == delim def teardown_function(): - delimiter = mycli.packages.special.delimiter - delimiter.set(';') + mycli.packages.special.set_delimiter(';') From 3f2298f1959ebfbed27f57e235a58740df0b0087 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sun, 13 Oct 2019 12:00:16 +0300 Subject: [PATCH 017/202] added --behave-args option to test runner --- setup.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 3081f19c..8c13e09e 100755 --- a/setup.py +++ b/setup.py @@ -57,18 +57,25 @@ def run(self): class test(TestCommand): - user_options = [('pytest-args=', 'a', 'Arguments to pass to pytest')] + user_options = [ + ('pytest-args=', 'a', 'Arguments to pass to pytest'), + ('behave-args=', 'b', 'Arguments to pass to pytest') + ] def initialize_options(self): TestCommand.initialize_options(self) self.pytest_args = '' + self.behave_args = '' def run_tests(self): unit_test_errno = subprocess.call( 'pytest ' + self.pytest_args, shell=True ) - cli_errno = subprocess.call('behave test/features', shell=True) + cli_errno = subprocess.call( + 'behave test/features ' + self.behave_args, + shell=True + ) sys.exit(unit_test_errno or cli_errno) From 28d4565bc9ac3c695b91db8e6ae22abf7a39ff5b Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sun, 13 Oct 2019 12:01:56 +0300 Subject: [PATCH 018/202] Restrict destructive command confirmation to ('yes', 'y') rather than ('yes', 'y', 'true', 't', '1') --- mycli/packages/prompt_utils.py | 22 +++++++++++++++++++++- test/features/crud_table.feature | 14 ++++++++++++++ test/features/steps/basic_commands.py | 21 +++++++++++++++++++++ test/features/steps/specials.py | 5 +++++ test/features/steps/wrappers.py | 14 ++++++++------ 5 files changed, 69 insertions(+), 7 deletions(-) diff --git a/mycli/packages/prompt_utils.py b/mycli/packages/prompt_utils.py index 138cef38..eb4912db 100644 --- a/mycli/packages/prompt_utils.py +++ b/mycli/packages/prompt_utils.py @@ -7,6 +7,26 @@ from .parseutils import is_destructive +class ConfirmBoolParamType(click.ParamType): + name = 'confirmation' + + def convert(self, value, param, ctx): + if isinstance(value, bool): + return bool(value) + value = value.lower() + if value in ('yes', 'y'): + return True + elif value in ('no', 'n'): + return False + self.fail('%s is not a valid boolean' % value, param, ctx) + + def __repr__(self): + return 'BOOL' + + +BOOLEAN_TYPE = ConfirmBoolParamType() + + def confirm_destructive_query(queries): """Check if the query is destructive and prompts the user to confirm. @@ -19,7 +39,7 @@ def confirm_destructive_query(queries): prompt_text = ("You're about to run a destructive command.\n" "Do you want to proceed? (y/n)") if is_destructive(queries) and sys.stdin.isatty(): - return prompt(prompt_text, type=bool) + return prompt(prompt_text, type=BOOLEAN_TYPE) def confirm(*args, **kwargs): diff --git a/test/features/crud_table.feature b/test/features/crud_table.feature index d2cc9dd8..ebcb8818 100644 --- a/test/features/crud_table.feature +++ b/test/features/crud_table.feature @@ -26,3 +26,17 @@ Feature: manipulate tables: then we see database connected when we select null then we see null selected + + Scenario: confirm destructive query + When we query "delete from foo;" + and we answer the destructive warning with "y" + then we see text "Your call!" + + Scenario: decline destructive query + When we query "delete from foo;" + and we answer the destructive warning with "n" + then we see text "Wise choice!" + + Scenario: confirm destructive query with invalid response + When we query "delete from foo;" + then we answer the destructive warning with invalid "1" and see text "is not a valid boolean" diff --git a/test/features/steps/basic_commands.py b/test/features/steps/basic_commands.py index 5764b3c6..cf6e4de1 100644 --- a/test/features/steps/basic_commands.py +++ b/test/features/steps/basic_commands.py @@ -73,9 +73,30 @@ def step_see_found(context): ''') + context.conf['pager_boundary'], timeout=5 ) + + @then(u'we confirm the destructive warning') def step_confirm_destructive_command(context): """Confirm destructive command.""" wrappers.expect_exact( context, 'You\'re about to run a destructive command.\r\nDo you want to proceed? (y/n):', timeout=2) context.cli.sendline('y') + + +@when(u'we answer the destructive warning with "{confirmation}"') +def step_confirm_destructive_command(context, confirmation): + """Confirm destructive command.""" + wrappers.expect_exact( + context, 'You\'re about to run a destructive command.\r\nDo you want to proceed? (y/n):', timeout=2) + context.cli.sendline(confirmation) + + +@then(u'we answer the destructive warning with invalid "{confirmation}" and see text "{text}"') +def step_confirm_destructive_command(context, confirmation, text): + """Confirm destructive command.""" + wrappers.expect_exact( + context, 'You\'re about to run a destructive command.\r\nDo you want to proceed? (y/n):', timeout=2) + context.cli.sendline(confirmation) + wrappers.expect_exact(context, text, timeout=2) + # we must exit the Click loop, or the feature will hang + context.cli.sendline('n') diff --git a/test/features/steps/specials.py b/test/features/steps/specials.py index 14a87cc3..924535fc 100644 --- a/test/features/steps/specials.py +++ b/test/features/steps/specials.py @@ -17,6 +17,11 @@ def step_refresh_completions(context): context.cli.sendline('rehash') +@then('we see text "{text}"') +def step_see_text(context, text): + """Wait to see given text message.""" + wrappers.expect_exact(context, text, timeout=2) + @then('we see completions refresh started') def step_see_refresh_started(context): """Wait to see refresh output.""" diff --git a/test/features/steps/wrappers.py b/test/features/steps/wrappers.py index 5dfd3800..763aadf9 100644 --- a/test/features/steps/wrappers.py +++ b/test/features/steps/wrappers.py @@ -85,11 +85,13 @@ def run_cli(context, run_args=None): context.currentdb = context.conf['dbname'] -def wait_prompt(context): +def wait_prompt(context, prompt=None): """Make sure prompt is displayed.""" - user = context.conf['user'] - host = context.conf['host'] - dbname = context.currentdb - expect_exact(context, '{0}@{1}:{2}> '.format( - user, host, dbname), timeout=5) + if prompt is None: + user = context.conf['user'] + host = context.conf['host'] + dbname = context.currentdb + prompt = '{0}@{1}:{2}> '.format( + user, host, dbname), + expect_exact(context, prompt, timeout=5) context.atprompt = True From 00d36219cacd9c63b5625d51f6abd78eb89e84e1 Mon Sep 17 00:00:00 2001 From: Jonathan Lloyd Date: Mon, 28 Oct 2019 22:29:08 +0000 Subject: [PATCH 019/202] Added clearer error message when failing to connect to default socket Fixes #572 --- mycli/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mycli/main.py b/mycli/main.py index 86eaf81c..2ef4fc52 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -438,9 +438,11 @@ def _connect(): self.logger.error( "traceback: %r", traceback.format_exc()) self.logger.debug('Retrying over TCP/IP') + self.echo( + "Failed to connect to local MySQL server through socket '{}':".format(socket)) self.echo(str(e), err=True) self.echo( - 'Failed to connect by socket, retrying over TCP/IP', err=True) + 'Retrying over TCP/IP', err=True) # Else fall back to TCP/IP localhost socket = "" From 568e9bb5065a0bd0ed179226f51e713fc7db89d8 Mon Sep 17 00:00:00 2001 From: Jonathan Lloyd Date: Mon, 28 Oct 2019 22:42:37 +0000 Subject: [PATCH 020/202] Added name to AUTHORS file --- mycli/AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/mycli/AUTHORS b/mycli/AUTHORS index 2e28ccad..36b9818c 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -67,6 +67,7 @@ Contributors: * Zane C. Bowers-Hadley * Mike Palandra * Georgy Frolov + * Jonathan Lloyd Creator: -------- From e5b1fb5f22ce85ed1cdf84579e4ce29546405273 Mon Sep 17 00:00:00 2001 From: Jonathan Lloyd Date: Mon, 28 Oct 2019 22:45:18 +0000 Subject: [PATCH 021/202] Updated Changelog --- changelog.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 292b969c..a0325c05 100644 --- a/changelog.md +++ b/changelog.md @@ -5,8 +5,8 @@ Features: --------- * Added DSN alias name as a format specifier to the prompt (Thanks: [Georgy Frolov]). * Mark `update` without `where`-clause as destructive query (Thanks: [Klaus Wünschel]). - * Added DELIMITER command (Thanks: [Georgy Frolov]) +* Added clearer error message when failing to connect to the default socket. 1.20.1 @@ -726,4 +726,4 @@ Bug Fixes: [Dick Marinus]: https://github.com/meeuw [François Pietka]: https://github.com/fpietka [Frederic Aoustin]: https://github.com/fraoustin -[Georgy Frolov]: https://github.com/pasenor \ No newline at end of file +[Georgy Frolov]: https://github.com/pasenor From 1480966c372ff96e2bd108aca3d0651747dc7778 Mon Sep 17 00:00:00 2001 From: Jonathan Lloyd Date: Tue, 29 Oct 2019 23:37:02 +0000 Subject: [PATCH 022/202] Fix linting error --- mycli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/main.py b/mycli/main.py index 2ef4fc52..7f42360c 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -439,7 +439,7 @@ def _connect(): "traceback: %r", traceback.format_exc()) self.logger.debug('Retrying over TCP/IP') self.echo( - "Failed to connect to local MySQL server through socket '{}':".format(socket)) + "Failed to connect to local MySQL server through socket '{}':".format(socket)) self.echo(str(e), err=True) self.echo( 'Retrying over TCP/IP', err=True) From a382b3d6286178e6a3eeb78a48621f9bcc75ad8c Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Mon, 4 Nov 2019 13:29:36 +0300 Subject: [PATCH 023/202] try more default socket paths --- mycli/main.py | 4 ++-- mycli/packages/filepaths.py | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index 7f42360c..9085e655 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -49,7 +49,7 @@ from .lexer import MyCliLexer from .__init__ import __version__ from .compat import WIN -from .packages.filepaths import dir_path_exists +from .packages.filepaths import dir_path_exists, guess_socket_location import itertools @@ -429,7 +429,7 @@ def _connect(): # Try a sensible default socket first (simplifies auth) # If we get a connection error, try tcp/ip localhost try: - socket = '/var/run/mysqld/mysqld.sock' + socket = guess_socket_location() _connect() except OperationalError as e: # These are "Can't open socket" and 2x "Can't connect" diff --git a/mycli/packages/filepaths.py b/mycli/packages/filepaths.py index 5ebdcd97..f3de46e1 100644 --- a/mycli/packages/filepaths.py +++ b/mycli/packages/filepaths.py @@ -3,11 +3,13 @@ from mycli.encodingutils import text_type import os +DEFAULT_SOCKET_DIRS = ('/var/run/', '/var/lib/', '/tmp') + def list_path(root_dir): """List directory if exists. - :param dir: str + :param root_dir: str :return: list """ @@ -84,3 +86,15 @@ def dir_path_exists(path): """ return os.path.exists(os.path.dirname(path)) + + +def guess_socket_location(): + """Try to guess the location of the default mysql socket file.""" + socket_dirs = filter(os.path.exists, DEFAULT_SOCKET_DIRS) + for directory in socket_dirs: + for r, dirs, files in os.walk(directory, topdown=True): + for filename in files: + if filename.startswith('mysql') and filename.endswith('.socket'): + return os.path.join(r, filename) + dirs[:] = [d for d in dirs if d.startswith('mysql')] + return '' From 879345192167873db73a727aec852c7f2b0b9412 Mon Sep 17 00:00:00 2001 From: Jakub Boukal Date: Wed, 6 Nov 2019 23:56:57 +0100 Subject: [PATCH 024/202] Extend main.is_dropping_database for create statement. Fix: #756 --- mycli/main.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/mycli/main.py b/mycli/main.py index 7f42360c..3d9ae4ed 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1218,6 +1218,7 @@ def need_completion_refresh(queries): def is_dropping_database(queries, dbname): """Determine if the query is dropping a specific database.""" + result = False if dbname is None: return False @@ -1236,7 +1237,13 @@ def normalize_db_name(db): if (first_token.value.lower() == 'drop' and second_token.value.lower() in ('database', 'schema') and database_name == dbname): - return True + result = True + elif (first_token.value.lower() == 'create' and + second_token.value.lower() in ('database', 'schema') and + database_name == dbname): + result = False + + return result def need_completion_reset(queries): From c5fdc0a77862dee188349c0c7b2755915e9e6b3b Mon Sep 17 00:00:00 2001 From: Jakub Boukal Date: Wed, 6 Nov 2019 23:57:06 +0100 Subject: [PATCH 025/202] Init test for main.is_dropping_database --- test/test_main.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/test_main.py b/test/test_main.py index 047d1b71..55362849 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -3,7 +3,7 @@ import click from click.testing import CliRunner -from mycli.main import MyCli, cli, thanks_picker, PACKAGE_ROOT +from mycli.main import MyCli, cli, thanks_picker, is_dropping_database, PACKAGE_ROOT from mycli.packages.special.main import COMMANDS as SPECIAL_COMMANDS from utils import USER, HOST, PORT, PASSWORD, dbtest, run @@ -151,6 +151,13 @@ def test_thanks_picker_utf8(): assert isinstance(name, text_type) +def test_is_dropping_database(): + is_dropping_text = "DROP DATABASE foo; USE sys;" + assert is_dropping_database(is_dropping_text, 'foo') + is_not_dropping_text = "DROP DATABASE foo; CREATE DATABASE foo; USE foo;" + assert not is_dropping_database(is_not_dropping_text, 'foo') + + def test_help_strings_end_with_periods(): """Make sure click options have help text that end with a period.""" for param in cli.params: From 5f4ff47bfb6a5e574e251900043463a1213d7dc9 Mon Sep 17 00:00:00 2001 From: Jakub Boukal Date: Thu, 7 Nov 2019 00:07:55 +0100 Subject: [PATCH 026/202] Add check with create after delete to changelog --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index a0325c05..a39271ba 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,7 @@ Features: * Mark `update` without `where`-clause as destructive query (Thanks: [Klaus Wünschel]). * Added DELIMITER command (Thanks: [Georgy Frolov]) * Added clearer error message when failing to connect to the default socket. +* Extend main.is_dropping_database check with create after delete statement. 1.20.1 From 461eb36c03f4eae7c90c64214312f00afb91dede Mon Sep 17 00:00:00 2001 From: Jakub Boukal Date: Thu, 7 Nov 2019 00:12:03 +0100 Subject: [PATCH 027/202] mycli is awesome --- mycli/AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/mycli/AUTHORS b/mycli/AUTHORS index 36b9818c..67db75ac 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -68,6 +68,7 @@ Contributors: * Mike Palandra * Georgy Frolov * Jonathan Lloyd + * Jakub Boukal Creator: -------- From 85c035233b3a3d4f3a7b428c5bab71172c477df1 Mon Sep 17 00:00:00 2001 From: Jakub Boukal Date: Thu, 7 Nov 2019 00:48:57 +0100 Subject: [PATCH 028/202] Enxtend test_is_dropping_database --- test/test_main.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/test_main.py b/test/test_main.py index 55362849..8061b86b 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -156,6 +156,10 @@ def test_is_dropping_database(): assert is_dropping_database(is_dropping_text, 'foo') is_not_dropping_text = "DROP DATABASE foo; CREATE DATABASE foo; USE foo;" assert not is_dropping_database(is_not_dropping_text, 'foo') + is_dropping_other_text = "DROP DATABASE bar; USE sys;" + assert not is_dropping_database(is_dropping_other_text, 'foo') + is_not_dropping_other_text = "DROP DATABASE foo; CREATE DATABASE bar; USE foo;" + assert is_dropping_database(is_not_dropping_other_text, 'foo') def test_help_strings_end_with_periods(): From 4ffe57aee61c73ba8e2e10552a8cbf239b639bfa Mon Sep 17 00:00:00 2001 From: Jakub Boukal Date: Thu, 7 Nov 2019 00:51:04 +0100 Subject: [PATCH 029/202] Remove USE from test_is_dropping_database test --- test/test_main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_main.py b/test/test_main.py index 8061b86b..6a529a8a 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -152,13 +152,13 @@ def test_thanks_picker_utf8(): def test_is_dropping_database(): - is_dropping_text = "DROP DATABASE foo; USE sys;" + is_dropping_text = "DROP DATABASE foo;" assert is_dropping_database(is_dropping_text, 'foo') - is_not_dropping_text = "DROP DATABASE foo; CREATE DATABASE foo; USE foo;" + is_not_dropping_text = "DROP DATABASE foo; CREATE DATABASE foo;" assert not is_dropping_database(is_not_dropping_text, 'foo') - is_dropping_other_text = "DROP DATABASE bar; USE sys;" + is_dropping_other_text = "DROP DATABASE bar;" assert not is_dropping_database(is_dropping_other_text, 'foo') - is_not_dropping_other_text = "DROP DATABASE foo; CREATE DATABASE bar; USE foo;" + is_not_dropping_other_text = "DROP DATABASE foo; CREATE DATABASE bar;" assert is_dropping_database(is_not_dropping_other_text, 'foo') From 9384d1e37cbd2bd95592839e41f86ce74b95c226 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Tue, 3 Dec 2019 13:54:50 +0300 Subject: [PATCH 030/202] Restrict prompt_toolkit version to <3.0.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8c13e09e..339629c4 100755 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ install_requirements = [ 'click >= 7.0', 'Pygments >= 1.6', - 'prompt_toolkit>=2.0.6', + 'prompt_toolkit>=2.0.6,<3.0.0', 'PyMySQL >= 0.9.2', 'sqlparse>=0.3.0,<0.4.0', 'configobj >= 5.0.5', From 55728f7438d41ba1361a0d79648da765b22a8faa Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Wed, 4 Dec 2019 23:53:45 +1100 Subject: [PATCH 031/202] Fix simple typo: unqiue -> unique Closes #801 --- mycli/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/config.py b/mycli/config.py index b1ec4f42..6b2bddf9 100644 --- a/mycli/config.py +++ b/mycli/config.py @@ -240,7 +240,7 @@ def _remove_pad(line): if pad_length > len(line) or len(set(line[-pad_length:])) != 1: # Pad length should be less than or equal to the length of the - # plaintext. The pad should have a single unqiue byte. + # plaintext. The pad should have a single unique byte. logger.warning('Invalid pad found in login path file.') return False From f7ea82c3a2fbab7cec29e646d12d2802b8a3e3c9 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Tue, 3 Dec 2019 12:30:14 +0300 Subject: [PATCH 032/202] Allow to set the output file multiple times: after a first write to the first output file all subsequent \o declarations were immediately undone --- changelog.md | 1 + mycli/packages/special/iocommands.py | 7 ++++--- test/features/iocommands.feature | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index a0325c05..844e36da 100644 --- a/changelog.md +++ b/changelog.md @@ -16,6 +16,7 @@ Bug Fixes: ---------- * Fix an error when using login paths with an explicit database name (Thanks: [Thomas Roten]). +* The \o command could only be used once per session (Thanks: [Georgy Frolov]) 1.20.0 diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index 4074772b..0d73b1d4 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -23,7 +23,8 @@ use_expanded_output = False PAGER_ENABLED = True tee_file = None -once_file = written_to_once_file = None +once_file = None +written_to_once_file = False favoritequeries = FavoriteQueries(ConfigObj()) delimiter_command = DelimiterCommand() @@ -341,9 +342,10 @@ def write_tee(output): 'Append next result to an output file (overwrite using -o).', aliases=('\\o', )) def set_once(arg, **_): - global once_file + global once_file, written_to_once_file once_file = parseargfile(arg) + written_to_once_file = False return [(None, None, None, "")] @@ -358,7 +360,6 @@ def write_once(output): once_file = None raise OSError("Cannot write to file '{}': {}".format( e.filename, e.strerror)) - with f: click.echo(output, file=f, nl=False) click.echo(u"\n", file=f, nl=False) diff --git a/test/features/iocommands.feature b/test/features/iocommands.feature index 246a44a3..95366eba 100644 --- a/test/features/iocommands.feature +++ b/test/features/iocommands.feature @@ -30,3 +30,18 @@ Feature: I/O commands then we see result "123" and we see result "456" and delimiter is set to "%" + + Scenario: send output to file + When we query "\o /tmp/output1.sql" + and we query "select 123" + and we query "system cat /tmp/output1.sql" + then we see result "123" + + Scenario: send output to file two times + When we query "\o /tmp/output1.sql" + and we query "select 123" + and we query "\o /tmp/output2.sql" + and we query "select 456" + and we query "system cat /tmp/output2.sql" + then we see result "456" + \ No newline at end of file From e4fe73e2e5146dc3e07869686912e74e0c145635 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sat, 7 Dec 2019 17:40:03 +0300 Subject: [PATCH 033/202] updated description in changelog --- changelog.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 844e36da..7e278cc6 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,10 @@ Features: * Added DELIMITER command (Thanks: [Georgy Frolov]) * Added clearer error message when failing to connect to the default socket. +Bug Fixes: +---------- + +* Allow \o command more than once per session (Thanks: [Georgy Frolov]) 1.20.1 ====== @@ -16,8 +20,6 @@ Bug Fixes: ---------- * Fix an error when using login paths with an explicit database name (Thanks: [Thomas Roten]). -* The \o command could only be used once per session (Thanks: [Georgy Frolov]) - 1.20.0 ====== From 31164db72047b50f3e5648372dfb18c2851fa718 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sat, 7 Dec 2019 18:06:55 +0300 Subject: [PATCH 034/202] fixed version of colorama (pep8radius dependency) --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1c1333d2..69e52d07 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,5 +8,6 @@ pexpect coverage==4.3.4 codecov==2.0.9 autopep8==1.3.3 +colorama==0.4.1 git+https://github.com/hayd/pep8radius.git # --error-status option not released click>=7.0 From 550c8bfb36f2ca35ea030e595b43783a0ce48f74 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sun, 15 Dec 2019 14:35:02 +0300 Subject: [PATCH 035/202] dont auto-execute query after an editor session --- mycli/main.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mycli/main.py b/mycli/main.py index 7f42360c..ec889a13 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -22,6 +22,7 @@ import sqlparse from prompt_toolkit.completion import DynamicCompleter from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode +from prompt_toolkit.key_binding.bindings.named_commands import register as prompt_register from prompt_toolkit.shortcuts import PromptSession, CompleteStyle from prompt_toolkit.document import Document from prompt_toolkit.filters import HasFocus, IsDone @@ -1278,5 +1279,13 @@ def thanks_picker(files=()): return choice(contents) +@prompt_register('edit-and-execute-command') +def edit_and_execute(event): + """Different from the prompt-toolkit default, we want to have a choice not + to execute a query after editing, hence validate_and_handle=False.""" + buff = event.current_buffer + buff.open_in_editor(validate_and_handle=False) + + if __name__ == "__main__": cli() From 6e15365bbf53d7e40c87cc3d0bf666e99a995b37 Mon Sep 17 00:00:00 2001 From: iTakeshi Date: Sat, 1 Feb 2020 15:49:07 +0900 Subject: [PATCH 036/202] search XDG_CONFIG_HOME for myclirc --- changelog.md | 1 + mycli/AUTHORS | 1 + mycli/main.py | 2 ++ 3 files changed, 4 insertions(+) diff --git a/changelog.md b/changelog.md index 7e278cc6..9082bfee 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,7 @@ Features: * Mark `update` without `where`-clause as destructive query (Thanks: [Klaus Wünschel]). * Added DELIMITER command (Thanks: [Georgy Frolov]) * Added clearer error message when failing to connect to the default socket. +* Search `${XDG_CONFIG_HOME}/mycli/myclirc` after `${HOME}/.myclirc` and before `/etc/myclirc` (Thanks: [Takeshi D. Itoh]) Bug Fixes: ---------- diff --git a/mycli/AUTHORS b/mycli/AUTHORS index 36b9818c..c4e89c2c 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -68,6 +68,7 @@ Contributors: * Mike Palandra * Georgy Frolov * Jonathan Lloyd + * Takeshi D. Itoh Creator: -------- diff --git a/mycli/main.py b/mycli/main.py index ec889a13..cb3aeab6 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -91,6 +91,8 @@ class MyCli(object): system_config_files = [ '/etc/myclirc', + os.path.join( + os.environ.get("XDG_CONFIG_HOME", "~/.config"), "mycli", "myclirc") ] default_config_file = os.path.join(PACKAGE_ROOT, 'myclirc') From 6d87744075519ec28353772988126ddc9efbd03e Mon Sep 17 00:00:00 2001 From: iTakeshi Date: Mon, 3 Feb 2020 23:27:09 +0900 Subject: [PATCH 037/202] check XDG_CONFIG_HOME is empty or not --- mycli/main.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index cb3aeab6..07b25968 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -89,10 +89,14 @@ class MyCli(object): '~/.my.cnf' ] + # check XDG_CONFIG_HOME exists and not an empty string + if os.environ.get("XDG_CONFIG_HOME"): + xdg_config_home = os.environ.get("XDG_CONFIG_HOME") + else: + xdg_config_home = "~/.config" system_config_files = [ '/etc/myclirc', - os.path.join( - os.environ.get("XDG_CONFIG_HOME", "~/.config"), "mycli", "myclirc") + os.path.join(xdg_config_home, "mycli", "myclirc") ] default_config_file = os.path.join(PACKAGE_ROOT, 'myclirc') From 8c3179a729413285bba0db2f82a0250979ce0bbe Mon Sep 17 00:00:00 2001 From: joestar Date: Wed, 19 Feb 2020 22:41:52 +0800 Subject: [PATCH 038/202] fix vscode test discover failed with pytest --- test/__init__.py | 0 test/conftest.py | 4 ++-- test/test_dbspecial.py | 2 +- test/test_main.py | 4 ++-- test/test_special_iocommands.py | 2 +- test/test_sqlexecute.py | 2 +- test/test_tabular_output.py | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 test/__init__.py diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/conftest.py b/test/conftest.py index 6daf374e..cf6d721b 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,6 +1,6 @@ import pytest -from utils import (HOST, USER, PASSWORD, PORT, CHARSET, create_db, - db_connection, SSH_USER, SSH_HOST, SSH_PORT) +from .utils import (HOST, USER, PASSWORD, PORT, CHARSET, create_db, + db_connection, SSH_USER, SSH_HOST, SSH_PORT) import mycli.sqlexecute diff --git a/test/test_dbspecial.py b/test/test_dbspecial.py index 8b2e909c..21e389ce 100644 --- a/test/test_dbspecial.py +++ b/test/test_dbspecial.py @@ -1,5 +1,5 @@ from mycli.packages.completion_engine import suggest_type -from test_completion_engine import sorted_dicts +from .test_completion_engine import sorted_dicts from mycli.packages.special.utils import format_uptime diff --git a/test/test_main.py b/test/test_main.py index 047d1b71..658629f9 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -5,7 +5,7 @@ from mycli.main import MyCli, cli, thanks_picker, PACKAGE_ROOT from mycli.packages.special.main import COMMANDS as SPECIAL_COMMANDS -from utils import USER, HOST, PORT, PASSWORD, dbtest, run +from .utils import USER, HOST, PORT, PASSWORD, dbtest, run from textwrap import dedent from collections import namedtuple @@ -388,7 +388,7 @@ def run_query(self, query, new_line=True): MockMyCli.connect_args["port"] == 5 and \ MockMyCli.connect_args["database"] == "arg_database" - # Use a DNS without password + # Use a DSN without password result = runner.invoke(mycli.main.cli, args=[ "mysql://dsn_user@dsn_host:6/dsn_database"] ) diff --git a/test/test_special_iocommands.py b/test/test_special_iocommands.py index 2f97742d..e6d3fc8b 100644 --- a/test/test_special_iocommands.py +++ b/test/test_special_iocommands.py @@ -12,7 +12,7 @@ import mycli.packages.special -from utils import dbtest, db_connection, send_ctrl_c +from .utils import dbtest, db_connection, send_ctrl_c def test_set_get_pager(): diff --git a/test/test_sqlexecute.py b/test/test_sqlexecute.py index d0e61662..f73c8670 100644 --- a/test/test_sqlexecute.py +++ b/test/test_sqlexecute.py @@ -5,7 +5,7 @@ import pytest import pymysql -from utils import run, dbtest, set_expanded_output, is_expanded_output +from .utils import run, dbtest, set_expanded_output, is_expanded_output def assert_result_equal(result, title=None, rows=None, headers=None, diff --git a/test/test_tabular_output.py b/test/test_tabular_output.py index 357c3257..9e854861 100644 --- a/test/test_tabular_output.py +++ b/test/test_tabular_output.py @@ -7,7 +7,7 @@ from mycli.packages.tabular_output import sql_format from cli_helpers.tabular_output import TabularOutputFormatter -from utils import USER, PASSWORD, HOST, PORT, dbtest +from .utils import USER, PASSWORD, HOST, PORT, dbtest import pytest from mycli.main import MyCli From ec17f50b3067516ab203c935d9d66b54911d81d2 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Tue, 3 Mar 2020 06:57:56 +0300 Subject: [PATCH 039/202] platform-specific default socket paths --- mycli/main.py | 6 ++++++ mycli/packages/filepaths.py | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index 9085e655..fe9c3eff 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -9,6 +9,7 @@ import re import fileinput from collections import namedtuple +from pwd import getpwuid from time import time from datetime import datetime from random import choice @@ -451,6 +452,11 @@ def _connect(): _connect() else: raise e + else: + socket_owner = getpwuid(os.stat(socket).st_uid).pw_name + self.echo( + "Using socket {}, owned by user {}".format(socket, socket_owner) + ) else: host = host or 'localhost' port = port or 3306 diff --git a/mycli/packages/filepaths.py b/mycli/packages/filepaths.py index f3de46e1..ef309e62 100644 --- a/mycli/packages/filepaths.py +++ b/mycli/packages/filepaths.py @@ -2,8 +2,16 @@ from __future__ import unicode_literals from mycli.encodingutils import text_type import os +import platform -DEFAULT_SOCKET_DIRS = ('/var/run/', '/var/lib/', '/tmp') + +if os.name == "posix": + if platform.system() == "Darwin": + DEFAULT_SOCKET_DIRS = ("/tmp",) + else: + DEFAULT_SOCKET_DIRS = ("/var/run", "/var/lib") +else: + DEFAULT_SOCKET_DIRS = () def list_path(root_dir): @@ -94,7 +102,7 @@ def guess_socket_location(): for directory in socket_dirs: for r, dirs, files in os.walk(directory, topdown=True): for filename in files: - if filename.startswith('mysql') and filename.endswith('.socket'): + if filename.startswith("mysql") and filename.endswith(".socket"): return os.path.join(r, filename) - dirs[:] = [d for d in dirs if d.startswith('mysql')] - return '' + dirs[:] = [d for d in dirs if d.startswith("mysql")] + return "" From dc5c14ee1f6af5794661d4768471d77318502029 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Tue, 3 Mar 2020 07:38:20 +0300 Subject: [PATCH 040/202] use socket from my.cnf if available --- mycli/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index fe9c3eff..bac58d85 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -426,11 +426,11 @@ def _connect(): raise e try: - if (socket is host is port is None) and not WIN: + if (host is None) and not WIN: # Try a sensible default socket first (simplifies auth) # If we get a connection error, try tcp/ip localhost try: - socket = guess_socket_location() + socket = socket or guess_socket_location() _connect() except OperationalError as e: # These are "Can't open socket" and 2x "Can't connect" From e661ff1b3774a23f0279f18aa38c3261655e8b2e Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Fri, 13 Mar 2020 09:27:00 -0400 Subject: [PATCH 041/202] Fix condition in change_db (resolve SyntaxWarning) ``` $ mycli /usr/local/Cellar/mycli/1.20.1_3/libexec/lib/python3.8/site-packages/mycli/main.py:224: SyntaxWarning: "is" with a literal. Did you mean "=="? if arg is '': ``` --- mycli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/main.py b/mycli/main.py index ec889a13..ce7a236a 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -223,7 +223,7 @@ def change_table_format(self, arg, **_): yield (None, None, None, msg) def change_db(self, arg, **_): - if arg is '': + if not arg: click.secho( "No database selected", err=True, fg="red" From 37c36ef5ebdf57f6aaf04c52ff80d7f23cd35a57 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Tue, 3 Mar 2020 20:40:54 +0300 Subject: [PATCH 042/202] fixed is_dropping_database() after sqlparse update --- changelog.md | 1 + mycli/main.py | 24 +----------------------- mycli/packages/parseutils.py | 29 +++++++++++++++++++++++++++++ test/test_parseutils.py | 26 ++++++++++++++++++++++++-- 4 files changed, 55 insertions(+), 25 deletions(-) diff --git a/changelog.md b/changelog.md index 7e278cc6..bda03405 100644 --- a/changelog.md +++ b/changelog.md @@ -12,6 +12,7 @@ Bug Fixes: ---------- * Allow \o command more than once per session (Thanks: [Georgy Frolov]) +* Fixed crash when the query dropping the current database starts with a comment (Thanks: [Georgy Frolov]) 1.20.1 ====== diff --git a/mycli/main.py b/mycli/main.py index ec889a13..d279c0b3 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -20,6 +20,7 @@ from cli_helpers.utils import strip_ansi import click import sqlparse +from mycli.packages.parseutils import is_dropping_database from prompt_toolkit.completion import DynamicCompleter from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode from prompt_toolkit.key_binding.bindings.named_commands import register as prompt_register @@ -1217,29 +1218,6 @@ def need_completion_refresh(queries): return False -def is_dropping_database(queries, dbname): - """Determine if the query is dropping a specific database.""" - if dbname is None: - return False - - def normalize_db_name(db): - return db.lower().strip('`"') - - dbname = normalize_db_name(dbname) - - for query in sqlparse.parse(queries): - if query.get_name() is None: - continue - - first_token = query.token_first(skip_cm=True) - _, second_token = query.token_next(0, skip_cm=True) - database_name = normalize_db_name(query.get_name()) - if (first_token.value.lower() == 'drop' and - second_token.value.lower() in ('database', 'schema') and - database_name == dbname): - return True - - def need_completion_reset(queries): """Determines if the statement is a database switch such as 'use' or '\\u'. When a database is changed the existing completions must be reset before we diff --git a/mycli/packages/parseutils.py b/mycli/packages/parseutils.py index 8a39fc67..299decac 100644 --- a/mycli/packages/parseutils.py +++ b/mycli/packages/parseutils.py @@ -238,3 +238,32 @@ def is_open_quote(sql): if __name__ == '__main__': sql = 'select * from (select t. from tabl t' print (extract_tables(sql)) + + +def is_dropping_database(queries, dbname): + """Determine if the query is dropping a specific database.""" + if dbname is None: + return False + + def normalize_db_name(db): + return db.lower().strip('`"') + + dbname = normalize_db_name(dbname) + + for query in sqlparse.parse(queries): + keywords = [t for t in query.tokens if t.is_keyword] + if len(keywords) < 2: + continue + if keywords[0].normalized == "DROP" and keywords[1].value.lower() in ( + "database", + "schema", + ): + database_token = next( + (t for t in query.tokens if isinstance(t, Identifier)), None + ) + return ( + database_token is not None + and normalize_db_name(database_token.get_name()) == dbname + ) + else: + return False diff --git a/test/test_parseutils.py b/test/test_parseutils.py index 1cca9145..c5271045 100644 --- a/test/test_parseutils.py +++ b/test/test_parseutils.py @@ -1,7 +1,7 @@ import pytest from mycli.packages.parseutils import ( - extract_tables, query_starts_with, queries_start_with, is_destructive, query_has_where_clause -) + extract_tables, query_starts_with, queries_start_with, is_destructive, query_has_where_clause, + is_dropping_database) def test_empty_string(): @@ -164,3 +164,25 @@ def test_is_destructive_update_without_where_clause(): ) def test_query_has_where_clause(sql, has_where_clause): assert query_has_where_clause(sql) is has_where_clause + + +@pytest.mark.parametrize( + ('sql', 'dbname', 'is_dropping'), + [ + ('select bar from foo', 'foo', False), + ('drop database "foo";', '`foo`', True), + ('drop schema foo', 'foo', True), + ('drop schema foo', 'bar', False), + ('drop database bar', 'foo', False), + ('drop database foo', None, False), + ('select bar from foo; drop database bazz', 'foo', False), + ('select bar from foo; drop database bazz', 'bazz', True), + ('-- dropping database \n ' + 'drop -- really dropping \n ' + 'schema abc -- now it is dropped', + 'abc', + True) + ] +) +def test_is_dropping_database(sql, dbname, is_dropping): + assert is_dropping_database(sql, dbname) == is_dropping From 6e661727862ec46cbe6bbb02edbdee26e237cf21 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Tue, 17 Mar 2020 19:47:47 +0300 Subject: [PATCH 043/202] retire python < 3.6 (#826) --- .travis.yml | 4 +-- changelog.md | 4 +++ mycli/clibuffer.py | 2 -- mycli/clistyle.py | 2 -- mycli/clitoolbar.py | 2 -- mycli/compat.py | 3 -- mycli/config.py | 1 - mycli/encodingutils.py | 36 ------------------- mycli/key_bindings.py | 1 - mycli/main.py | 16 ++++----- mycli/packages/completion_engine.py | 11 +----- mycli/packages/filepaths.py | 7 ++-- mycli/packages/parseutils.py | 1 - mycli/packages/prompt_utils.py | 4 --- mycli/packages/special/dbcommands.py | 1 - mycli/packages/special/delimitercommand.py | 3 -- mycli/packages/special/favoritequeries.py | 3 -- mycli/packages/special/iocommands.py | 3 +- mycli/packages/special/main.py | 1 - mycli/packages/tabular_output/sql_format.py | 2 -- mycli/sqlcompleter.py | 2 -- mycli/sqlexecute.py | 2 -- release.py | 4 +-- setup.py | 10 ++---- test/features/db_utils.py | 4 --- test/features/environment.py | 4 --- test/features/fixture_utils.py | 5 +-- test/features/steps/auto_vertical.py | 2 -- test/features/steps/basic_commands.py | 2 -- test/features/steps/crud_database.py | 2 -- test/features/steps/crud_table.py | 2 -- test/features/steps/iocommands.py | 2 -- test/features/steps/named_queries.py | 2 -- test/features/steps/specials.py | 2 -- test/features/steps/wrappers.py | 3 -- test/test_clistyle.py | 1 - test/test_main.py | 6 +--- test/test_naive_completion.py | 1 - test/test_prompt_utils.py | 3 -- ...est_smart_completion_public_schema_only.py | 2 -- test/test_special_iocommands.py | 3 -- test/test_sqlexecute.py | 2 -- test/test_tabular_output.py | 2 -- test/utils.py | 3 -- tox.ini | 2 +- 45 files changed, 22 insertions(+), 158 deletions(-) delete mode 100644 mycli/encodingutils.py diff --git a/.travis.yml b/.travis.yml index 3b4d98ca..1ea67b2a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,7 @@ language: python python: - - "2.7" - - "3.4" - - "3.5" - "3.6" + - "3.7" matrix: include: diff --git a/changelog.md b/changelog.md index bda03405..8ea28218 100644 --- a/changelog.md +++ b/changelog.md @@ -14,6 +14,10 @@ Bug Fixes: * Allow \o command more than once per session (Thanks: [Georgy Frolov]) * Fixed crash when the query dropping the current database starts with a comment (Thanks: [Georgy Frolov]) +Internal: +--------- +* deprecate python versions 2.7, 3.4, 3.5 + 1.20.1 ====== diff --git a/mycli/clibuffer.py b/mycli/clibuffer.py index 3e720c6d..c9d29d1d 100644 --- a/mycli/clibuffer.py +++ b/mycli/clibuffer.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import Condition from prompt_toolkit.application import get_app diff --git a/mycli/clistyle.py b/mycli/clistyle.py index 6f8b03af..c94f7931 100644 --- a/mycli/clistyle.py +++ b/mycli/clistyle.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import logging import pygments.styles diff --git a/mycli/clitoolbar.py b/mycli/clitoolbar.py index cf73b97d..e03e1826 100644 --- a/mycli/clitoolbar.py +++ b/mycli/clitoolbar.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.application import get_app from prompt_toolkit.enums import EditingMode diff --git a/mycli/compat.py b/mycli/compat.py index ee1167b0..2ebfe07f 100644 --- a/mycli/compat.py +++ b/mycli/compat.py @@ -1,9 +1,6 @@ -# -*- coding: utf-8 -*- """Platform and Python version compatibility support.""" import sys -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 WIN = sys.platform in ('win32', 'cygwin') diff --git a/mycli/config.py b/mycli/config.py index 6b2bddf9..77475099 100644 --- a/mycli/config.py +++ b/mycli/config.py @@ -1,4 +1,3 @@ -from __future__ import print_function import shutil from io import BytesIO, TextIOWrapper import logging diff --git a/mycli/encodingutils.py b/mycli/encodingutils.py deleted file mode 100644 index d53076f1..00000000 --- a/mycli/encodingutils.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from mycli.compat import PY2 - - -if PY2: - text_type = unicode - binary_type = str -else: - text_type = str - binary_type = bytes - - -def unicode2utf8(arg): - """Convert strings to UTF8-encoded bytes. - - Only in Python 2. In Python 3 the args are expected as unicode. - - """ - - if PY2 and isinstance(arg, text_type): - return arg.encode('utf-8') - return arg - - -def utf8tounicode(arg): - """Convert UTF8-encoded bytes to strings. - - Only in Python 2. In Python 3 the errors are returned as strings. - - """ - - if PY2 and isinstance(arg, binary_type): - return arg.decode('utf-8') - return arg diff --git a/mycli/key_bindings.py b/mycli/key_bindings.py index 53ff55ef..57b917bf 100644 --- a/mycli/key_bindings.py +++ b/mycli/key_bindings.py @@ -1,4 +1,3 @@ -from __future__ import unicode_literals import logging from prompt_toolkit.enums import EditingMode from prompt_toolkit.filters import completion_is_selected diff --git a/mycli/main.py b/mycli/main.py index d279c0b3..34755c9b 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1,6 +1,3 @@ -from __future__ import unicode_literals -from __future__ import print_function - import os import sys import traceback @@ -47,7 +44,6 @@ open_mylogin_cnf, read_config_files, str_to_bool, strip_matching_quotes) from .key_bindings import mycli_bindings -from .encodingutils import utf8tounicode, text_type from .lexer import MyCliLexer from .__init__ import __version__ from .compat import WIN @@ -241,7 +237,7 @@ def execute_from_file(self, arg, **_): message = 'Missing required argument, filename.' return [(None, None, None, message)] try: - with open(os.path.expanduser(arg), encoding='utf-8') as f: + with open(os.path.expanduser(arg)) as f: query = f.read() except IOError as e: return [(None, None, None, str(e))] @@ -751,7 +747,7 @@ def one_iteration(text=None): def log_output(self, output): """Log the output in the audit log, if it's enabled.""" if self.logfile: - click.echo(utf8tounicode(output), file=self.logfile) + click.echo(output, file=self.logfile) def echo(self, s, **kwargs): """Print a message to stdout. @@ -926,8 +922,8 @@ def format_output(self, title, cur, headers, expanded=False, column_types = None if hasattr(cur, 'description'): def get_col_type(col): - col_type = FIELD_TYPES.get(col[1], text_type) - return col_type if type(col_type) is type else text_type + col_type = FIELD_TYPES.get(col[1], str) + return col_type if type(col_type) is type else str column_types = [get_col_type(col) for col in cur.description] if max_width is not None: @@ -938,7 +934,7 @@ def get_col_type(col): column_types=column_types, **output_kwargs) - if isinstance(formatted, (text_type)): + if isinstance(formatted, str): formatted = formatted.splitlines() formatted = iter(formatted) @@ -947,7 +943,7 @@ def get_col_type(col): if len(strip_ansi(first_line)) > max_width: formatted = self.formatter.format_output( cur, headers, format_name='vertical', column_types=column_types, **output_kwargs) - if isinstance(formatted, (text_type)): + if isinstance(formatted, str): formatted = iter(formatted.splitlines()) else: formatted = itertools.chain([first_line], formatted) diff --git a/mycli/packages/completion_engine.py b/mycli/packages/completion_engine.py index bea79274..2b19c32d 100644 --- a/mycli/packages/completion_engine.py +++ b/mycli/packages/completion_engine.py @@ -1,4 +1,3 @@ -from __future__ import print_function import os import sys import sqlparse @@ -7,14 +6,6 @@ from .parseutils import last_word, extract_tables, find_prev_keyword from .special import parse_special_command -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 - -if PY3: - string_types = str -else: - string_types = basestring - def suggest_type(full_text, text_before_cursor): """Takes the full_text that is typed so far and also the text before the @@ -123,7 +114,7 @@ def suggest_special(text): def suggest_based_on_last_token(token, text_before_cursor, full_text, identifier): - if isinstance(token, string_types): + if isinstance(token, str): token_v = token.lower() elif isinstance(token, Comparison): # If 'token' is a Comparison type such as diff --git a/mycli/packages/filepaths.py b/mycli/packages/filepaths.py index 5ebdcd97..ac58851d 100644 --- a/mycli/packages/filepaths.py +++ b/mycli/packages/filepaths.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -from __future__ import unicode_literals -from mycli.encodingutils import text_type import os @@ -62,10 +59,10 @@ def suggest_path(root_dir): """ if not root_dir: - return [text_type(os.path.abspath(os.sep)), text_type('~'), text_type(os.curdir), text_type(os.pardir)] + return [os.path.abspath(os.sep), '~', os.curdir, os.pardir] if '~' in root_dir: - root_dir = text_type(os.path.expanduser(root_dir)) + root_dir = os.path.expanduser(root_dir) if not os.path.exists(root_dir): root_dir, _ = os.path.split(root_dir) diff --git a/mycli/packages/parseutils.py b/mycli/packages/parseutils.py index 299decac..8412204c 100644 --- a/mycli/packages/parseutils.py +++ b/mycli/packages/parseutils.py @@ -1,4 +1,3 @@ -from __future__ import print_function import re import sqlparse from sqlparse.sql import IdentifierList, Identifier, Function diff --git a/mycli/packages/prompt_utils.py b/mycli/packages/prompt_utils.py index eb4912db..fb1e431a 100644 --- a/mycli/packages/prompt_utils.py +++ b/mycli/packages/prompt_utils.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - - import sys import click from .parseutils import is_destructive diff --git a/mycli/packages/special/dbcommands.py b/mycli/packages/special/dbcommands.py index d29507a6..ed90e4c3 100644 --- a/mycli/packages/special/dbcommands.py +++ b/mycli/packages/special/dbcommands.py @@ -1,4 +1,3 @@ -from __future__ import unicode_literals import logging import os import platform diff --git a/mycli/packages/special/delimitercommand.py b/mycli/packages/special/delimitercommand.py index a7dc7979..994b134b 100644 --- a/mycli/packages/special/delimitercommand.py +++ b/mycli/packages/special/delimitercommand.py @@ -1,6 +1,3 @@ -# coding: utf-8 -from __future__ import unicode_literals - import re import sqlparse diff --git a/mycli/packages/special/favoritequeries.py b/mycli/packages/special/favoritequeries.py index 55dd8cad..65248a76 100644 --- a/mycli/packages/special/favoritequeries.py +++ b/mycli/packages/special/favoritequeries.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - class FavoriteQueries(object): section_name = 'favorite_queries' diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index 0d73b1d4..c58310fb 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -1,4 +1,3 @@ -from __future__ import unicode_literals import os import re import locale @@ -152,7 +151,7 @@ def open_external_editor(filename=None, sql=None): if filename: try: - with open(filename, encoding='utf-8') as f: + with open(filename) as f: query = f.read() except IOError: message = 'Error reading file: %s.' % filename diff --git a/mycli/packages/special/main.py b/mycli/packages/special/main.py index f6e7a115..dddba66a 100644 --- a/mycli/packages/special/main.py +++ b/mycli/packages/special/main.py @@ -1,4 +1,3 @@ -from __future__ import unicode_literals import logging from collections import namedtuple diff --git a/mycli/packages/tabular_output/sql_format.py b/mycli/packages/tabular_output/sql_format.py index b5e43466..3ad0aa2a 100644 --- a/mycli/packages/tabular_output/sql_format.py +++ b/mycli/packages/tabular_output/sql_format.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """Format adapter for sql.""" -from __future__ import unicode_literals from cli_helpers.utils import filter_dict_by_key from mycli.packages.parseutils import extract_tables diff --git a/mycli/sqlcompleter.py b/mycli/sqlcompleter.py index 2f932338..36b4934e 100644 --- a/mycli/sqlcompleter.py +++ b/mycli/sqlcompleter.py @@ -1,5 +1,3 @@ -from __future__ import print_function -from __future__ import unicode_literals import logging from re import compile, escape from collections import Counter diff --git a/mycli/sqlexecute.py b/mycli/sqlexecute.py index 85ea39ff..88db9dc4 100644 --- a/mycli/sqlexecute.py +++ b/mycli/sqlexecute.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import logging import pymysql import sqlparse diff --git a/release.py b/release.py index 18e1b8f2..ef84927d 100755 --- a/release.py +++ b/release.py @@ -1,7 +1,5 @@ -#!/usr/bin/env python """A script to publish a release of mycli to PyPI.""" -from __future__ import print_function import io from optparse import OptionParser import re @@ -49,7 +47,7 @@ def version(version_file): _version_re = re.compile( r'__version__\s+=\s+(?P[\'"])(?P.*)(?P=quote)') - with io.open(version_file, encoding='utf-8') as f: + with open(version_file) as f: ver = _version_re.search(f.read()).group('version') return ver diff --git a/setup.py b/setup.py index 339629c4..a2ece1d1 100755 --- a/setup.py +++ b/setup.py @@ -10,9 +10,9 @@ _version_re = re.compile(r'__version__\s+=\s+(.*)') -with open('mycli/__init__.py', 'rb') as f: - version = str(ast.literal_eval(_version_re.search( - f.read().decode('utf-8')).group(1))) +with open('mycli/__init__.py') as f: + version = ast.literal_eval(_version_re.search( + f.read()).group(1)) description = 'CLI for MySQL Database. With auto-completion and syntax highlighting.' @@ -99,11 +99,7 @@ def run_tests(self): 'License :: OSI Approved :: BSD License', 'Operating System :: Unix', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: SQL', diff --git a/test/features/db_utils.py b/test/features/db_utils.py index ef0b42ff..c29dedb3 100644 --- a/test/features/db_utils.py +++ b/test/features/db_utils.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals -from __future__ import print_function - import pymysql diff --git a/test/features/environment.py b/test/features/environment.py index 4d090a99..c906b3b4 100644 --- a/test/features/environment.py +++ b/test/features/environment.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals -from __future__ import print_function - import os import sys from tempfile import mkstemp diff --git a/test/features/fixture_utils.py b/test/features/fixture_utils.py index a171e34c..f85e0f65 100644 --- a/test/features/fixture_utils.py +++ b/test/features/fixture_utils.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import print_function - import os import io @@ -13,7 +10,7 @@ def read_fixture_lines(filename): """ lines = [] - for line in io.open(filename, 'r', encoding='utf8'): + for line in open(filename): lines.append(line.strip()) return lines diff --git a/test/features/steps/auto_vertical.py b/test/features/steps/auto_vertical.py index e8cb60f0..6a67106a 100644 --- a/test/features/steps/auto_vertical.py +++ b/test/features/steps/auto_vertical.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -from __future__ import unicode_literals from textwrap import dedent from behave import then, when diff --git a/test/features/steps/basic_commands.py b/test/features/steps/basic_commands.py index cf6e4de1..425ef674 100644 --- a/test/features/steps/basic_commands.py +++ b/test/features/steps/basic_commands.py @@ -1,11 +1,9 @@ -# -*- coding: utf-8 """Steps for behavioral style tests are defined in this module. Each step is defined by the string decorating it. This string is used to call the step in "*.feature" file. """ -from __future__ import unicode_literals from behave import when from textwrap import dedent diff --git a/test/features/steps/crud_database.py b/test/features/steps/crud_database.py index ef7f3f62..a0bfa530 100644 --- a/test/features/steps/crud_database.py +++ b/test/features/steps/crud_database.py @@ -1,11 +1,9 @@ -# -*- coding: utf-8 -*- """Steps for behavioral style tests are defined in this module. Each step is defined by the string decorating it. This string is used to call the step in "*.feature" file. """ -from __future__ import unicode_literals import pexpect diff --git a/test/features/steps/crud_table.py b/test/features/steps/crud_table.py index 01b2bbf9..f715f0ca 100644 --- a/test/features/steps/crud_table.py +++ b/test/features/steps/crud_table.py @@ -1,11 +1,9 @@ -# -*- coding: utf-8 """Steps for behavioral style tests are defined in this module. Each step is defined by the string decorating it. This string is used to call the step in "*.feature" file. """ -from __future__ import unicode_literals import wrappers from behave import when, then diff --git a/test/features/steps/iocommands.py b/test/features/steps/iocommands.py index 2f3efb31..bbabf431 100644 --- a/test/features/steps/iocommands.py +++ b/test/features/steps/iocommands.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -from __future__ import unicode_literals import os import wrappers diff --git a/test/features/steps/named_queries.py b/test/features/steps/named_queries.py index b82d5f4c..bc1f8663 100644 --- a/test/features/steps/named_queries.py +++ b/test/features/steps/named_queries.py @@ -1,11 +1,9 @@ -# -*- coding: utf-8 """Steps for behavioral style tests are defined in this module. Each step is defined by the string decorating it. This string is used to call the step in "*.feature" file. """ -from __future__ import unicode_literals import wrappers from behave import when, then diff --git a/test/features/steps/specials.py b/test/features/steps/specials.py index 924535fc..e8b99e3e 100644 --- a/test/features/steps/specials.py +++ b/test/features/steps/specials.py @@ -1,11 +1,9 @@ -# -*- coding: utf-8 """Steps for behavioral style tests are defined in this module. Each step is defined by the string decorating it. This string is used to call the step in "*.feature" file. """ -from __future__ import unicode_literals import wrappers from behave import when, then diff --git a/test/features/steps/wrappers.py b/test/features/steps/wrappers.py index 763aadf9..565ca591 100644 --- a/test/features/steps/wrappers.py +++ b/test/features/steps/wrappers.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -from __future__ import unicode_literals - import re import pexpect import sys diff --git a/test/test_clistyle.py b/test/test_clistyle.py index e18a5303..f82cdf0e 100644 --- a/test/test_clistyle.py +++ b/test/test_clistyle.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Test the mycli.clistyle module.""" import pytest diff --git a/test/test_main.py b/test/test_main.py index 658629f9..47395dfd 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -13,10 +13,6 @@ from tempfile import NamedTemporaryFile from textwrap import dedent -try: - text_type = basestring -except NameError: - text_type = str test_dir = os.path.abspath(os.path.dirname(__file__)) project_dir = os.path.dirname(test_dir) @@ -148,7 +144,7 @@ def test_thanks_picker_utf8(): sponsor_file = os.path.join(PACKAGE_ROOT, 'SPONSORS') name = thanks_picker((author_file, sponsor_file)) - assert isinstance(name, text_type) + assert name and isinstance(name, str) def test_help_strings_end_with_periods(): diff --git a/test/test_naive_completion.py b/test/test_naive_completion.py index 908f9ffe..14c1bf5a 100644 --- a/test/test_naive_completion.py +++ b/test/test_naive_completion.py @@ -1,4 +1,3 @@ -from __future__ import unicode_literals import pytest from prompt_toolkit.completion import Completion from prompt_toolkit.document import Document diff --git a/test/test_prompt_utils.py b/test/test_prompt_utils.py index 1838f580..2373fac8 100644 --- a/test/test_prompt_utils.py +++ b/test/test_prompt_utils.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- - - import click from mycli.packages.prompt_utils import confirm_destructive_query diff --git a/test/test_smart_completion_public_schema_only.py b/test/test_smart_completion_public_schema_only.py index 0fda3faa..b66c696b 100644 --- a/test/test_smart_completion_public_schema_only.py +++ b/test/test_smart_completion_public_schema_only.py @@ -1,5 +1,3 @@ -# coding: utf-8 -from __future__ import unicode_literals import pytest from mock import patch from prompt_toolkit.completion import Completion diff --git a/test/test_special_iocommands.py b/test/test_special_iocommands.py index e6d3fc8b..b8b3acb3 100644 --- a/test/test_special_iocommands.py +++ b/test/test_special_iocommands.py @@ -1,6 +1,3 @@ -# coding: utf-8 -from __future__ import unicode_literals - import os import stat import tempfile diff --git a/test/test_sqlexecute.py b/test/test_sqlexecute.py index f73c8670..e445166f 100644 --- a/test/test_sqlexecute.py +++ b/test/test_sqlexecute.py @@ -1,5 +1,3 @@ -# coding=UTF-8 - import os import pytest diff --git a/test/test_tabular_output.py b/test/test_tabular_output.py index 9e854861..501198f3 100644 --- a/test/test_tabular_output.py +++ b/test/test_tabular_output.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """Test the sql output adapter.""" -from __future__ import unicode_literals from textwrap import dedent from mycli.packages.tabular_output import sql_format diff --git a/test/utils.py b/test/utils.py index dc7b9de5..57669658 100644 --- a/test/utils.py +++ b/test/utils.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- - - import os import time import signal diff --git a/tox.ini b/tox.ini index 630e59a8..4054c6c8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27, py34, py35, py36, py37 +envlist = py36, py37 [testenv] deps = pytest From 1c07f33b50ce439c9ea3de0f28374d9fd3f49171 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Wed, 18 Mar 2020 19:09:02 +0300 Subject: [PATCH 044/202] support python 3.8 (#837) --- .travis.yml | 1 + changelog.md | 2 +- setup.py | 1 + tox.ini | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1ea67b2a..0afb5cc2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: python python: - "3.6" - "3.7" + - "3.8" matrix: include: diff --git a/changelog.md b/changelog.md index 8ea28218..1b07ccb0 100644 --- a/changelog.md +++ b/changelog.md @@ -16,7 +16,7 @@ Bug Fixes: Internal: --------- -* deprecate python versions 2.7, 3.4, 3.5 +* deprecate python versions 2.7, 3.4, 3.5; support python 3.8 1.20.1 ====== diff --git a/setup.py b/setup.py index a2ece1d1..bd332a60 100755 --- a/setup.py +++ b/setup.py @@ -102,6 +102,7 @@ def run_tests(self): 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Programming Language :: SQL', 'Topic :: Database', 'Topic :: Database :: Front-Ends', diff --git a/tox.ini b/tox.ini index 4054c6c8..612e8b7f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py36, py37 +envlist = py36, py37, py38 [testenv] deps = pytest From e671e4154ae593f384e57e90a35defb606226c48 Mon Sep 17 00:00:00 2001 From: laixintao Date: Thu, 19 Mar 2020 02:59:56 +0800 Subject: [PATCH 045/202] coverage.py is broken on python3.8, upgrade to the latest version. --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 69e52d07..b8b79a47 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,7 +5,7 @@ tox twine==1.12.1 behave pexpect -coverage==4.3.4 +coverage==5.0.4 codecov==2.0.9 autopep8==1.3.3 colorama==0.4.1 From bec165ad2dccd9d7fd338b398b9669fc4350a92c Mon Sep 17 00:00:00 2001 From: Nathan Huang Date: Wed, 15 Apr 2020 12:25:23 +0800 Subject: [PATCH 046/202] add list ssh config --- mycli/main.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/mycli/main.py b/mycli/main.py index ee0fd54e..98e1ca9a 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -980,6 +980,8 @@ def get_last_query(self): @click.option('--ssh-port', default=22, help='Port to connect to ssh server.') @click.option('--ssh-password', help='Password to connect to ssh server.') @click.option('--ssh-key-filename', help='Private key filename (identify file) for the ssh connection.') +@click.option('--ssh-config-path', help='Path to ssh configuration.', + default=os.getenv('HOME') + '/.ssh/config') @click.option('--ssl-ca', help='CA file in PEM format.', type=click.Path(exists=True)) @click.option('--ssl-capath', help='CA directory.') @@ -1001,6 +1003,8 @@ def get_last_query(self): help='Use DSN configured into the [alias_dsn] section of myclirc file.') @click.option('--list-dsn', 'list_dsn', is_flag=True, help='list of DSN configured into the [alias_dsn] section of myclirc file.') +@click.option('--list-ssh-config', 'list_ssh_config', is_flag=True, + help='list of ssh configuration in the ssh config.') @click.option('-R', '--prompt', 'prompt', help='Prompt format (Default: "{0}").'.format( MyCli.default_prompt)) @@ -1033,7 +1037,7 @@ def cli(database, user, host, port, socket, password, dbname, ssl_ca, ssl_capath, ssl_cert, ssl_key, ssl_cipher, ssl_verify_server_cert, table, csv, warn, execute, myclirc, dsn, list_dsn, ssh_user, ssh_host, ssh_port, ssh_password, - ssh_key_filename): + ssh_key_filename, list_ssh_config, ssh_config_path): """A MySQL terminal client with auto-completion and syntax highlighting. \b @@ -1070,6 +1074,31 @@ def cli(database, user, host, port, socket, password, dbname, else: click.secho(alias) sys.exit(0) + if list_ssh_config: + if not paramiko: + click.secho( + "Cannot use SSH transport because paramiko isn't installed, " + "please install paramiko or don't use --list-ssh-config=", + err=True, fg='red' + ) + exit(1) + try: + ssh_config = paramiko.config.SSHConfig().from_path(ssh_config_path) + except paramiko.ssh_exception.ConfigParseError as err: + click.secho('Invalid SSH configuration file. '\ + 'Please check the SSH configuration file.', + err=True, fg='red') + exit(1) + except FileNotFoundError as e: + click.secho(str(e), err=True, fg='red') + exit(1) + for host in ssh_config.get_hostnames(): + if verbose: + host_config = ssh_config.lookup(host) + click.secho("{} : {}".format(host, host_config.get('hostname'))) + else: + click.secho(host) + sys.exit(0) # Choose which ever one has a valid value. database = dbname or database From e09bbe2220bea0e9a754fc9e574ad1f3097a4634 Mon Sep 17 00:00:00 2001 From: Nathan Huang Date: Wed, 15 Apr 2020 14:58:25 +0800 Subject: [PATCH 047/202] add read from ssh config --- mycli/main.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/mycli/main.py b/mycli/main.py index 98e1ca9a..b440c17e 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -982,6 +982,7 @@ def get_last_query(self): @click.option('--ssh-key-filename', help='Private key filename (identify file) for the ssh connection.') @click.option('--ssh-config-path', help='Path to ssh configuration.', default=os.getenv('HOME') + '/.ssh/config') +@click.option('--ssh-config-host', help='Host to connect to ssh server reading from ssh configuration.') @click.option('--ssl-ca', help='CA file in PEM format.', type=click.Path(exists=True)) @click.option('--ssl-capath', help='CA directory.') @@ -1037,7 +1038,7 @@ def cli(database, user, host, port, socket, password, dbname, ssl_ca, ssl_capath, ssl_cert, ssl_key, ssl_cipher, ssl_verify_server_cert, table, csv, warn, execute, myclirc, dsn, list_dsn, ssh_user, ssh_host, ssh_port, ssh_password, - ssh_key_filename, list_ssh_config, ssh_config_path): + ssh_key_filename, list_ssh_config, ssh_config_path, ssh_config_host): """A MySQL terminal client with auto-completion and syntax highlighting. \b @@ -1149,6 +1150,33 @@ def cli(database, user, host, port, socket, password, dbname, if not port: port = uri.port + if ssh_config_host: + if not paramiko: + click.secho( + "Cannot use SSH transport because paramiko isn't installed, " + "please install paramiko or don't use --ssh-config_host=", + err=True, fg='red' + ) + exit(1) + ssh_config = paramiko.config.SSHConfig().from_path(ssh_config_path) + try: + ssh_config = paramiko.config.SSHConfig().from_path(ssh_config_path) + except paramiko.ssh_exception.ConfigParseError as err: + click.secho('Invalid SSH configuration file. '\ + 'Please check the SSH configuration file.', + err=True, fg='red') + exit(1) + except FileNotFoundError as e: + click.secho(str(e), err=True, fg='red') + exit(1) + ssh_config = ssh_config.lookup(ssh_config_host) + ssh_host = ssh_host if ssh_host else ssh_config.get('hostname') + ssh_user = ssh_user if ssh_user else ssh_config.get('user') + if ssh_config.get('port'): + # port has a default value, overwrite it if it's in the config + ssh_port = int(ssh_config.get('port')) + ssh_key_filename = ssh_key_filename if ssh_key_filename else ssh_config.get('identityfile', [''])[0] + if not paramiko and ssh_host: click.secho( "Cannot use SSH transport because paramiko isn't installed, " From 44459905374219520e9f1c55e0d7180feac8c60a Mon Sep 17 00:00:00 2001 From: Nathan Huang Date: Wed, 15 Apr 2020 14:58:56 +0800 Subject: [PATCH 048/202] add testing for list ssh config --- test/test_main.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/test_main.py b/test/test_main.py index 47395dfd..c8c0997d 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -283,6 +283,24 @@ def test_list_dsn(): assert result.output == "test : mysql://test/test\n" +def test_list_ssh_config(): + runner = CliRunner() + with NamedTemporaryFile(mode="w") as ssh_config: + ssh_config.write(dedent("""\ + Host test + Hostname test.example.com + User joe + Port 22222 + IdentityFile ~/.ssh/gateway + """)) + ssh_config.flush() + args = ['--list-ssh-config', '--ssh-config-path', ssh_config.name] + result = runner.invoke(cli, args=args) + assert "test\n" in result.output + result = runner.invoke(cli, args=args + ['--verbose']) + assert "test : test.example.com\n" in result.output + + def test_dsn(monkeypatch): # Setup classes to mock mycli.main.MyCli class Formatter: From d425154355bbcfc5b3dd0a7a60f0d3b07cdc18e4 Mon Sep 17 00:00:00 2001 From: Nathan Huang Date: Wed, 15 Apr 2020 15:03:23 +0800 Subject: [PATCH 049/202] only overwrite ssh port if it's default port --- mycli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/main.py b/mycli/main.py index b440c17e..6f800a96 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1172,7 +1172,7 @@ def cli(database, user, host, port, socket, password, dbname, ssh_config = ssh_config.lookup(ssh_config_host) ssh_host = ssh_host if ssh_host else ssh_config.get('hostname') ssh_user = ssh_user if ssh_user else ssh_config.get('user') - if ssh_config.get('port'): + if ssh_config.get('port') and ssh_port != 22: # port has a default value, overwrite it if it's in the config ssh_port = int(ssh_config.get('port')) ssh_key_filename = ssh_key_filename if ssh_key_filename else ssh_config.get('identityfile', [''])[0] From 1665b66d0ee88aab85e20eac43a67e7cd3312d15 Mon Sep 17 00:00:00 2001 From: Nathan Huang Date: Wed, 15 Apr 2020 15:36:48 +0800 Subject: [PATCH 050/202] fix typo --- mycli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/main.py b/mycli/main.py index 6f800a96..c400ced2 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1172,7 +1172,7 @@ def cli(database, user, host, port, socket, password, dbname, ssh_config = ssh_config.lookup(ssh_config_host) ssh_host = ssh_host if ssh_host else ssh_config.get('hostname') ssh_user = ssh_user if ssh_user else ssh_config.get('user') - if ssh_config.get('port') and ssh_port != 22: + if ssh_config.get('port') and ssh_port == 22: # port has a default value, overwrite it if it's in the config ssh_port = int(ssh_config.get('port')) ssh_key_filename = ssh_key_filename if ssh_key_filename else ssh_config.get('identityfile', [''])[0] From d1f7f4be23383d4954a4e5272cfc17a5ad8177b8 Mon Sep 17 00:00:00 2001 From: Nathan Huang Date: Wed, 15 Apr 2020 15:43:23 +0800 Subject: [PATCH 051/202] add testing for ssh config --- test/test_main.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/test/test_main.py b/test/test_main.py index c8c0997d..36d19838 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -413,3 +413,73 @@ def run_query(self, query, new_line=True): MockMyCli.connect_args["host"] == "dsn_host" and \ MockMyCli.connect_args["port"] == 6 and \ MockMyCli.connect_args["database"] == "dsn_database" + + +def test_ssh_config(monkeypatch): + # Setup classes to mock mycli.main.MyCli + class Formatter: + format_name = None + class Logger: + def debug(self, *args, **args_dict): + pass + def warning(self, *args, **args_dict): + pass + class MockMyCli: + config = {'alias_dsn': {}} + def __init__(self, **args): + self.logger = Logger() + self.destructive_warning = False + self.formatter = Formatter() + def connect(self, **args): + MockMyCli.connect_args = args + def run_query(self, query, new_line=True): + pass + + import mycli.main + monkeypatch.setattr(mycli.main, 'MyCli', MockMyCli) + runner = CliRunner() + + # Setup temporary configuration + with NamedTemporaryFile(mode="w") as ssh_config: + ssh_config.write(dedent("""\ + Host test + Hostname test.example.com + User joe + Port 22222 + IdentityFile ~/.ssh/gateway + """)) + ssh_config.flush() + + # When a user supplies a ssh config. + result = runner.invoke(mycli.main.cli, args=[ + "--ssh-config-path", + ssh_config.name, + "--ssh-config-host", + "test" + ]) + assert result.exit_code == 0, result.output + " " + str(result.exception) + assert \ + MockMyCli.connect_args["ssh_user"] == "joe" and \ + MockMyCli.connect_args["ssh_host"] == "test.example.com" and \ + MockMyCli.connect_args["ssh_port"] == 22222 and \ + MockMyCli.connect_args["ssh_key_filename"] == os.getenv("HOME") + "/.ssh/gateway" + + # When a user supplies a ssh config host as argument to mycli, + # and used command line arguments, use the command line + # arguments. + result = runner.invoke(mycli.main.cli, args=[ + "--ssh-config-path", + ssh_config.name, + "--ssh-config-host", + "test", + "--ssh-user", "arg_user", + "--ssh-host", "arg_host", + "--ssh-port", "3", + "--ssh-key-filename", "/path/to/key" + ]) + assert result.exit_code == 0, result.output + " " + str(result.exception) + assert \ + MockMyCli.connect_args["ssh_user"] == "arg_user" and \ + MockMyCli.connect_args["ssh_host"] == "arg_host" and \ + MockMyCli.connect_args["ssh_port"] == 3 and \ + MockMyCli.connect_args["ssh_key_filename"] == "/path/to/key" From 4c1d8d925d8de61330c9e675a7c2fab33be51770 Mon Sep 17 00:00:00 2001 From: Nathan Huang Date: Wed, 15 Apr 2020 15:46:09 +0800 Subject: [PATCH 052/202] fix typo --- mycli/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index c400ced2..cacdf83d 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1005,7 +1005,7 @@ def get_last_query(self): @click.option('--list-dsn', 'list_dsn', is_flag=True, help='list of DSN configured into the [alias_dsn] section of myclirc file.') @click.option('--list-ssh-config', 'list_ssh_config', is_flag=True, - help='list of ssh configuration in the ssh config.') + help='list of ssh configurations in the ssh config.') @click.option('-R', '--prompt', 'prompt', help='Prompt format (Default: "{0}").'.format( MyCli.default_prompt)) @@ -1158,7 +1158,6 @@ def cli(database, user, host, port, socket, password, dbname, err=True, fg='red' ) exit(1) - ssh_config = paramiko.config.SSHConfig().from_path(ssh_config_path) try: ssh_config = paramiko.config.SSHConfig().from_path(ssh_config_path) except paramiko.ssh_exception.ConfigParseError as err: From 283532fa5cf42abe0cb5a722be6094e7bd760d06 Mon Sep 17 00:00:00 2001 From: Nathan Huang Date: Wed, 15 Apr 2020 15:50:28 +0800 Subject: [PATCH 053/202] modify readme and AUTHORS --- README.md | 4 +++- mycli/AUTHORS | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b2426db5..1ad3f444 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A command line client for MySQL that can do auto-completion and syntax highlighting. -HomePage: [http://mycli.net](http://mycli.net) +HomePage: [http://mycli.net](http://mycli.net) Documentation: [http://mycli.net/docs](http://mycli.net/docs) ![Completion](screenshots/tables.png) @@ -63,6 +63,8 @@ $ sudo apt-get install mycli # Only on debian or ubuntu --ssh-password TEXT Password to connect to ssh server. --ssh-key-filename TEXT Private key filename (identify file) for the ssh connection. + --ssh-config-path TEXT Path to ssh configuation + --ssh-config-host TEXT Host for ssh server in ssh configuations --ssl-ca PATH CA file in PEM format. --ssl-capath TEXT CA directory. --ssl-cert PATH X509 cert in PEM format. diff --git a/mycli/AUTHORS b/mycli/AUTHORS index 36b9818c..f065e2c4 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -68,6 +68,7 @@ Contributors: * Mike Palandra * Georgy Frolov * Jonathan Lloyd + * Nathan Huang Creator: -------- From 9f7800d2e5244cb34e8c294ccd1f512a2721ada7 Mon Sep 17 00:00:00 2001 From: Nathan Huang Date: Wed, 15 Apr 2020 16:35:22 +0800 Subject: [PATCH 054/202] add changelog --- changelog.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/changelog.md b/changelog.md index 1b07ccb0..76659d4e 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,16 @@ TBD === +Features: +--------- +* Add an option `--ssh-config-host` to read ssh configuration from OpenSSH configuration file. +* Add an option `--list-ssh-config` to list ssh configurations. +* Add an option `--ssh-config-path` to choose ssh configuration path. +--------- + +1.20.2 +====== + Features: --------- * Added DSN alias name as a format specifier to the prompt (Thanks: [Georgy Frolov]). From 9ee3fa28e8308d96aee3a8b606fcb248d5a736f1 Mon Sep 17 00:00:00 2001 From: Nathan Huang Date: Wed, 15 Apr 2020 16:39:09 +0800 Subject: [PATCH 055/202] modify readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1ad3f444..d8174bd7 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ $ sudo apt-get install mycli # Only on debian or ubuntu section of myclirc file. --list-dsn list of DSN configured into the [alias_dsn] section of myclirc file. + --list-ssh-config list of ssh configuration in the ssh config. -R, --prompt TEXT Prompt format (Default: "\t \u@\h:\d> "). -l, --logfile FILENAME Log every query and its results to a file. --defaults-group-suffix TEXT Read MySQL config groups with the specified From dc5867811584254b9dc68b502fc234b6d6639459 Mon Sep 17 00:00:00 2001 From: Nathan Huang Date: Wed, 15 Apr 2020 16:53:41 +0800 Subject: [PATCH 056/202] add new package for developing --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 69e52d07..faea79fd 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,3 +11,4 @@ autopep8==1.3.3 colorama==0.4.1 git+https://github.com/hayd/pep8radius.git # --error-status option not released click>=7.0 +paramiko==2.7.1 From bd5d5a68528f373eb3b9701bbb40be13066702e6 Mon Sep 17 00:00:00 2001 From: Nathan Huang Date: Wed, 15 Apr 2020 17:04:18 +0800 Subject: [PATCH 057/202] lint --- mycli/main.py | 16 +++++++++------- test/test_main.py | 9 ++++++--- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index cacdf83d..ba017136 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1005,7 +1005,7 @@ def get_last_query(self): @click.option('--list-dsn', 'list_dsn', is_flag=True, help='list of DSN configured into the [alias_dsn] section of myclirc file.') @click.option('--list-ssh-config', 'list_ssh_config', is_flag=True, - help='list of ssh configurations in the ssh config.') + help='list of ssh configurations in the ssh config.') @click.option('-R', '--prompt', 'prompt', help='Prompt format (Default: "{0}").'.format( MyCli.default_prompt)) @@ -1087,8 +1087,8 @@ def cli(database, user, host, port, socket, password, dbname, ssh_config = paramiko.config.SSHConfig().from_path(ssh_config_path) except paramiko.ssh_exception.ConfigParseError as err: click.secho('Invalid SSH configuration file. '\ - 'Please check the SSH configuration file.', - err=True, fg='red') + 'Please check the SSH configuration file.', + err=True, fg='red') exit(1) except FileNotFoundError as e: click.secho(str(e), err=True, fg='red') @@ -1096,7 +1096,8 @@ def cli(database, user, host, port, socket, password, dbname, for host in ssh_config.get_hostnames(): if verbose: host_config = ssh_config.lookup(host) - click.secho("{} : {}".format(host, host_config.get('hostname'))) + click.secho("{} : {}".format( + host, host_config.get('hostname'))) else: click.secho(host) sys.exit(0) @@ -1162,8 +1163,8 @@ def cli(database, user, host, port, socket, password, dbname, ssh_config = paramiko.config.SSHConfig().from_path(ssh_config_path) except paramiko.ssh_exception.ConfigParseError as err: click.secho('Invalid SSH configuration file. '\ - 'Please check the SSH configuration file.', - err=True, fg='red') + 'Please check the SSH configuration file.', + err=True, fg='red') exit(1) except FileNotFoundError as e: click.secho(str(e), err=True, fg='red') @@ -1174,7 +1175,8 @@ def cli(database, user, host, port, socket, password, dbname, if ssh_config.get('port') and ssh_port == 22: # port has a default value, overwrite it if it's in the config ssh_port = int(ssh_config.get('port')) - ssh_key_filename = ssh_key_filename if ssh_key_filename else ssh_config.get('identityfile', [''])[0] + ssh_key_filename = ssh_key_filename if ssh_key_filename else ssh_config.get( + 'identityfile', [''])[0] if not paramiko and ssh_host: click.secho( diff --git a/test/test_main.py b/test/test_main.py index 36d19838..5f3c08b5 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -457,12 +457,14 @@ def run_query(self, query, new_line=True): "--ssh-config-host", "test" ]) - assert result.exit_code == 0, result.output + " " + str(result.exception) + assert result.exit_code == 0, result.output + \ + " " + str(result.exception) assert \ MockMyCli.connect_args["ssh_user"] == "joe" and \ MockMyCli.connect_args["ssh_host"] == "test.example.com" and \ MockMyCli.connect_args["ssh_port"] == 22222 and \ - MockMyCli.connect_args["ssh_key_filename"] == os.getenv("HOME") + "/.ssh/gateway" + MockMyCli.connect_args["ssh_key_filename"] == os.getenv( + "HOME") + "/.ssh/gateway" # When a user supplies a ssh config host as argument to mycli, # and used command line arguments, use the command line @@ -477,7 +479,8 @@ def run_query(self, query, new_line=True): "--ssh-port", "3", "--ssh-key-filename", "/path/to/key" ]) - assert result.exit_code == 0, result.output + " " + str(result.exception) + assert result.exit_code == 0, result.output + \ + " " + str(result.exception) assert \ MockMyCli.connect_args["ssh_user"] == "arg_user" and \ MockMyCli.connect_args["ssh_host"] == "arg_host" and \ From d688868647f09a8384bde1c8cc9343dc278017b5 Mon Sep 17 00:00:00 2001 From: Nathan Huang Date: Wed, 15 Apr 2020 17:07:43 +0800 Subject: [PATCH 058/202] fix typo --- mycli/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index ba017136..5a109a8a 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1086,7 +1086,7 @@ def cli(database, user, host, port, socket, password, dbname, try: ssh_config = paramiko.config.SSHConfig().from_path(ssh_config_path) except paramiko.ssh_exception.ConfigParseError as err: - click.secho('Invalid SSH configuration file. '\ + click.secho('Invalid SSH configuration file. ' 'Please check the SSH configuration file.', err=True, fg='red') exit(1) @@ -1162,7 +1162,7 @@ def cli(database, user, host, port, socket, password, dbname, try: ssh_config = paramiko.config.SSHConfig().from_path(ssh_config_path) except paramiko.ssh_exception.ConfigParseError as err: - click.secho('Invalid SSH configuration file. '\ + click.secho('Invalid SSH configuration file. ' 'Please check the SSH configuration file.', err=True, fg='red') exit(1) From 5318c31aef859629249e8d6dcd8c5e9382fde77a Mon Sep 17 00:00:00 2001 From: Nathan Huang Date: Wed, 15 Apr 2020 17:12:00 +0800 Subject: [PATCH 059/202] lint --- test/test_main.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/test_main.py b/test/test_main.py index 5f3c08b5..3f92bd1b 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -419,19 +419,25 @@ def test_ssh_config(monkeypatch): # Setup classes to mock mycli.main.MyCli class Formatter: format_name = None + class Logger: def debug(self, *args, **args_dict): pass + def warning(self, *args, **args_dict): pass + class MockMyCli: config = {'alias_dsn': {}} + def __init__(self, **args): self.logger = Logger() self.destructive_warning = False self.formatter = Formatter() + def connect(self, **args): MockMyCli.connect_args = args + def run_query(self, query, new_line=True): pass From 6eb0caa2141085c66fe9cd209ad3982bb52830a3 Mon Sep 17 00:00:00 2001 From: laixintao Date: Thu, 16 Apr 2020 20:29:45 +0800 Subject: [PATCH 060/202] add myself to authors. --- mycli/AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/mycli/AUTHORS b/mycli/AUTHORS index 36b9818c..08fe984a 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -68,6 +68,7 @@ Contributors: * Mike Palandra * Georgy Frolov * Jonathan Lloyd + * laixintao Creator: -------- From 918cd6f6aae3d524a5775a405d51fb5943ec6045 Mon Sep 17 00:00:00 2001 From: Nathan Huang Date: Fri, 17 Apr 2020 17:21:44 +0800 Subject: [PATCH 061/202] update cross platform adjust --- mycli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/main.py b/mycli/main.py index 5a109a8a..0ef7a1ac 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -981,7 +981,7 @@ def get_last_query(self): @click.option('--ssh-password', help='Password to connect to ssh server.') @click.option('--ssh-key-filename', help='Private key filename (identify file) for the ssh connection.') @click.option('--ssh-config-path', help='Path to ssh configuration.', - default=os.getenv('HOME') + '/.ssh/config') + default=os.path.expanduser('~') + '/.ssh/config') @click.option('--ssh-config-host', help='Host to connect to ssh server reading from ssh configuration.') @click.option('--ssl-ca', help='CA file in PEM format.', type=click.Path(exists=True)) From ff136c09f99cddf12fbba5a03a18ccce45513870 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Fri, 17 Apr 2020 12:42:42 -0700 Subject: [PATCH 062/202] Update release.py to remove unnecessary checks. --- release.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/release.py b/release.py index ef84927d..30c41b3f 100755 --- a/release.py +++ b/release.py @@ -90,12 +90,6 @@ def checklist(questions): if DEBUG: subprocess.check_output = lambda x: x - checks = ['Have you created the debian package?', - 'Have you updated the AUTHORS file?', - 'Have you updated the `Usage` section of the README?', - ] - checklist(checks) - ver = version('mycli/__init__.py') print('Releasing Version:', ver) From 01b7cd70bd655817e0df61bf05de85cfe23afa43 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Fri, 17 Apr 2020 12:43:29 -0700 Subject: [PATCH 063/202] Update changelog for 1.21.0. --- changelog.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 1b07ccb0..c76883f6 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,5 @@ -TBD -=== +1.21.0 +====== Features: --------- From 03b2ab83402f162de92a1e2c407c9d6d1ac3f9df Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Fri, 17 Apr 2020 12:43:52 -0700 Subject: [PATCH 064/202] Releasing version 1.21.0 --- mycli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/__init__.py b/mycli/__init__.py index d1f2def7..b74f54f8 100644 --- a/mycli/__init__.py +++ b/mycli/__init__.py @@ -1 +1 @@ -__version__ = '1.20.1' +__version__ = '1.21.0' From 7daa136c574ac4c82b6c15d4d35d3a01f409c026 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Fri, 17 Apr 2020 13:49:25 -0700 Subject: [PATCH 065/202] Use the multiline continuation char from config. --- mycli/main.py | 4 ++-- mycli/myclirc | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index ee0fd54e..cdeba42c 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -161,7 +161,7 @@ def __init__(self, sqlexecute=None, prompt=None, prompt_cnf = self.read_my_cnf_files(self.cnf_files, ['prompt'])['prompt'] self.prompt_format = prompt or prompt_cnf or c['main']['prompt'] or \ self.default_prompt - self.prompt_continuation_format = c['main']['prompt_continuation'] + self.multiline_continuation_char = c['main']['prompt_continuation'] keyword_casing = c['main'].get('keyword_casing', 'auto') self.query_history = [] @@ -539,7 +539,7 @@ def get_message(): return [('class:prompt', prompt)] def get_continuation(width, line_number, is_soft_wrap): - continuation = ' ' * (width - 1) + ' ' + continuation = self.multiline_continuation_char * (width - 1) + ' ' return [('class:continuation', continuation)] def show_suggestion_tip(): diff --git a/mycli/myclirc b/mycli/myclirc index 31270d9d..cdf769f2 100644 --- a/mycli/myclirc +++ b/mycli/myclirc @@ -66,7 +66,7 @@ wider_completion_menu = False # \A - DSN alias name (from the [alias_dsn] section) # \u - Username prompt = '\t \u@\h:\d> ' -prompt_continuation = '-> ' +prompt_continuation = ' ' # Skip intro info on startup and outro info on exit less_chatty = False From 723256a412bd51cf0f085224ff0672dbe89bc8bb Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Fri, 17 Apr 2020 20:43:43 -0700 Subject: [PATCH 066/202] Fix favorites completion (#846) * Make FavoriteQueries a singleton and remove globals. * Add a test for special command completion. * Add changelog. * Fix typo. * Lint fixes. --- changelog.md | 13 ++++++++++++ mycli/main.py | 3 ++- mycli/packages/special/favoritequeries.py | 7 ++++++ mycli/packages/special/iocommands.py | 26 +++++++++-------------- mycli/sqlcompleter.py | 4 ++-- test/test_completion_engine.py | 8 ++++++- 6 files changed, 41 insertions(+), 20 deletions(-) diff --git a/changelog.md b/changelog.md index c76883f6..f69f6597 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,16 @@ +TBD +=== + +Features: +--------- + + +Bug Fixes: +---------- + +* Fix broken auto-completion for favorite queries (Thanks: [Amjith]). + + 1.21.0 ====== diff --git a/mycli/main.py b/mycli/main.py index ee0fd54e..baff3cfb 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -34,6 +34,7 @@ from .packages.prompt_utils import confirm, confirm_destructive_query from .packages.tabular_output import sql_format from .packages import special +from .packages.special.favoritequeries import FavoriteQueries from .sqlcompleter import SQLCompleter from .clitoolbar import create_toolbar_tokens_func from .clistyle import style_factory, style_factory_output @@ -117,7 +118,7 @@ def __init__(self, sqlexecute=None, prompt=None, self.key_bindings = c['main']['key_bindings'] special.set_timing_enabled(c['main'].as_bool('timing')) - special.set_favorite_queries(self.config) + FavoriteQueries.instance = FavoriteQueries.from_config(self.config) self.dsn_alias = None self.formatter = TabularOutputFormatter( diff --git a/mycli/packages/special/favoritequeries.py b/mycli/packages/special/favoritequeries.py index 65248a76..0b91400e 100644 --- a/mycli/packages/special/favoritequeries.py +++ b/mycli/packages/special/favoritequeries.py @@ -31,9 +31,16 @@ class FavoriteQueries(object): simple: Deleted ''' + # Class-level variable, for convenience to use as a singleton. + instance = None + def __init__(self, config): self.config = config + @classmethod + def from_config(cls, config): + return FavoriteQueries(config) + def list(self): return self.config.get(self.section_name, []) diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index c58310fb..11dca8d0 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -9,7 +9,6 @@ import click import sqlparse -from configobj import ConfigObj from . import export from .main import special_command, NO_QUERY, PARSED_QUERY @@ -24,7 +23,6 @@ tee_file = None once_file = None written_to_once_file = False -favoritequeries = FavoriteQueries(ConfigObj()) delimiter_command = DelimiterCommand() @@ -39,11 +37,6 @@ def set_pager_enabled(val): PAGER_ENABLED = val -@export -def set_favorite_queries(config): - global favoritequeries - favoritequeries = FavoriteQueries(config) - @export def is_pager_enabled(): return PAGER_ENABLED @@ -177,7 +170,7 @@ def execute_favorite_query(cur, arg, **_): name, _, arg_str = arg.partition(' ') args = shlex.split(arg_str) - query = favoritequeries.get(name) + query = FavoriteQueries.instance.get(name) if query is None: message = "No favorite query: %s" % (name) yield (None, None, None, message) @@ -201,10 +194,11 @@ def list_favorite_queries(): Returns (title, rows, headers, status)""" headers = ["Name", "Query"] - rows = [(r, favoritequeries.get(r)) for r in favoritequeries.list()] + rows = [(r, FavoriteQueries.instance.get(r)) + for r in FavoriteQueries.instance.list()] if not rows: - status = '\nNo favorite queries found.' + favoritequeries.usage + status = '\nNo favorite queries found.' + FavoriteQueries.instance.usage else: status = '' return [('', rows, headers, status)] @@ -230,7 +224,7 @@ def save_favorite_query(arg, **_): """Save a new favorite query. Returns (title, rows, headers, status)""" - usage = 'Syntax: \\fs name query.\n\n' + favoritequeries.usage + usage = 'Syntax: \\fs name query.\n\n' + FavoriteQueries.instance.usage if not arg: return [(None, None, None, usage)] @@ -241,18 +235,18 @@ def save_favorite_query(arg, **_): return [(None, None, None, usage + 'Err: Both name and query are required.')] - favoritequeries.save(name, query) + FavoriteQueries.instance.save(name, query) return [(None, None, None, "Saved.")] + @special_command('\\fd', '\\fd [name]', 'Delete a favorite query.') def delete_favorite_query(arg, **_): - """Delete an existing favorite query. - """ - usage = 'Syntax: \\fd name.\n\n' + favoritequeries.usage + """Delete an existing favorite query.""" + usage = 'Syntax: \\fd name.\n\n' + FavoriteQueries.instance.usage if not arg: return [(None, None, None, usage)] - status = favoritequeries.delete(arg) + status = FavoriteQueries.instance.delete(arg) return [(None, None, None, status)] diff --git a/mycli/sqlcompleter.py b/mycli/sqlcompleter.py index 36b4934e..20611be6 100644 --- a/mycli/sqlcompleter.py +++ b/mycli/sqlcompleter.py @@ -7,7 +7,7 @@ from .packages.completion_engine import suggest_type from .packages.parseutils import last_word from .packages.filepaths import parse_path, complete_path, suggest_path -from .packages.special.iocommands import favoritequeries +from .packages.special.favoritequeries import FavoriteQueries _logger = logging.getLogger(__name__) @@ -355,7 +355,7 @@ def get_completions(self, document, complete_event, smart_completion=None): completions.extend(special) elif suggestion['type'] == 'favoritequery': queries = self.find_matches(word_before_cursor, - favoritequeries.list(), + FavoriteQueries.instance.list(), start_only=False, fuzzy=True) completions.extend(queries) elif suggestion['type'] == 'table_format': diff --git a/test/test_completion_engine.py b/test/test_completion_engine.py index 98b89e1e..9e7c608b 100644 --- a/test/test_completion_engine.py +++ b/test/test_completion_engine.py @@ -1,5 +1,4 @@ from mycli.packages.completion_engine import suggest_type -import os import pytest @@ -525,6 +524,13 @@ def test_source_is_file(expression): assert suggestions == [{'type': 'file_name'}] +@pytest.mark.parametrize("expression", [ + "\\f ", +]) +def test_favorite_name_suggestion(expression): + suggestions = suggest_type(expression, expression) + assert suggestions == [{'type': 'favoritequery'}] + def test_order_by(): text = 'select * from foo order by ' suggestions = suggest_type(text, text) From e70935c4753a447838461ef11afde4e0c064dfa0 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sat, 18 Apr 2020 19:02:43 -0700 Subject: [PATCH 067/202] Add left padding to continuation prompt char. --- mycli/main.py | 8 ++++++-- mycli/myclirc | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index cdeba42c..7a4bab2f 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -538,8 +538,12 @@ def get_message(): prompt = self.get_prompt('\\d> ') return [('class:prompt', prompt)] - def get_continuation(width, line_number, is_soft_wrap): - continuation = self.multiline_continuation_char * (width - 1) + ' ' + def get_continuation(width, *_): + if self.multiline_continuation_char: + left_padding = width - len(self.multiline_continuation_char) + continuation = " " * max((left_padding - 1), 0) + self.multiline_continuation_char + " " + else: + continuation = " " return [('class:continuation', continuation)] def show_suggestion_tip(): diff --git a/mycli/myclirc b/mycli/myclirc index cdf769f2..534b2015 100644 --- a/mycli/myclirc +++ b/mycli/myclirc @@ -66,7 +66,7 @@ wider_completion_menu = False # \A - DSN alias name (from the [alias_dsn] section) # \u - Username prompt = '\t \u@\h:\d> ' -prompt_continuation = ' ' +prompt_continuation = '->' # Skip intro info on startup and outro info on exit less_chatty = False From 3db74ddadcc53f59fc2787464420046b81f145c2 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sat, 18 Apr 2020 19:50:28 -0700 Subject: [PATCH 068/202] Lint fixes. --- mycli/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mycli/main.py b/mycli/main.py index bac58d85..248aa9f6 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -455,7 +455,8 @@ def _connect(): else: socket_owner = getpwuid(os.stat(socket).st_uid).pw_name self.echo( - "Using socket {}, owned by user {}".format(socket, socket_owner) + "Using socket {}, owned by user {}".format( + socket, socket_owner) ) else: host = host or 'localhost' From 471c114b2066d89e0b3eadfe02fa188c0ef63337 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sun, 19 Apr 2020 10:50:58 +0300 Subject: [PATCH 069/202] lint --- mycli/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mycli/main.py b/mycli/main.py index 7a4bab2f..01f46cbf 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -541,7 +541,9 @@ def get_message(): def get_continuation(width, *_): if self.multiline_continuation_char: left_padding = width - len(self.multiline_continuation_char) - continuation = " " * max((left_padding - 1), 0) + self.multiline_continuation_char + " " + continuation = " " * \ + max((left_padding - 1), 0) + \ + self.multiline_continuation_char + " " else: continuation = " " return [('class:continuation', continuation)] From 9453dda9072b615cd0fc30c8e11de348b2a84ee3 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sun, 19 Apr 2020 21:46:44 +0300 Subject: [PATCH 070/202] test for no destructive warning with --no-warn --- test/features/crud_table.feature | 9 ++++++++- test/features/environment.py | 8 ++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/test/features/crud_table.feature b/test/features/crud_table.feature index ebcb8818..3384efd7 100644 --- a/test/features/crud_table.feature +++ b/test/features/crud_table.feature @@ -28,7 +28,8 @@ Feature: manipulate tables: then we see null selected Scenario: confirm destructive query - When we query "delete from foo;" + When we query "create table foo(x integer);" + and we query "delete from foo;" and we answer the destructive warning with "y" then we see text "Your call!" @@ -37,6 +38,12 @@ Feature: manipulate tables: and we answer the destructive warning with "n" then we see text "Wise choice!" + Scenario: no destructive warning if disabled in config + When we run dbcli with --no-warn + and we query "create table blabla(x integer);" + and we query "delete from blabla;" + Then we see text "Query OK" + Scenario: confirm destructive query with invalid response When we query "delete from foo;" then we answer the destructive warning with invalid "1" and see text "is not a valid boolean" diff --git a/test/features/environment.py b/test/features/environment.py index c906b3b4..422c2642 100644 --- a/test/features/environment.py +++ b/test/features/environment.py @@ -8,6 +8,8 @@ from steps.wrappers import run_cli, wait_prompt +test_log_file = os.path.join(os.environ['HOME'], '.mycli.test.log') + def before_all(context): """Set env parameters.""" @@ -95,12 +97,18 @@ def before_step(context, _): def before_scenario(context, _): + with open(test_log_file, 'w') as f: + f.write('') run_cli(context) wait_prompt(context) def after_scenario(context, _): """Cleans up after each test complete.""" + with open(test_log_file) as f: + for line in f: + if 'error' in line.lower(): + raise RuntimeError(f'Error in log file: {line}') if hasattr(context, 'cli') and not context.exit_sent: # Quit nicely. From 08e8d2433e6a2cbb276c9d360d139d6c04535829 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sun, 19 Apr 2020 21:55:12 +0300 Subject: [PATCH 071/202] fixed undefined variable --- changelog.md | 2 +- mycli/main.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index a339d3b6..b6bf8f3e 100644 --- a/changelog.md +++ b/changelog.md @@ -9,7 +9,7 @@ Bug Fixes: ---------- * Fix broken auto-completion for favorite queries (Thanks: [Amjith]). - +* Fix undefined variable exception when running with --no-warn (Thanks: [Georgy Frolov]) 1.21.0 ====== diff --git a/mycli/main.py b/mycli/main.py index ff6a0bc6..d7a7a0ba 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -587,6 +587,8 @@ def one_iteration(text=None): else: self.echo('Wise choice!') return + else: + destroy = True # Keep track of whether or not the query is mutating. In case # of a multi-statement query, the overall query is considered From d1966b11d4f64572814dca3d21b76b3da7d0b6d8 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sun, 19 Apr 2020 12:41:45 -0700 Subject: [PATCH 072/202] Update changelog for 1.21.1. --- changelog.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/changelog.md b/changelog.md index b6bf8f3e..f6e8fe2b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,9 +1,5 @@ -TBD -=== - -Features: ---------- - +1.21.1 +====== Bug Fixes: ---------- From 26a25be4dc5327b5d76b0f0781904c5af7ddfe68 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sun, 19 Apr 2020 13:15:15 -0700 Subject: [PATCH 073/202] Releasing version 1.21.1 --- mycli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/__init__.py b/mycli/__init__.py index b74f54f8..d8d8ff44 100644 --- a/mycli/__init__.py +++ b/mycli/__init__.py @@ -1 +1 @@ -__version__ = '1.21.0' +__version__ = '1.21.1' From 69172945b376bd44c7df6f29afff0faf1f5c011a Mon Sep 17 00:00:00 2001 From: Nathan Huang Date: Mon, 20 Apr 2020 11:11:27 +0800 Subject: [PATCH 074/202] update warning word. --- README.md | 6 +++--- mycli/main.py | 8 +++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d8174bd7..efe804d8 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,8 @@ $ sudo apt-get install mycli # Only on debian or ubuntu --ssh-password TEXT Password to connect to ssh server. --ssh-key-filename TEXT Private key filename (identify file) for the ssh connection. - --ssh-config-path TEXT Path to ssh configuation - --ssh-config-host TEXT Host for ssh server in ssh configuations + --ssh-config-path TEXT Path to ssh configuation. + --ssh-config-host TEXT Host for ssh server in ssh configuations (requires paramiko). --ssl-ca PATH CA file in PEM format. --ssl-capath TEXT CA directory. --ssl-cert PATH X509 cert in PEM format. @@ -80,7 +80,7 @@ $ sudo apt-get install mycli # Only on debian or ubuntu section of myclirc file. --list-dsn list of DSN configured into the [alias_dsn] section of myclirc file. - --list-ssh-config list of ssh configuration in the ssh config. + --list-ssh-config list ssh configurations in the ssh config (requires paramiko). -R, --prompt TEXT Prompt format (Default: "\t \u@\h:\d> "). -l, --logfile FILENAME Log every query and its results to a file. --defaults-group-suffix TEXT Read MySQL config groups with the specified diff --git a/mycli/main.py b/mycli/main.py index 69fd6ca6..f5a3600e 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1006,7 +1006,7 @@ def get_last_query(self): @click.option('--list-dsn', 'list_dsn', is_flag=True, help='list of DSN configured into the [alias_dsn] section of myclirc file.') @click.option('--list-ssh-config', 'list_ssh_config', is_flag=True, - help='list of ssh configurations in the ssh config.') + help='list ssh configurations in the ssh config (requires paramiko).') @click.option('-R', '--prompt', 'prompt', help='Prompt format (Default: "{0}").'.format( MyCli.default_prompt)) @@ -1079,8 +1079,7 @@ def cli(database, user, host, port, socket, password, dbname, if list_ssh_config: if not paramiko: click.secho( - "Cannot use SSH transport because paramiko isn't installed, " - "please install paramiko or don't use --list-ssh-config=", + "This features requires paramiko. Please install paramiko and try again.", err=True, fg='red' ) exit(1) @@ -1155,8 +1154,7 @@ def cli(database, user, host, port, socket, password, dbname, if ssh_config_host: if not paramiko: click.secho( - "Cannot use SSH transport because paramiko isn't installed, " - "please install paramiko or don't use --ssh-config_host=", + "This features requires paramiko. Please install paramiko and try again.", err=True, fg='red' ) exit(1) From de1e7fe47843032fddf5b82fee592f75ccb30ca6 Mon Sep 17 00:00:00 2001 From: Thomas Roten Date: Tue, 21 Apr 2020 09:44:28 -0500 Subject: [PATCH 075/202] move is_dropping_database tests to parseutils. --- test/test_main.py | 13 +------------ test/test_parseutils.py | 2 ++ 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/test/test_main.py b/test/test_main.py index cde32832..47395dfd 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -3,7 +3,7 @@ import click from click.testing import CliRunner -from mycli.main import MyCli, cli, thanks_picker, is_dropping_database, PACKAGE_ROOT +from mycli.main import MyCli, cli, thanks_picker, PACKAGE_ROOT from mycli.packages.special.main import COMMANDS as SPECIAL_COMMANDS from .utils import USER, HOST, PORT, PASSWORD, dbtest, run @@ -147,17 +147,6 @@ def test_thanks_picker_utf8(): assert name and isinstance(name, str) -def test_is_dropping_database(): - is_dropping_text = "DROP DATABASE foo;" - assert is_dropping_database(is_dropping_text, 'foo') - is_not_dropping_text = "DROP DATABASE foo; CREATE DATABASE foo;" - assert not is_dropping_database(is_not_dropping_text, 'foo') - is_dropping_other_text = "DROP DATABASE bar;" - assert not is_dropping_database(is_dropping_other_text, 'foo') - is_not_dropping_other_text = "DROP DATABASE foo; CREATE DATABASE bar;" - assert is_dropping_database(is_not_dropping_other_text, 'foo') - - def test_help_strings_end_with_periods(): """Make sure click options have help text that end with a period.""" for param in cli.params: diff --git a/test/test_parseutils.py b/test/test_parseutils.py index c5271045..920a08db 100644 --- a/test/test_parseutils.py +++ b/test/test_parseutils.py @@ -175,6 +175,8 @@ def test_query_has_where_clause(sql, has_where_clause): ('drop schema foo', 'bar', False), ('drop database bar', 'foo', False), ('drop database foo', None, False), + ('drop database foo; create database foo', 'foo', False), + ('drop database foo; create database bar', 'foo', True), ('select bar from foo; drop database bazz', 'foo', False), ('select bar from foo; drop database bazz', 'bazz', True), ('-- dropping database \n ' From 08d56d8b479111641094f86d324f1ff232451b05 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Thu, 23 Apr 2020 11:00:56 +0300 Subject: [PATCH 076/202] python-prompt-toolkit version upgrade --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bd332a60..e58a4313 100755 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ install_requirements = [ 'click >= 7.0', 'Pygments >= 1.6', - 'prompt_toolkit>=2.0.6,<3.0.0', + 'prompt_toolkit>=3.0.0,<4.0.0', 'PyMySQL >= 0.9.2', 'sqlparse>=0.3.0,<0.4.0', 'configobj >= 5.0.5', From 5072494ece75856a5252b75ba381ad8d03b5b0dc Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Thu, 23 Apr 2020 11:11:13 +0300 Subject: [PATCH 077/202] disable CPR --- test/features/environment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/features/environment.py b/test/features/environment.py index 422c2642..1a49dbed 100644 --- a/test/features/environment.py +++ b/test/features/environment.py @@ -17,6 +17,7 @@ def before_all(context): os.environ['COLUMNS'] = "100" os.environ['EDITOR'] = 'ex' os.environ['LC_ALL'] = 'en_US.utf8' + os.environ['PROMPT_TOOLKIT_NO_CPR'] = '1' test_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) login_path_file = os.path.join(test_dir, 'mylogin.cnf') From a15df7416b40a1872e374f3c75ab19f3fc78d984 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Thu, 23 Apr 2020 12:46:09 +0300 Subject: [PATCH 078/202] fixed sillent tcp/ip fallbacks inside pymysql --- mycli/main.py | 23 +++++++++++++++-------- mycli/packages/filepaths.py | 5 +++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index 4f9c3bdb..1c01d2aa 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -430,6 +430,17 @@ def _connect(): else: raise e + def _fallback_to_tcp_ip(): + self.echo( + 'Retrying over TCP/IP', err=True) + + # Else fall back to TCP/IP localhost + nonlocal socket, host, port + socket = "" + host = 'localhost' + port = 3306 + _connect() + try: if (host is None) and not WIN: # Try a sensible default socket first (simplifies auth) @@ -437,6 +448,9 @@ def _connect(): try: socket = socket or guess_socket_location() _connect() + except FileNotFoundError: + self.echo('Failed to find socket file at default locations') + _fallback_to_tcp_ip() except OperationalError as e: # These are "Can't open socket" and 2x "Can't connect" if [code for code in (2001, 2002, 2003) if code == e.args[0]]: @@ -447,14 +461,7 @@ def _connect(): self.echo( "Failed to connect to local MySQL server through socket '{}':".format(socket)) self.echo(str(e), err=True) - self.echo( - 'Retrying over TCP/IP', err=True) - - # Else fall back to TCP/IP localhost - socket = "" - host = 'localhost' - port = 3306 - _connect() + _fallback_to_tcp_ip() else: raise e else: diff --git a/mycli/packages/filepaths.py b/mycli/packages/filepaths.py index f2950d7d..26b77c40 100644 --- a/mycli/packages/filepaths.py +++ b/mycli/packages/filepaths.py @@ -99,7 +99,8 @@ def guess_socket_location(): for directory in socket_dirs: for r, dirs, files in os.walk(directory, topdown=True): for filename in files: - if filename.startswith("mysql") and filename.endswith(".socket"): + name, ext = os.path.splitext(filename) + if name.startswith("mysql") and ext in ('.socket', '.sock'): return os.path.join(r, filename) dirs[:] = [d for d in dirs if d.startswith("mysql")] - return "" + raise FileNotFoundError From d8d94f97f0c4c6c676a3a005a27f2765f2f85751 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Tue, 28 Apr 2020 11:30:35 +0300 Subject: [PATCH 079/202] Fix binary data representation in SQL formatters --- mycli/packages/tabular_output/sql_format.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/mycli/packages/tabular_output/sql_format.py b/mycli/packages/tabular_output/sql_format.py index 3ad0aa2a..cf0ce5a7 100644 --- a/mycli/packages/tabular_output/sql_format.py +++ b/mycli/packages/tabular_output/sql_format.py @@ -9,6 +9,13 @@ preprocessors = () +def escape_for_sql_statement(value): + if isinstance(value, bytes): + return f"X'{value.hex()}'" + else: + return formatter.mycli.sqlexecute.conn.escape(value) + + def adapter(data, headers, table_format=None, **kwargs): tables = extract_tables(formatter.query) if len(tables) > 0: @@ -19,13 +26,12 @@ def adapter(data, headers, table_format=None, **kwargs): table_name = table[1] else: table_name = "`DUAL`" - escape = formatter.mycli.sqlexecute.conn.escape if table_format == 'sql-insert': h = "`, `".join(headers) yield "INSERT INTO {} (`{}`) VALUES".format(table_name, h) prefix = " " for d in data: - values = ", ".join(escape(v) for i, v in enumerate(d)) + values = ", ".join(escape_for_sql_statement(v) for i, v in enumerate(d)) yield "{}({})".format(prefix, values) if prefix == " ": prefix = ", " @@ -39,11 +45,11 @@ def adapter(data, headers, table_format=None, **kwargs): yield "UPDATE {} SET".format(table_name) prefix = " " for i, v in enumerate(d[keys:], keys): - yield "{}`{}` = {}".format(prefix, headers[i], escape(v)) + yield "{}`{}` = {}".format(prefix, headers[i], escape_for_sql_statement(v)) if prefix == " ": prefix = ", " f = "`{}` = {}" - where = (f.format(headers[i], escape(d[i])) for i in range(keys)) + where = (f.format(headers[i], escape_for_sql_statement(d[i])) for i in range(keys)) yield "WHERE {};".format(" AND ".join(where)) From eee98f25117c9b54ac36023f29830af44b46c973 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sun, 3 May 2020 11:56:12 +0300 Subject: [PATCH 080/202] update tests for sql output --- test/test_tabular_output.py | 40 +++++++++++++++++++++++-------------- test/utils.py | 2 +- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/test/test_tabular_output.py b/test/test_tabular_output.py index 501198f3..fe7e5da3 100644 --- a/test/test_tabular_output.py +++ b/test/test_tabular_output.py @@ -23,13 +23,21 @@ def mycli(): @dbtest def test_sql_output(mycli): """Test the sql output adapter.""" - headers = ['letters', 'number', 'optional', 'float'] + headers = ['letters', 'number', 'optional', 'float', 'binary'] class FakeCursor(object): def __init__(self): - self.data = [('abc', 1, None, 10.0), ('d', 456, '1', 0.5)] - self.description = [(None, FIELD_TYPE.VARCHAR), (None, FIELD_TYPE.LONG), - (None, FIELD_TYPE.LONG), (None, FIELD_TYPE.FLOAT)] + self.data = [ + ('abc', 1, None, 10.0, b'\xAA'), + ('d', 456, '1', 0.5, b'\xAA\xBB') + ] + self.description = [ + (None, FIELD_TYPE.VARCHAR), + (None, FIELD_TYPE.LONG), + (None, FIELD_TYPE.LONG), + (None, FIELD_TYPE.FLOAT), + (None, FIELD_TYPE.BLOB) + ] def __iter__(self): return self @@ -40,8 +48,6 @@ def __next__(self): else: raise StopIteration() - next = __next__ # Python 2 - def description(self): return self.description @@ -55,11 +61,13 @@ def description(self): `number` = 1 , `optional` = NULL , `float` = 10 + , `binary` = X'aa' WHERE `letters` = 'abc'; UPDATE `DUAL` SET `number` = 456 , `optional` = '1' , `float` = 0.5 + , `binary` = X'aabb' WHERE `letters` = 'd';''') # Test sql-update-2 output format assert list(mycli.change_table_format("sql-update-2")) == \ @@ -70,10 +78,12 @@ def description(self): UPDATE `DUAL` SET `optional` = NULL , `float` = 10 + , `binary` = X'aa' WHERE `letters` = 'abc' AND `number` = 1; UPDATE `DUAL` SET `optional` = '1' , `float` = 0.5 + , `binary` = X'aabb' WHERE `letters` = 'd' AND `number` = 456;''') # Test sql-insert output format (without table name) assert list(mycli.change_table_format("sql-insert")) == \ @@ -81,9 +91,9 @@ def description(self): mycli.formatter.query = "" output = mycli.format_output(None, FakeCursor(), headers) assert "\n".join(output) == dedent('''\ - INSERT INTO `DUAL` (`letters`, `number`, `optional`, `float`) VALUES - ('abc', 1, NULL, 10) - , ('d', 456, '1', 0.5) + INSERT INTO `DUAL` (`letters`, `number`, `optional`, `float`, `binary`) VALUES + ('abc', 1, NULL, 10, X'aa') + , ('d', 456, '1', 0.5, X'aabb') ;''') # Test sql-insert output format (with table name) assert list(mycli.change_table_format("sql-insert")) == \ @@ -91,9 +101,9 @@ def description(self): mycli.formatter.query = "SELECT * FROM `table`" output = mycli.format_output(None, FakeCursor(), headers) assert "\n".join(output) == dedent('''\ - INSERT INTO `table` (`letters`, `number`, `optional`, `float`) VALUES - ('abc', 1, NULL, 10) - , ('d', 456, '1', 0.5) + INSERT INTO `table` (`letters`, `number`, `optional`, `float`, `binary`) VALUES + ('abc', 1, NULL, 10, X'aa') + , ('d', 456, '1', 0.5, X'aabb') ;''') # Test sql-insert output format (with database + table name) assert list(mycli.change_table_format("sql-insert")) == \ @@ -101,7 +111,7 @@ def description(self): mycli.formatter.query = "SELECT * FROM `database`.`table`" output = mycli.format_output(None, FakeCursor(), headers) assert "\n".join(output) == dedent('''\ - INSERT INTO `database`.`table` (`letters`, `number`, `optional`, `float`) VALUES - ('abc', 1, NULL, 10) - , ('d', 456, '1', 0.5) + INSERT INTO `database`.`table` (`letters`, `number`, `optional`, `float`, `binary`) VALUES + ('abc', 1, NULL, 10, X'aa') + , ('d', 456, '1', 0.5, X'aabb') ;''') diff --git a/test/utils.py b/test/utils.py index 57669658..66b41940 100644 --- a/test/utils.py +++ b/test/utils.py @@ -12,7 +12,7 @@ PASSWORD = os.getenv('PYTEST_PASSWORD') USER = os.getenv('PYTEST_USER', 'root') HOST = os.getenv('PYTEST_HOST', 'localhost') -PORT = os.getenv('PYTEST_PORT', 3306) +PORT = int(os.getenv('PYTEST_PORT', 3306)) CHARSET = os.getenv('PYTEST_CHARSET', 'utf8') SSH_USER = os.getenv('PYTEST_SSH_USER', None) SSH_HOST = os.getenv('PYTEST_SSH_HOST', None) From e62231169df7add02b0d6776ab575668a7aced08 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sun, 3 May 2020 21:17:04 -0700 Subject: [PATCH 081/202] Lint fixes. --- mycli/packages/tabular_output/sql_format.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mycli/packages/tabular_output/sql_format.py b/mycli/packages/tabular_output/sql_format.py index cf0ce5a7..730e6332 100644 --- a/mycli/packages/tabular_output/sql_format.py +++ b/mycli/packages/tabular_output/sql_format.py @@ -31,7 +31,8 @@ def adapter(data, headers, table_format=None, **kwargs): yield "INSERT INTO {} (`{}`) VALUES".format(table_name, h) prefix = " " for d in data: - values = ", ".join(escape_for_sql_statement(v) for i, v in enumerate(d)) + values = ", ".join(escape_for_sql_statement(v) + for i, v in enumerate(d)) yield "{}({})".format(prefix, values) if prefix == " ": prefix = ", " @@ -49,7 +50,8 @@ def adapter(data, headers, table_format=None, **kwargs): if prefix == " ": prefix = ", " f = "`{}` = {}" - where = (f.format(headers[i], escape_for_sql_statement(d[i])) for i in range(keys)) + where = (f.format(headers[i], escape_for_sql_statement( + d[i])) for i in range(keys)) yield "WHERE {};".format(" AND ".join(where)) From df44b068ae10dfa37eac4682c62ddff932966cc3 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Wed, 6 May 2020 19:16:17 +0300 Subject: [PATCH 082/202] fix discovering .cnf files that are included with !includedirs --- mycli/config.py | 27 ++++++++++- mycli/main.py | 97 ++++++++++++++++++++++++++----------- mycli/packages/filepaths.py | 2 +- mycli/sqlexecute.py | 1 - 4 files changed, 96 insertions(+), 31 deletions(-) diff --git a/mycli/config.py b/mycli/config.py index 77475099..03fb502a 100644 --- a/mycli/config.py +++ b/mycli/config.py @@ -1,4 +1,5 @@ import shutil +from copy import copy from io import BytesIO, TextIOWrapper import logging import os @@ -58,12 +59,34 @@ def read_config_file(f, list_values=True): return config +def get_included_configs(config_path) -> list: + """Get a list of configuration files that are included into config_path + with !includedir directive.""" + if not os.path.exists(config_path): + return [] + included_configs = [] + with open(config_path) as f: + include_directives = filter( + lambda s: s.startswith('!includedir'), + f + ) + dirs = map(lambda s: s.strip().split()[-1], include_directives) + dirs = filter(os.path.isdir, dirs) + for dir in dirs: + for filename in os.listdir(dir): + if filename.endswith('.cnf'): + included_configs.append(os.path.join(dir, filename)) + return included_configs + + def read_config_files(files, list_values=True): """Read and merge a list of config files.""" config = ConfigObj(list_values=list_values) - - for _file in files: + _files = copy(files) + while _files: + _file = _files.pop(0) + _files = get_included_configs(_file) + _files _config = read_config_file(_file, list_values=list_values) if bool(_config) is True: config.merge(_config) diff --git a/mycli/main.py b/mycli/main.py index 1c01d2aa..55afedd5 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -386,7 +386,7 @@ def connect(self, database='', user='', passwd='', host='', port='', if port or host: socket = '' else: - socket = socket or cnf['socket'] + socket = socket or cnf['socket'] or guess_socket_location() user = user or cnf['user'] or os.getenv('USER') host = host or cnf['host'] port = port or cnf['port'] @@ -430,27 +430,13 @@ def _connect(): else: raise e - def _fallback_to_tcp_ip(): - self.echo( - 'Retrying over TCP/IP', err=True) - - # Else fall back to TCP/IP localhost - nonlocal socket, host, port - socket = "" - host = 'localhost' - port = 3306 - _connect() - try: - if (host is None) and not WIN: - # Try a sensible default socket first (simplifies auth) - # If we get a connection error, try tcp/ip localhost + if not WIN and socket: + socket_owner = getpwuid(os.stat(socket).st_uid).pw_name + self.echo( + f"Connecting to socket {socket}, owned by user {socket_owner}") try: - socket = socket or guess_socket_location() _connect() - except FileNotFoundError: - self.echo('Failed to find socket file at default locations') - _fallback_to_tcp_ip() except OperationalError as e: # These are "Can't open socket" and 2x "Can't connect" if [code for code in (2001, 2002, 2003) if code == e.args[0]]: @@ -461,15 +447,16 @@ def _fallback_to_tcp_ip(): self.echo( "Failed to connect to local MySQL server through socket '{}':".format(socket)) self.echo(str(e), err=True) - _fallback_to_tcp_ip() + self.echo( + 'Retrying over TCP/IP', err=True) + + # Else fall back to TCP/IP localhost + socket = "" + host = 'localhost' + port = 3306 + _connect() else: raise e - else: - socket_owner = getpwuid(os.stat(socket).st_uid).pw_name - self.echo( - "Using socket {}, owned by user {}".format( - socket, socket_owner) - ) else: host = host or 'localhost' port = port or 3306 @@ -1009,6 +996,9 @@ def get_last_query(self): @click.option('--ssh-port', default=22, help='Port to connect to ssh server.') @click.option('--ssh-password', help='Password to connect to ssh server.') @click.option('--ssh-key-filename', help='Private key filename (identify file) for the ssh connection.') +@click.option('--ssh-config-path', help='Path to ssh configuration.', + default=os.path.expanduser('~') + '/.ssh/config') +@click.option('--ssh-config-host', help='Host to connect to ssh server reading from ssh configuration.') @click.option('--ssl-ca', help='CA file in PEM format.', type=click.Path(exists=True)) @click.option('--ssl-capath', help='CA directory.') @@ -1030,6 +1020,8 @@ def get_last_query(self): help='Use DSN configured into the [alias_dsn] section of myclirc file.') @click.option('--list-dsn', 'list_dsn', is_flag=True, help='list of DSN configured into the [alias_dsn] section of myclirc file.') +@click.option('--list-ssh-config', 'list_ssh_config', is_flag=True, + help='list ssh configurations in the ssh config (requires paramiko).') @click.option('-R', '--prompt', 'prompt', help='Prompt format (Default: "{0}").'.format( MyCli.default_prompt)) @@ -1062,7 +1054,7 @@ def cli(database, user, host, port, socket, password, dbname, ssl_ca, ssl_capath, ssl_cert, ssl_key, ssl_cipher, ssl_verify_server_cert, table, csv, warn, execute, myclirc, dsn, list_dsn, ssh_user, ssh_host, ssh_port, ssh_password, - ssh_key_filename): + ssh_key_filename, list_ssh_config, ssh_config_path, ssh_config_host): """A MySQL terminal client with auto-completion and syntax highlighting. \b @@ -1099,6 +1091,31 @@ def cli(database, user, host, port, socket, password, dbname, else: click.secho(alias) sys.exit(0) + if list_ssh_config: + if not paramiko: + click.secho( + "This features requires paramiko. Please install paramiko and try again.", + err=True, fg='red' + ) + exit(1) + try: + ssh_config = paramiko.config.SSHConfig().from_path(ssh_config_path) + except paramiko.ssh_exception.ConfigParseError as err: + click.secho('Invalid SSH configuration file. ' + 'Please check the SSH configuration file.', + err=True, fg='red') + exit(1) + except FileNotFoundError as e: + click.secho(str(e), err=True, fg='red') + exit(1) + for host in ssh_config.get_hostnames(): + if verbose: + host_config = ssh_config.lookup(host) + click.secho("{} : {}".format( + host, host_config.get('hostname'))) + else: + click.secho(host) + sys.exit(0) # Choose which ever one has a valid value. database = dbname or database @@ -1149,6 +1166,32 @@ def cli(database, user, host, port, socket, password, dbname, if not port: port = uri.port + if ssh_config_host: + if not paramiko: + click.secho( + "This features requires paramiko. Please install paramiko and try again.", + err=True, fg='red' + ) + exit(1) + try: + ssh_config = paramiko.config.SSHConfig().from_path(ssh_config_path) + except paramiko.ssh_exception.ConfigParseError as err: + click.secho('Invalid SSH configuration file. ' + 'Please check the SSH configuration file.', + err=True, fg='red') + exit(1) + except FileNotFoundError as e: + click.secho(str(e), err=True, fg='red') + exit(1) + ssh_config = ssh_config.lookup(ssh_config_host) + ssh_host = ssh_host if ssh_host else ssh_config.get('hostname') + ssh_user = ssh_user if ssh_user else ssh_config.get('user') + if ssh_config.get('port') and ssh_port == 22: + # port has a default value, overwrite it if it's in the config + ssh_port = int(ssh_config.get('port')) + ssh_key_filename = ssh_key_filename if ssh_key_filename else ssh_config.get( + 'identityfile', [''])[0] + if not paramiko and ssh_host: click.secho( "Cannot use SSH transport because paramiko isn't installed, " diff --git a/mycli/packages/filepaths.py b/mycli/packages/filepaths.py index 26b77c40..79fe26dc 100644 --- a/mycli/packages/filepaths.py +++ b/mycli/packages/filepaths.py @@ -103,4 +103,4 @@ def guess_socket_location(): if name.startswith("mysql") and ext in ('.socket', '.sock'): return os.path.join(r, filename) dirs[:] = [d for d in dirs if d.startswith("mysql")] - raise FileNotFoundError + return None diff --git a/mycli/sqlexecute.py b/mycli/sqlexecute.py index 88db9dc4..f38da6f6 100644 --- a/mycli/sqlexecute.py +++ b/mycli/sqlexecute.py @@ -191,7 +191,6 @@ def run(self, statement): if not cur.nextset() or (not cur.rowcount and cur.description is None): break - def get_result(self, cursor): """Get the current result's data from the cursor.""" title = headers = None From a3cf82a4f34c8f86ea6ebc524e9aac4c08e340a1 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sun, 10 May 2020 22:32:31 +0300 Subject: [PATCH 083/202] read myclird section in .cnf files --- mycli/config.py | 47 ++++++++++++++++++++++++++++++++--------------- mycli/main.py | 2 +- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/mycli/config.py b/mycli/config.py index 03fb502a..e0f2d1fc 100644 --- a/mycli/config.py +++ b/mycli/config.py @@ -1,3 +1,4 @@ +import io import shutil from copy import copy from io import BytesIO, TextIOWrapper @@ -6,6 +7,7 @@ from os.path import exists import struct import sys +from typing import Union from configobj import ConfigObj, ConfigObjError from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes @@ -59,23 +61,34 @@ def read_config_file(f, list_values=True): return config -def get_included_configs(config_path) -> list: +def get_included_configs(config_file: Union[str, io.TextIOWrapper]) -> list: """Get a list of configuration files that are included into config_path - with !includedir directive.""" - if not os.path.exists(config_path): + with !includedir directive. + + "Normal" configs should be passed as file paths. The only exception + is .mylogin which is decoded into a stream. However, it never + contains include directives and so will be ignored by this + function. + + """ + if not isinstance(config_file, str) or not os.path.isfile(config_file): return [] included_configs = [] - with open(config_path) as f: - include_directives = filter( - lambda s: s.startswith('!includedir'), - f - ) - dirs = map(lambda s: s.strip().split()[-1], include_directives) - dirs = filter(os.path.isdir, dirs) - for dir in dirs: - for filename in os.listdir(dir): - if filename.endswith('.cnf'): - included_configs.append(os.path.join(dir, filename)) + + try: + with open(config_file) as f: + include_directives = filter( + lambda s: s.startswith('!includedir'), + f + ) + dirs = map(lambda s: s.strip().split()[-1], include_directives) + dirs = filter(os.path.isdir, dirs) + for dir in dirs: + for filename in os.listdir(dir): + if filename.endswith('.cnf'): + included_configs.append(os.path.join(dir, filename)) + except (PermissionError, UnicodeDecodeError): + pass return included_configs @@ -86,8 +99,12 @@ def read_config_files(files, list_values=True): _files = copy(files) while _files: _file = _files.pop(0) - _files = get_included_configs(_file) + _files _config = read_config_file(_file, list_values=list_values) + + # expand includes only if we were able to parse config + # (otherwise we'll just encounter the same errors again) + if config is not None: + _files = get_included_configs(_file) + _files if bool(_config) is True: config.merge(_config) config.filename = _config.filename diff --git a/mycli/main.py b/mycli/main.py index 55afedd5..1fe2a848 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -318,7 +318,7 @@ def read_my_cnf_files(self, files, keys): """ cnf = read_config_files(files, list_values=False) - sections = ['client'] + sections = ['client', 'mysqld'] if self.login_path and self.login_path != 'client': sections.append(self.login_path) From 451759ea48c882c3bb9903e729ba9dea5a3da37f Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Tue, 12 May 2020 20:54:28 +0300 Subject: [PATCH 084/202] added python_requires to setup.py to prevent installation on python 2 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index e58a4313..5a5cddef 100755 --- a/setup.py +++ b/setup.py @@ -94,6 +94,7 @@ def run_tests(self): 'console_scripts': ['mycli = mycli.main:cli'], }, cmdclass={'lint': lint, 'test': test}, + python_requires=">=3.6", classifiers=[ 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', From f949e0e48225d6edf954862667cdccd67c21bd31 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sat, 23 May 2020 22:17:16 +0300 Subject: [PATCH 085/202] Changed paramiko usage to work with older versions --- mycli/main.py | 71 ++++++++++-------------- mycli/packages/paramiko_stub/__init__.py | 28 ++++++++++ mycli/sqlexecute.py | 6 +- 3 files changed, 59 insertions(+), 46 deletions(-) create mode 100644 mycli/packages/paramiko_stub/__init__.py diff --git a/mycli/main.py b/mycli/main.py index fc56df58..34b5c4f6 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -65,7 +65,7 @@ try: import paramiko except ImportError: - paramiko = False + from mycli.packages.paramiko_stub import paramiko # Query tuples are used for maintaining history Query = namedtuple('Query', ['query', 'successful', 'mutating']) @@ -1091,22 +1091,7 @@ def cli(database, user, host, port, socket, password, dbname, click.secho(alias) sys.exit(0) if list_ssh_config: - if not paramiko: - click.secho( - "This features requires paramiko. Please install paramiko and try again.", - err=True, fg='red' - ) - exit(1) - try: - ssh_config = paramiko.config.SSHConfig().from_path(ssh_config_path) - except paramiko.ssh_exception.ConfigParseError as err: - click.secho('Invalid SSH configuration file. ' - 'Please check the SSH configuration file.', - err=True, fg='red') - exit(1) - except FileNotFoundError as e: - click.secho(str(e), err=True, fg='red') - exit(1) + ssh_config = read_ssh_config(ssh_config_path) for host in ssh_config.get_hostnames(): if verbose: host_config = ssh_config.lookup(host) @@ -1166,38 +1151,16 @@ def cli(database, user, host, port, socket, password, dbname, port = uri.port if ssh_config_host: - if not paramiko: - click.secho( - "This features requires paramiko. Please install paramiko and try again.", - err=True, fg='red' - ) - exit(1) - try: - ssh_config = paramiko.config.SSHConfig().from_path(ssh_config_path) - except paramiko.ssh_exception.ConfigParseError as err: - click.secho('Invalid SSH configuration file. ' - 'Please check the SSH configuration file.', - err=True, fg='red') - exit(1) - except FileNotFoundError as e: - click.secho(str(e), err=True, fg='red') - exit(1) - ssh_config = ssh_config.lookup(ssh_config_host) + ssh_config = read_ssh_config( + ssh_config_path + ).lookup(ssh_config_host) ssh_host = ssh_host if ssh_host else ssh_config.get('hostname') ssh_user = ssh_user if ssh_user else ssh_config.get('user') if ssh_config.get('port') and ssh_port == 22: # port has a default value, overwrite it if it's in the config ssh_port = int(ssh_config.get('port')) ssh_key_filename = ssh_key_filename if ssh_key_filename else ssh_config.get( - 'identityfile', [''])[0] - - if not paramiko and ssh_host: - click.secho( - "Cannot use SSH transport because paramiko isn't installed, " - "please install paramiko or don't use --ssh-host=", - err=True, fg="red" - ) - exit(1) + 'identityfile', [None])[0] ssh_key_filename = ssh_key_filename and os.path.expanduser(ssh_key_filename) @@ -1308,6 +1271,7 @@ def is_mutating(status): 'replace', 'truncate', 'load', 'rename']) return status.split(None, 1)[0].lower() in mutating + def is_select(status): """Returns true if the first word in status is 'select'.""" if not status: @@ -1332,5 +1296,26 @@ def edit_and_execute(event): buff.open_in_editor(validate_and_handle=False) +def read_ssh_config(ssh_config_path): + ssh_config = paramiko.config.SSHConfig() + try: + with open(ssh_config_path) as f: + ssh_config.parse(f) + # Paramiko prior to version 2.7 raises Exception on parse errors. + # In 2.7 it has become paramiko.ssh_exception.SSHException, + # but let's catch everything for compatibility + except Exception as err: + click.secho( + f'Could not parse SSH configuration file {ssh_config_path}:\n{err} ', + err=True, fg='red' + ) + sys.exit(1) + except FileNotFoundError as e: + click.secho(str(e), err=True, fg='red') + sys.exit(1) + else: + return ssh_config + + if __name__ == "__main__": cli() diff --git a/mycli/packages/paramiko_stub/__init__.py b/mycli/packages/paramiko_stub/__init__.py new file mode 100644 index 00000000..045b00ea --- /dev/null +++ b/mycli/packages/paramiko_stub/__init__.py @@ -0,0 +1,28 @@ +"""A module to import instead of paramiko when it is not available (to avoid +checking for paramiko all over the place). + +When paramiko is first envoked, it simply shuts down mycli, telling +user they either have to install paramiko or should not use SSH +features. + +""" + + +class Paramiko: + def __getattr__(self, name): + import sys + from textwrap import dedent + print(dedent(""" + To enable certain SSH features you need to install paramiko: + + pip install paramiko + + It is required for the following configuration options: + --list-ssh-config + --ssh-config-host + --ssh-host + """)) + sys.exit(1) + + +paramiko = Paramiko() diff --git a/mycli/sqlexecute.py b/mycli/sqlexecute.py index 88db9dc4..82b9f000 100644 --- a/mycli/sqlexecute.py +++ b/mycli/sqlexecute.py @@ -8,8 +8,8 @@ decoders) try: import paramiko -except: - paramiko = False +except ImportError: + from mycli.packages.paramiko_stub import paramiko _logger = logging.getLogger(__name__) @@ -118,7 +118,7 @@ def connect(self, database=None, user=None, password=None, host=None, defer_connect=defer_connect ) - if ssh_host and paramiko: + if ssh_host: client = paramiko.SSHClient() client.load_system_host_keys() client.set_missing_host_key_policy(paramiko.WarningPolicy()) From e06071d9afd244d394107c92c041d44c447a72a5 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sat, 23 May 2020 22:17:59 +0300 Subject: [PATCH 086/202] tell pytest to only look for tests in the test directory --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5a5cddef..156cd1ac 100755 --- a/setup.py +++ b/setup.py @@ -69,7 +69,7 @@ def initialize_options(self): def run_tests(self): unit_test_errno = subprocess.call( - 'pytest ' + self.pytest_args, + 'pytest test/ ' + self.pytest_args, shell=True ) cli_errno = subprocess.call( From 977e409aa8abd7804d1ee62809e791e23f2716db Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sun, 24 May 2020 00:00:24 +0300 Subject: [PATCH 087/202] Don't try to connect to socket over SSH --- mycli/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mycli/main.py b/mycli/main.py index 1fe2a848..00cffd0a 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -383,7 +383,8 @@ def connect(self, database='', user='', passwd='', host='', port='', # Fall back to config values only if user did not specify a value. database = database or cnf['database'] - if port or host: + # Socket interface not supported for SSH connections + if port or host or ssh_host or ssh_port: socket = '' else: socket = socket or cnf['socket'] or guess_socket_location() From 5557448d9ade57f05d395c76dd9b556b9998321b Mon Sep 17 00:00:00 2001 From: laixintao Date: Thu, 28 May 2020 10:04:01 +0800 Subject: [PATCH 088/202] highlight null value. --- mycli/clistyle.py | 1 + mycli/myclirc | 1 + setup.py | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/mycli/clistyle.py b/mycli/clistyle.py index c94f7931..293f0f43 100644 --- a/mycli/clistyle.py +++ b/mycli/clistyle.py @@ -34,6 +34,7 @@ Token.Output.Header: 'output.header', Token.Output.OddRow: 'output.odd-row', Token.Output.EvenRow: 'output.even-row', + Token.Output.Null: 'output.null', Token.Prompt: 'prompt', Token.Continuation: 'continuation', } diff --git a/mycli/myclirc b/mycli/myclirc index 534b2015..ba3ea1eb 100644 --- a/mycli/myclirc +++ b/mycli/myclirc @@ -111,6 +111,7 @@ bottom-toolbar.transaction.failed = 'bg:#222222 #ff005f bold' output.header = "#00ff5f bold" output.odd-row = "" output.even-row = "" +output.null = "#808080" # Favorite queries. [favorite_queries] diff --git a/setup.py b/setup.py index bd332a60..d6e3f27a 100755 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ 'sqlparse>=0.3.0,<0.4.0', 'configobj >= 5.0.5', 'cryptography >= 1.0.0', - 'cli_helpers[styles] > 1.1.0', + 'cli_helpers[styles] >= 2.0.1', ] From 4783f5b13457d0262a353d0ffd9a6a0d500116d2 Mon Sep 17 00:00:00 2001 From: laixintao Date: Thu, 28 May 2020 10:07:14 +0800 Subject: [PATCH 089/202] update changelog. --- changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.md b/changelog.md index f6e8fe2b..2cf26831 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ Bug Fixes: * Fix broken auto-completion for favorite queries (Thanks: [Amjith]). * Fix undefined variable exception when running with --no-warn (Thanks: [Georgy Frolov]) +* Support setting color for null value (Thanks: [laixintao]) 1.21.0 ====== @@ -745,3 +746,4 @@ Bug Fixes: [François Pietka]: https://github.com/fpietka [Frederic Aoustin]: https://github.com/fraoustin [Georgy Frolov]: https://github.com/pasenor +[laixintao]: https://github.com/laixintao From 2f0a6df77e542376e70c98c8eaec44e8acbb4a24 Mon Sep 17 00:00:00 2001 From: kevinhwang91 Date: Fri, 29 May 2020 17:41:00 +0800 Subject: [PATCH 090/202] fix always generate `~/.myclirc` Without 'XDG_CONFIG_HOME' but `~/.config/mycli/myclirc` is existed, mycli always generates `~/.myclirc config` file. It's caused by failing to compare `~` with `$HOME` in config path, fix by using `os.path.expanduser()`. --- mycli/AUTHORS | 1 + mycli/main.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mycli/AUTHORS b/mycli/AUTHORS index b3636d90..f7994bad 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -72,6 +72,7 @@ Contributors: * Jakub Boukal * Takeshi D. Itoh * laixintao + * kevinhwang91 Creator: -------- diff --git a/mycli/main.py b/mycli/main.py index d298f202..368bba4d 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -95,7 +95,7 @@ class MyCli(object): xdg_config_home = "~/.config" system_config_files = [ '/etc/myclirc', - os.path.join(xdg_config_home, "mycli", "myclirc") + os.path.join(os.path.expanduser(xdg_config_home), "mycli", "myclirc") ] default_config_file = os.path.join(PACKAGE_ROOT, 'myclirc') From 692ce21571abd4447c4d951d48b01fd156d5ca6d Mon Sep 17 00:00:00 2001 From: KITAGAWA Yasutaka Date: Wed, 10 Jun 2020 21:24:06 +0900 Subject: [PATCH 091/202] Add --init-command option --- mycli/main.py | 14 +++++++++----- mycli/sqlexecute.py | 21 +++++++++++++++------ test/test_main.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index d298f202..1e310d3e 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -360,7 +360,7 @@ def merge_ssl_with_cnf(self, ssl, cnf): def connect(self, database='', user='', passwd='', host='', port='', socket='', charset='', local_infile='', ssl='', ssh_user='', ssh_host='', ssh_port='', - ssh_password='', ssh_key_filename=''): + ssh_password='', ssh_key_filename='', init_command=''): cnf = {'database': None, 'user': None, @@ -417,7 +417,7 @@ def _connect(): self.sqlexecute = SQLExecute( database, user, passwd, host, port, socket, charset, local_infile, ssl, ssh_user, ssh_host, ssh_port, - ssh_password, ssh_key_filename + ssh_password, ssh_key_filename, init_command ) except OperationalError as e: if ('Access denied for user' in e.args[1]): @@ -426,7 +426,7 @@ def _connect(): self.sqlexecute = SQLExecute( database, user, new_passwd, host, port, socket, charset, local_infile, ssl, ssh_user, ssh_host, - ssh_port, ssh_password, ssh_key_filename + ssh_port, ssh_password, ssh_key_filename, init_command ) else: raise e @@ -1048,6 +1048,8 @@ def get_last_query(self): help='Read this path from the login file.') @click.option('-e', '--execute', type=str, help='Execute command and quit.') +@click.option('--init-command', type=str, + help='SQL statement to execute after connecting.') @click.argument('database', default='', nargs=1) def cli(database, user, host, port, socket, password, dbname, version, verbose, prompt, logfile, defaults_group_suffix, @@ -1055,7 +1057,8 @@ def cli(database, user, host, port, socket, password, dbname, ssl_ca, ssl_capath, ssl_cert, ssl_key, ssl_cipher, ssl_verify_server_cert, table, csv, warn, execute, myclirc, dsn, list_dsn, ssh_user, ssh_host, ssh_port, ssh_password, - ssh_key_filename, list_ssh_config, ssh_config_path, ssh_config_host): + ssh_key_filename, list_ssh_config, ssh_config_path, ssh_config_host, + init_command): """A MySQL terminal client with auto-completion and syntax highlighting. \b @@ -1179,7 +1182,8 @@ def cli(database, user, host, port, socket, password, dbname, ssh_host=ssh_host, ssh_port=ssh_port, ssh_password=ssh_password, - ssh_key_filename=ssh_key_filename + ssh_key_filename=ssh_key_filename, + init_command=init_command ) mycli.logger.debug('Launch Params: \n' diff --git a/mycli/sqlexecute.py b/mycli/sqlexecute.py index 035d98d1..4610c2e8 100644 --- a/mycli/sqlexecute.py +++ b/mycli/sqlexecute.py @@ -42,7 +42,7 @@ class SQLExecute(object): def __init__(self, database, user, password, host, port, socket, charset, local_infile, ssl, ssh_user, ssh_host, ssh_port, ssh_password, - ssh_key_filename): + ssh_key_filename, init_command=None): self.dbname = database self.user = user self.password = password @@ -59,12 +59,13 @@ def __init__(self, database, user, password, host, port, socket, charset, self.ssh_port = ssh_port self.ssh_password = ssh_password self.ssh_key_filename = ssh_key_filename + self.init_command = init_command self.connect() def connect(self, database=None, user=None, password=None, host=None, port=None, socket=None, charset=None, local_infile=None, ssl=None, ssh_host=None, ssh_port=None, ssh_user=None, - ssh_password=None, ssh_key_filename=None): + ssh_password=None, ssh_key_filename=None, init_command=None): db = (database or self.dbname) user = (user or self.user) password = (password or self.password) @@ -79,6 +80,7 @@ def connect(self, database=None, user=None, password=None, host=None, ssh_port = (ssh_port or self.ssh_port) ssh_password = (ssh_password or self.ssh_password) ssh_key_filename = (ssh_key_filename or self.ssh_key_filename) + init_command = (init_command or self.init_command) _logger.debug( 'Connection DB Params: \n' '\tdatabase: %r' @@ -93,9 +95,11 @@ def connect(self, database=None, user=None, password=None, host=None, '\tssh_host: %r' '\tssh_port: %r' '\tssh_password: %r' - '\tssh_key_filename: %r', + '\tssh_key_filename: %r' + '\tinit_command: %r', db, user, host, port, socket, charset, local_infile, ssl, - ssh_user, ssh_host, ssh_port, ssh_password, ssh_key_filename + ssh_user, ssh_host, ssh_port, ssh_password, ssh_key_filename, + init_command ) conv = conversions.copy() conv.update({ @@ -110,12 +114,16 @@ def connect(self, database=None, user=None, password=None, host=None, if ssh_host: defer_connect = True + client_flag = pymysql.constants.CLIENT.INTERACTIVE + if init_command and len(list(special.split_queries(init_command))) > 1: + client_flag |= pymysql.constants.CLIENT.MULTI_STATEMENTS + conn = pymysql.connect( database=db, user=user, password=password, host=host, port=port, unix_socket=socket, use_unicode=True, charset=charset, - autocommit=True, client_flag=pymysql.constants.CLIENT.INTERACTIVE, + autocommit=True, client_flag=client_flag, local_infile=local_infile, conv=conv, ssl=ssl, program_name="mycli", - defer_connect=defer_connect + defer_connect=defer_connect, init_command=init_command ) if ssh_host: @@ -146,6 +154,7 @@ def connect(self, database=None, user=None, password=None, host=None, self.socket = socket self.charset = charset self.ssl = ssl + self.init_command = init_command # retrieve connection id self.reset_connection_id() diff --git a/test/test_main.py b/test/test_main.py index 3f92bd1b..707c359b 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -492,3 +492,37 @@ def run_query(self, query, new_line=True): MockMyCli.connect_args["ssh_host"] == "arg_host" and \ MockMyCli.connect_args["ssh_port"] == 3 and \ MockMyCli.connect_args["ssh_key_filename"] == "/path/to/key" + + +@dbtest +def test_init_command_arg(executor): + init_command = "set sql_select_limit=1000" + sql = 'show variables like "sql_select_limit";' + runner = CliRunner() + result = runner.invoke( + cli, args=CLI_ARGS + ["--init-command", init_command], input=sql + ) + + expected = "sql_select_limit\t1000\n" + assert result.exit_code == 0 + assert expected in result.output + + +@dbtest +def test_init_command_multiple_arg(executor): + init_command = 'set sql_select_limit=2000; set max_join_size=20000' + sql = ( + 'show variables like "sql_select_limit";\n' + 'show variables like "max_join_size"' + ) + runner = CliRunner() + result = runner.invoke( + cli, args=CLI_ARGS + ['--init-command', init_command], input=sql + ) + + expected_sql_select_limit = 'sql_select_limit\t2000\n' + expected_max_join_size = 'max_join_size\t20000\n' + + assert result.exit_code == 0 + assert expected_sql_select_limit in result.output + assert expected_max_join_size in result.output From 7faffa2b49d346c251318ec2688cc8d1a575d8c2 Mon Sep 17 00:00:00 2001 From: KITAGAWA Yasutaka Date: Wed, 10 Jun 2020 21:55:05 +0900 Subject: [PATCH 092/202] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index efe804d8..d5a06879 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ $ sudo apt-get install mycli # Only on debian or ubuntu --local-infile BOOLEAN Enable/disable LOAD DATA LOCAL INFILE. --login-path TEXT Read this path from the login file. -e, --execute TEXT Execute command and quit. + --init-command TEXT SQL statement to execute after connecting. --help Show this message and exit. Features From a506476242adadb0cba7858215fef727f40ea78b Mon Sep 17 00:00:00 2001 From: KITAGAWA Yasutaka Date: Wed, 10 Jun 2020 22:05:30 +0900 Subject: [PATCH 093/202] Update changelog and AUTHORS --- changelog.md | 1 + mycli/AUTHORS | 1 + 2 files changed, 2 insertions(+) diff --git a/changelog.md b/changelog.md index d8b3c4d6..a7868463 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ Features: * Add an option `--ssh-config-host` to read ssh configuration from OpenSSH configuration file. * Add an option `--list-ssh-config` to list ssh configurations. * Add an option `--ssh-config-path` to choose ssh configuration path. +* Add an option `--init-command` to execute SQL after connecting (Thanks: [KITAGAWA Yasutaka]). 1.21.1 diff --git a/mycli/AUTHORS b/mycli/AUTHORS index b3636d90..ba548b14 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -72,6 +72,7 @@ Contributors: * Jakub Boukal * Takeshi D. Itoh * laixintao + * KITAGAWA Yasutaka Creator: -------- From 3a3f016fbf604b785ce3e5397fc59dfea006b07d Mon Sep 17 00:00:00 2001 From: Zach DeCook Date: Tue, 14 Jul 2020 10:30:53 -0400 Subject: [PATCH 094/202] * Command Line: Allow specifying empty password --- mycli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/main.py b/mycli/main.py index d298f202..8d97045e 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -393,7 +393,7 @@ def connect(self, database='', user='', passwd='', host='', port='', port = port or cnf['port'] ssl = ssl or {} - passwd = passwd or cnf['password'] + passwd = passwd if isinstance(passwd, str) else cnf['password'] charset = charset or cnf['default-character-set'] or 'utf8' # Favor whichever local_infile option is set. From a35ed9dfd8b2bbfd2ac59f4d35af2f695797855a Mon Sep 17 00:00:00 2001 From: Zach DeCook Date: Tue, 14 Jul 2020 10:36:32 -0400 Subject: [PATCH 095/202] - Specifying empty password: Add to changelog and add self to authors --- changelog.md | 6 ++++++ mycli/AUTHORS | 1 + 2 files changed, 7 insertions(+) diff --git a/changelog.md b/changelog.md index d8b3c4d6..99153343 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,11 @@ Features: * Add an option `--list-ssh-config` to list ssh configurations. * Add an option `--ssh-config-path` to choose ssh configuration path. +Bug Fixes: +---------- + +* Fix specifying empty password with `--password=''` when config file has a password set (Thanks: [Zach DeCook]). + 1.21.1 ====== @@ -757,3 +762,4 @@ Bug Fixes: [François Pietka]: https://github.com/fpietka [Frederic Aoustin]: https://github.com/fraoustin [Georgy Frolov]: https://github.com/pasenor +[Zach DeCook]: https://zachdecook.com diff --git a/mycli/AUTHORS b/mycli/AUTHORS index b3636d90..0eeb0899 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -72,6 +72,7 @@ Contributors: * Jakub Boukal * Takeshi D. Itoh * laixintao + * Zach DeCook Creator: -------- From 722d2e694812ec0b5153c436a75327db788b0513 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sat, 25 Jul 2020 08:21:46 -0600 Subject: [PATCH 096/202] Fix the pymysql breakage. --- mycli/sqlexecute.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mycli/sqlexecute.py b/mycli/sqlexecute.py index 035d98d1..c68af0fa 100644 --- a/mycli/sqlexecute.py +++ b/mycli/sqlexecute.py @@ -3,7 +3,7 @@ import sqlparse from .packages import special from pymysql.constants import FIELD_TYPE -from pymysql.converters import (convert_mysql_timestamp, convert_datetime, +from pymysql.converters import (convert_datetime, convert_timedelta, convert_date, conversions, decoders) try: @@ -99,7 +99,7 @@ def connect(self, database=None, user=None, password=None, host=None, ) conv = conversions.copy() conv.update({ - FIELD_TYPE.TIMESTAMP: lambda obj: (convert_mysql_timestamp(obj) or obj), + FIELD_TYPE.TIMESTAMP: lambda obj: (convert_datetime(obj) or obj), FIELD_TYPE.DATETIME: lambda obj: (convert_datetime(obj) or obj), FIELD_TYPE.TIME: lambda obj: (convert_timedelta(obj) or obj), FIELD_TYPE.DATE: lambda obj: (convert_date(obj) or obj), From a6f63cf5ffcb264314382eb9e68dbfd933e4da33 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sat, 25 Jul 2020 08:22:43 -0600 Subject: [PATCH 097/202] Fix the pymysql breakage. --- changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/changelog.md b/changelog.md index d8b3c4d6..1709d718 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,10 @@ TBD === +Bug Fixes: +---------- +* Fix the breaking change introduced in PyMySQL 0.10.0. (Thanks: [Amjith]). + Features: --------- * Add an option `--ssh-config-host` to read ssh configuration from OpenSSH configuration file. From 59ddbfeee1f35be903fec662cc51a0cbb2f5a424 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sun, 26 Jul 2020 00:16:28 +0300 Subject: [PATCH 098/202] fix tests --- test/test_sqlexecute.py | 2 +- test/test_tabular_output.py | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/test/test_sqlexecute.py b/test/test_sqlexecute.py index e445166f..c2d38be7 100644 --- a/test/test_sqlexecute.py +++ b/test/test_sqlexecute.py @@ -82,7 +82,7 @@ def test_invalid_syntax(executor): @dbtest def test_invalid_column_name(executor): - with pytest.raises(pymysql.InternalError) as excinfo: + with pytest.raises(pymysql.err.OperationalError) as excinfo: run(executor, 'select invalid command') assert "Unknown column 'invalid' in 'field list'" in str(excinfo.value) diff --git a/test/test_tabular_output.py b/test/test_tabular_output.py index fe7e5da3..7d7d000c 100644 --- a/test/test_tabular_output.py +++ b/test/test_tabular_output.py @@ -56,17 +56,18 @@ def description(self): [(None, None, None, 'Changed table format to sql-update')] mycli.formatter.query = "" output = mycli.format_output(None, FakeCursor(), headers) - assert "\n".join(output) == dedent('''\ + actual = "\n".join(output) + assert actual == dedent('''\ UPDATE `DUAL` SET `number` = 1 , `optional` = NULL - , `float` = 10 + , `float` = 10.0e0 , `binary` = X'aa' WHERE `letters` = 'abc'; UPDATE `DUAL` SET `number` = 456 , `optional` = '1' - , `float` = 0.5 + , `float` = 0.5e0 , `binary` = X'aabb' WHERE `letters` = 'd';''') # Test sql-update-2 output format @@ -77,12 +78,12 @@ def description(self): assert "\n".join(output) == dedent('''\ UPDATE `DUAL` SET `optional` = NULL - , `float` = 10 + , `float` = 10.0e0 , `binary` = X'aa' WHERE `letters` = 'abc' AND `number` = 1; UPDATE `DUAL` SET `optional` = '1' - , `float` = 0.5 + , `float` = 0.5e0 , `binary` = X'aabb' WHERE `letters` = 'd' AND `number` = 456;''') # Test sql-insert output format (without table name) @@ -92,8 +93,8 @@ def description(self): output = mycli.format_output(None, FakeCursor(), headers) assert "\n".join(output) == dedent('''\ INSERT INTO `DUAL` (`letters`, `number`, `optional`, `float`, `binary`) VALUES - ('abc', 1, NULL, 10, X'aa') - , ('d', 456, '1', 0.5, X'aabb') + ('abc', 1, NULL, 10.0e0, X'aa') + , ('d', 456, '1', 0.5e0, X'aabb') ;''') # Test sql-insert output format (with table name) assert list(mycli.change_table_format("sql-insert")) == \ @@ -102,8 +103,8 @@ def description(self): output = mycli.format_output(None, FakeCursor(), headers) assert "\n".join(output) == dedent('''\ INSERT INTO `table` (`letters`, `number`, `optional`, `float`, `binary`) VALUES - ('abc', 1, NULL, 10, X'aa') - , ('d', 456, '1', 0.5, X'aabb') + ('abc', 1, NULL, 10.0e0, X'aa') + , ('d', 456, '1', 0.5e0, X'aabb') ;''') # Test sql-insert output format (with database + table name) assert list(mycli.change_table_format("sql-insert")) == \ @@ -112,6 +113,6 @@ def description(self): output = mycli.format_output(None, FakeCursor(), headers) assert "\n".join(output) == dedent('''\ INSERT INTO `database`.`table` (`letters`, `number`, `optional`, `float`, `binary`) VALUES - ('abc', 1, NULL, 10, X'aa') - , ('d', 456, '1', 0.5, X'aabb') + ('abc', 1, NULL, 10.0e0, X'aa') + , ('d', 456, '1', 0.5e0, X'aabb') ;''') From a940755880501d21930e0c38e6bdf81f6c201f41 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sun, 26 Jul 2020 00:27:19 +0300 Subject: [PATCH 099/202] increase timeout in feature "auto_vertical on with large query" to pass CI --- test/features/steps/auto_vertical.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/features/steps/auto_vertical.py b/test/features/steps/auto_vertical.py index 6a67106a..974740d7 100644 --- a/test/features/steps/auto_vertical.py +++ b/test/features/steps/auto_vertical.py @@ -41,5 +41,5 @@ def step_see_large_results(context): '***************************\r\n' + '{}\r\n'.format('\r\n'.join(rows) + '\r\n')) - wrappers.expect_pager(context, expected, timeout=5) + wrappers.expect_pager(context, expected, timeout=10) wrappers.expect_exact(context, '1 row in set', timeout=2) From e46057f490683f4e7dd27e2c2c60bf8daccebd44 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sat, 25 Jul 2020 15:38:22 -0600 Subject: [PATCH 100/202] Releasing version 1.22.0 --- mycli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/__init__.py b/mycli/__init__.py index d8d8ff44..689aa86d 100644 --- a/mycli/__init__.py +++ b/mycli/__init__.py @@ -1 +1 @@ -__version__ = '1.21.1' +__version__ = '1.22.0' From 83cf3bf82812084855274c8a75f457a3d29f0759 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sat, 25 Jul 2020 15:40:24 -0600 Subject: [PATCH 101/202] Releasing version 1.22.1 --- mycli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/__init__.py b/mycli/__init__.py index 689aa86d..800d5195 100644 --- a/mycli/__init__.py +++ b/mycli/__init__.py @@ -1 +1 @@ -__version__ = '1.22.0' +__version__ = '1.22.1' From d0ea710d3d14544fe586b330b7eab019ff0936f3 Mon Sep 17 00:00:00 2001 From: KITAGAWA Yasutaka Date: Wed, 29 Jul 2020 08:07:53 +0900 Subject: [PATCH 102/202] Fix test --- test/test_tabular_output.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_tabular_output.py b/test/test_tabular_output.py index 7d7d000c..c20c7de2 100644 --- a/test/test_tabular_output.py +++ b/test/test_tabular_output.py @@ -16,7 +16,7 @@ @pytest.fixture def mycli(): cli = MyCli() - cli.connect(None, USER, PASSWORD, HOST, PORT, None) + cli.connect(None, USER, PASSWORD, HOST, PORT, None, init_command=None) return cli From 0fda13f72fdfb95ad25325b91333a3faeb3102cd Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sat, 1 Aug 2020 13:10:08 -0600 Subject: [PATCH 103/202] Make pwd module optional. --- mycli/main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mycli/main.py b/mycli/main.py index d298f202..03797a0f 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -6,7 +6,10 @@ import re import fileinput from collections import namedtuple -from pwd import getpwuid +try: + from pwd import getpwuid +except ImportError: + pass from time import time from datetime import datetime from random import choice From a9ac591a9b602d23e689ee9c40e25f981e72ab45 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sat, 1 Aug 2020 13:13:38 -0600 Subject: [PATCH 104/202] Changelog update. --- changelog.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/changelog.md b/changelog.md index 1709d718..ba3075a5 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,13 @@ TBD === +Bug Fixes: +---------- +* Make the `pwd` module optional. + +1.22.1 +====== + Bug Fixes: ---------- * Fix the breaking change introduced in PyMySQL 0.10.0. (Thanks: [Amjith]). From 97d01693284173fcff4ef21de4fea561a57cb445 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Mon, 3 Aug 2020 10:11:48 -0600 Subject: [PATCH 105/202] Changelog update. --- changelog.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index ba3075a5..a4fea354 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,5 @@ -TBD -=== +1.22.2 +====== Bug Fixes: ---------- From 00ce216034c4c3b101d40347f4e8f2e601ae04b5 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Mon, 3 Aug 2020 10:12:16 -0600 Subject: [PATCH 106/202] Releasing version 1.22.2 --- mycli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/__init__.py b/mycli/__init__.py index 800d5195..53bfe2e0 100644 --- a/mycli/__init__.py +++ b/mycli/__init__.py @@ -1 +1 @@ -__version__ = '1.22.1' +__version__ = '1.22.2' From 7cebe7399a1692aef409fa16ccf46e70e8c590bd Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Mon, 3 Aug 2020 10:21:27 -0600 Subject: [PATCH 107/202] Changelog update. --- changelog.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 4800248b..1694646a 100644 --- a/changelog.md +++ b/changelog.md @@ -1,8 +1,17 @@ +TBD +=== + +Features: +--------- + +* Add an option `--init-command` to execute SQL after connecting (Thanks: [KITAGAWA Yasutaka]). + 1.22.2 ====== Bug Fixes: ---------- + * Make the `pwd` module optional. 1.22.1 @@ -17,7 +26,6 @@ Features: * Add an option `--ssh-config-host` to read ssh configuration from OpenSSH configuration file. * Add an option `--list-ssh-config` to list ssh configurations. * Add an option `--ssh-config-path` to choose ssh configuration path. -* Add an option `--init-command` to execute SQL after connecting (Thanks: [KITAGAWA Yasutaka]). 1.21.1 From 8e94465699dcfafdc98c1191c662c48f18fa3f3a Mon Sep 17 00:00:00 2001 From: Nicolas Palumbo Date: Tue, 1 Sep 2020 10:19:41 +0200 Subject: [PATCH 108/202] Add shortcut to login-path parameter. --- mycli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/main.py b/mycli/main.py index e0115bbe..57daaa91 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1047,7 +1047,7 @@ def get_last_query(self): help='Warn before running a destructive query.') @click.option('--local-infile', type=bool, help='Enable/disable LOAD DATA LOCAL INFILE.') -@click.option('--login-path', type=str, +@click.option('-L', '--login-path', type=str, help='Read this path from the login file.') @click.option('-e', '--execute', type=str, help='Execute command and quit.') From a8956bf6f046f317c809d00b51748a2698febb27 Mon Sep 17 00:00:00 2001 From: Nicolas Palumbo Date: Tue, 1 Sep 2020 11:07:38 +0200 Subject: [PATCH 109/202] Change shortcut to -g to avoid conflicts. --- mycli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/main.py b/mycli/main.py index 57daaa91..4200c5a9 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1047,7 +1047,7 @@ def get_last_query(self): help='Warn before running a destructive query.') @click.option('--local-infile', type=bool, help='Enable/disable LOAD DATA LOCAL INFILE.') -@click.option('-L', '--login-path', type=str, +@click.option('-g', '--login-path', type=str, help='Read this path from the login file.') @click.option('-e', '--execute', type=str, help='Execute command and quit.') From 5a096020f157462cf0250d3232ac4461c66c7ded Mon Sep 17 00:00:00 2001 From: Nicolas Palumbo Date: Tue, 1 Sep 2020 11:10:23 +0200 Subject: [PATCH 110/202] Update changelog and AUTHORS --- changelog.md | 3 ++- mycli/AUTHORS | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 1a4ae2c0..14cdaab9 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,7 @@ Features: --------- * Add an option `--init-command` to execute SQL after connecting (Thanks: [KITAGAWA Yasutaka]). +* Add `-g` shortcut to option `--login-path`. 1.22.2 ====== @@ -12,7 +13,7 @@ Features: Bug Fixes: ---------- -* Make the `pwd` module optional. +* Make the `pwd` module optional. 1.22.1 ====== diff --git a/mycli/AUTHORS b/mycli/AUTHORS index c50a15cd..77026512 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -75,6 +75,7 @@ Contributors: * Zach DeCook * kevinhwang91 * KITAGAWA Yasutaka + * Nicolas Palumbo Creator: -------- From 07420f024a1c97bd8b63f1fc58cdc0c667ec4362 Mon Sep 17 00:00:00 2001 From: Ivan Date: Thu, 3 Sep 2020 14:17:55 +0000 Subject: [PATCH 111/202] Use InputMode.REPLACE_SINGLE InputMode.REPLACE_SINGLE was added to prompt-toolkit. This fixes `Exception ` in single-char replace mode when using prompt_toolkit>=3.0.6. --- changelog.md | 1 + mycli/AUTHORS | 1 + mycli/clitoolbar.py | 1 + setup.py | 2 +- 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 1a4ae2c0..df044e2f 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,7 @@ Features: --------- * Add an option `--init-command` to execute SQL after connecting (Thanks: [KITAGAWA Yasutaka]). +* Use InputMode.REPLACE_SINGLE 1.22.2 ====== diff --git a/mycli/AUTHORS b/mycli/AUTHORS index c50a15cd..89be14de 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -75,6 +75,7 @@ Contributors: * Zach DeCook * kevinhwang91 * KITAGAWA Yasutaka + * bitkeen Creator: -------- diff --git a/mycli/clitoolbar.py b/mycli/clitoolbar.py index e03e1826..eec2978f 100644 --- a/mycli/clitoolbar.py +++ b/mycli/clitoolbar.py @@ -48,5 +48,6 @@ def _get_vi_mode(): InputMode.INSERT: 'I', InputMode.NAVIGATION: 'N', InputMode.REPLACE: 'R', + InputMode.REPLACE_SINGLE: 'R', InputMode.INSERT_MULTIPLE: 'M', }[get_app().vi_state.input_mode] diff --git a/setup.py b/setup.py index 1f355ba6..8d10109a 100755 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ install_requirements = [ 'click >= 7.0', 'Pygments >= 1.6', - 'prompt_toolkit>=3.0.0,<4.0.0', + 'prompt_toolkit>=3.0.6,<4.0.0', 'PyMySQL >= 0.9.2', 'sqlparse>=0.3.0,<0.4.0', 'configobj >= 5.0.5', From 75a885c2534b3dda3a8a62dd9167431ce615ed48 Mon Sep 17 00:00:00 2001 From: morgan Date: Thu, 3 Sep 2020 12:58:24 -0400 Subject: [PATCH 112/202] no get on ipython-sql Connection --- mycli/magic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/magic.py b/mycli/magic.py index 5527f72d..b1a3268a 100644 --- a/mycli/magic.py +++ b/mycli/magic.py @@ -19,7 +19,7 @@ def load_ipython_extension(ipython): def mycli_line_magic(line): _logger.debug('mycli magic called: %r', line) parsed = sql.parse.parse(line, {}) - conn = sql.connection.Connection.get(parsed['connection']) + conn = sql.connection.Connection(parsed['connection']) try: # A corresponding mycli object already exists From 659a5b7211f57326372baaa92d1124be8cd7192a Mon Sep 17 00:00:00 2001 From: morgan Date: Thu, 3 Sep 2020 13:04:33 -0400 Subject: [PATCH 113/202] AUTHORS file + changelog --- changelog.md | 1 + mycli/AUTHORS | 1 + 2 files changed, 2 insertions(+) diff --git a/changelog.md b/changelog.md index 1a4ae2c0..62d7c055 100644 --- a/changelog.md +++ b/changelog.md @@ -13,6 +13,7 @@ Bug Fixes: ---------- * Make the `pwd` module optional. +* Fixed iPython magic 1.22.1 ====== diff --git a/mycli/AUTHORS b/mycli/AUTHORS index c50a15cd..af72f12c 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -75,6 +75,7 @@ Contributors: * Zach DeCook * kevinhwang91 * KITAGAWA Yasutaka + * Morgan Mitchell Creator: -------- From b3eca6241c82614f1c11decb84f6a9850f766fc7 Mon Sep 17 00:00:00 2001 From: morgan Date: Thu, 3 Sep 2020 13:05:10 -0400 Subject: [PATCH 114/202] spacing --- mycli/AUTHORS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/AUTHORS b/mycli/AUTHORS index af72f12c..d07bce2d 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -75,7 +75,7 @@ Contributors: * Zach DeCook * kevinhwang91 * KITAGAWA Yasutaka - * Morgan Mitchell + * Morgan Mitchell Creator: -------- From 8edc588cffa075a73975f23d4c6d759cb7cecf92 Mon Sep 17 00:00:00 2001 From: Andy Teijelo Date: Tue, 20 Oct 2020 10:53:14 -0400 Subject: [PATCH 115/202] Call expanduser before passing paths down to the config module --- mycli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/main.py b/mycli/main.py index e0115bbe..b5e2a0c5 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -88,7 +88,7 @@ class MyCli(object): '/etc/my.cnf', '/etc/mysql/my.cnf', '/usr/local/etc/my.cnf', - '~/.my.cnf' + os.path.expanduser('~/.my.cnf'), ] # check XDG_CONFIG_HOME exists and not an empty string From d69e86850f6d96771327703a003359dc95eec6ca Mon Sep 17 00:00:00 2001 From: Andy Teijelo Date: Tue, 20 Oct 2020 10:56:03 -0400 Subject: [PATCH 116/202] Update changelog and AUTHORS --- changelog.md | 4 ++++ mycli/AUTHORS | 1 + 2 files changed, 5 insertions(+) diff --git a/changelog.md b/changelog.md index 1a4ae2c0..eff4b4da 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,10 @@ TBD === +Bug Fixes: +---------- +* Fix config file include logic + Features: --------- diff --git a/mycli/AUTHORS b/mycli/AUTHORS index c50a15cd..49b65559 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -75,6 +75,7 @@ Contributors: * Zach DeCook * kevinhwang91 * KITAGAWA Yasutaka + * Andy Teijelo Pérez Creator: -------- From 17f093d7b70ab2d9f3c6eababa041bf76f029aac Mon Sep 17 00:00:00 2001 From: Massimiliano Torromeo Date: Thu, 22 Oct 2020 01:16:29 +0200 Subject: [PATCH 117/202] Remove unnecessary use of python 2 compat type (#900) Since python 2 compat has been removed and sqlparse 0.4 also doesn't ship with the compat module anymore, there is no reason to use the text_type alias for str/unicode. --- changelog.md | 7 ++++++- mycli/AUTHORS | 1 + mycli/packages/completion_engine.py | 3 +-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index 1a4ae2c0..508c8013 100644 --- a/changelog.md +++ b/changelog.md @@ -6,13 +6,17 @@ Features: * Add an option `--init-command` to execute SQL after connecting (Thanks: [KITAGAWA Yasutaka]). +Bug Fixes: +---------- +* Fixed compatibility with sqlparse 0.4 (Thanks: [mtorromeo]). + 1.22.2 ====== Bug Fixes: ---------- -* Make the `pwd` module optional. +* Make the `pwd` module optional. 1.22.1 ====== @@ -785,3 +789,4 @@ Bug Fixes: [Georgy Frolov]: https://github.com/pasenor [Zach DeCook]: https://zachdecook.com [laixintao]: https://github.com/laixintao +[mtorromeo]: https://github.com/mtorromeo diff --git a/mycli/AUTHORS b/mycli/AUTHORS index c50a15cd..a1204b02 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -75,6 +75,7 @@ Contributors: * Zach DeCook * kevinhwang91 * KITAGAWA Yasutaka + * Massimiliano Torromeo Creator: -------- diff --git a/mycli/packages/completion_engine.py b/mycli/packages/completion_engine.py index 2b19c32d..3cff2ccc 100644 --- a/mycli/packages/completion_engine.py +++ b/mycli/packages/completion_engine.py @@ -2,7 +2,6 @@ import sys import sqlparse from sqlparse.sql import Comparison, Identifier, Where -from sqlparse.compat import text_type from .parseutils import last_word, extract_tables, find_prev_keyword from .special import parse_special_command @@ -55,7 +54,7 @@ def suggest_type(full_text, text_before_cursor): stmt_start, stmt_end = 0, 0 for statement in parsed: - stmt_len = len(text_type(statement)) + stmt_len = len(str(statement)) stmt_start, stmt_end = stmt_end, stmt_end + stmt_len if stmt_end >= current_pos: From d976fbd2523262a51155a86be11fcc4655bf88c9 Mon Sep 17 00:00:00 2001 From: Irina Truong Date: Mon, 26 Oct 2020 10:51:10 -0700 Subject: [PATCH 118/202] Fix behave tests on master. (#904) * Possibly fix behave tests on master. * Run behave with --no-capture. --- .travis.yml | 2 +- mycli/main.py | 2 +- requirements-dev.txt | 4 ++-- setup.py | 2 +- test/features/environment.py | 5 +++-- test/features/steps/crud_database.py | 11 +++-------- test/features/steps/wrappers.py | 2 +- 7 files changed, 12 insertions(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0afb5cc2..182dea73 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ matrix: install: - pip install -r requirements-dev.txt - - pip install -e . + - pip install --no-cache-dir -e . - sudo rm -f /etc/mysql/conf.d/performance-schema.cnf - sudo service mysql restart diff --git a/mycli/main.py b/mycli/main.py index e0115bbe..dffd7246 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -152,7 +152,7 @@ def __init__(self, sqlexecute=None, prompt=None, c['main'].as_bool('auto_vertical_output') # Write user config if system config wasn't the last config loaded. - if c.filename not in self.system_config_files: + if c.filename not in self.system_config_files and not os.path.exists(myclirc): write_default_config(self.default_config_file, myclirc) # audit log diff --git a/requirements-dev.txt b/requirements-dev.txt index 8e206a56..7a38ed5c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,8 +3,8 @@ pytest!=3.3.0 pytest-cov==2.4.0 tox twine==1.12.1 -behave -pexpect +behave>=1.2.4 +pexpect==3.3 coverage==5.0.4 codecov==2.0.9 autopep8==1.3.3 diff --git a/setup.py b/setup.py index 1f355ba6..fbab22ef 100755 --- a/setup.py +++ b/setup.py @@ -65,7 +65,7 @@ class test(TestCommand): def initialize_options(self): TestCommand.initialize_options(self) self.pytest_args = '' - self.behave_args = '' + self.behave_args = '--no-capture' def run_tests(self): unit_test_errno = subprocess.call( diff --git a/test/features/environment.py b/test/features/environment.py index 1a49dbed..cb351403 100644 --- a/test/features/environment.py +++ b/test/features/environment.py @@ -16,7 +16,7 @@ def before_all(context): os.environ['LINES'] = "100" os.environ['COLUMNS'] = "100" os.environ['EDITOR'] = 'ex' - os.environ['LC_ALL'] = 'en_US.utf8' + os.environ['LC_ALL'] = 'en_US.UTF-8' os.environ['PROMPT_TOOLKIT_NO_CPR'] = '1' test_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) @@ -118,11 +118,12 @@ def after_scenario(context, _): host = context.conf['host'] dbname = context.currentdb context.cli.expect_exact( - '{0}@{1}:{2}> '.format( + '{0}@{1}:{2}>'.format( user, host, dbname ), timeout=5 ) + context.cli.sendcontrol('c') context.cli.sendcontrol('d') context.cli.expect_exact(pexpect.EOF, timeout=5) diff --git a/test/features/steps/crud_database.py b/test/features/steps/crud_database.py index a0bfa530..be6dec05 100644 --- a/test/features/steps/crud_database.py +++ b/test/features/steps/crud_database.py @@ -64,15 +64,13 @@ def step_see_prompt(context): user = context.conf['user'] host = context.conf['host'] dbname = context.currentdb - wrappers.expect_exact(context, '{0}@{1}:{2}> '.format( - user, host, dbname), timeout=5) - context.atprompt = True + wrappers.wait_prompt(context, '{0}@{1}:{2}> '.format(user, host, dbname)) @then('we see help output') def step_see_help(context): for expected_line in context.fixture_data['help_commands.txt']: - wrappers.expect_exact(context, expected_line + '\r\n', timeout=1) + wrappers.expect_exact(context, expected_line, timeout=1) @then('we see database created') @@ -96,10 +94,7 @@ def step_see_db_dropped_no_default(context): context.currentdb = None wrappers.expect_exact(context, 'Query OK, 0 rows affected', timeout=2) - wrappers.expect_exact(context, '{0}@{1}:{2}> '.format( - user, host, database), timeout=5) - - context.atprompt = True + wrappers.wait_prompt(context, '{0}@{1}:{2}>'.format(user, host, database)) @then('we see database connected') diff --git a/test/features/steps/wrappers.py b/test/features/steps/wrappers.py index 565ca591..de833dd2 100644 --- a/test/features/steps/wrappers.py +++ b/test/features/steps/wrappers.py @@ -88,7 +88,7 @@ def wait_prompt(context, prompt=None): user = context.conf['user'] host = context.conf['host'] dbname = context.currentdb - prompt = '{0}@{1}:{2}> '.format( + prompt = '{0}@{1}:{2}>'.format( user, host, dbname), expect_exact(context, prompt, timeout=5) context.atprompt = True From b1768ff27fa958fe3d73bc65a0decfb99312228c Mon Sep 17 00:00:00 2001 From: Seamile Date: Tue, 29 Dec 2020 11:12:32 +0800 Subject: [PATCH 119/202] Add commandline option charset --- mycli/main.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index dffd7246..c06bb0f0 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1053,6 +1053,7 @@ def get_last_query(self): help='Execute command and quit.') @click.option('--init-command', type=str, help='SQL statement to execute after connecting.') +@click.option('--charset', help='Character set for MySQL Client.') @click.argument('database', default='', nargs=1) def cli(database, user, host, port, socket, password, dbname, version, verbose, prompt, logfile, defaults_group_suffix, @@ -1061,7 +1062,7 @@ def cli(database, user, host, port, socket, password, dbname, ssl_verify_server_cert, table, csv, warn, execute, myclirc, dsn, list_dsn, ssh_user, ssh_host, ssh_port, ssh_password, ssh_key_filename, list_ssh_config, ssh_config_path, ssh_config_host, - init_command): + init_command, charset): """A MySQL terminal client with auto-completion and syntax highlighting. \b @@ -1186,7 +1187,8 @@ def cli(database, user, host, port, socket, password, dbname, ssh_port=ssh_port, ssh_password=ssh_password, ssh_key_filename=ssh_key_filename, - init_command=init_command + init_command=init_command, + charset=charset ) mycli.logger.debug('Launch Params: \n' From 0f0c66deb7b83c859f7a33f02037ef9510d08006 Mon Sep 17 00:00:00 2001 From: Seamile Date: Tue, 29 Dec 2020 18:07:49 +0800 Subject: [PATCH 120/202] modify `changelog.md` and `AUTHORS` --- changelog.md | 3 ++- mycli/AUTHORS | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index b296c87b..d6bc29b9 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ Features: * Add an option `--init-command` to execute SQL after connecting (Thanks: [KITAGAWA Yasutaka]). * Use InputMode.REPLACE_SINGLE +* Add an option `--charset` to set the default charset when connect database. Bug Fixes: ---------- @@ -18,7 +19,7 @@ Bug Fixes: Bug Fixes: ---------- -* Make the `pwd` module optional. +* Make the `pwd` module optional. 1.22.1 ====== diff --git a/mycli/AUTHORS b/mycli/AUTHORS index 58e1b82e..02a7a4ba 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -78,6 +78,7 @@ Contributors: * bitkeen * Morgan Mitchell * Massimiliano Torromeo + * Seamile Creator: -------- From 6b863a528f4fb4da05a7f2a42b5f4559346b3e38 Mon Sep 17 00:00:00 2001 From: Seamile Date: Thu, 31 Dec 2020 22:36:11 +0800 Subject: [PATCH 121/202] modify the help text --- README.md | 1 + mycli/main.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d5a06879..47efe5f2 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ $ sudo apt-get install mycli # Only on debian or ubuntu --login-path TEXT Read this path from the login file. -e, --execute TEXT Execute command and quit. --init-command TEXT SQL statement to execute after connecting. + --charset TEXT Character set for MySQL session. --help Show this message and exit. Features diff --git a/mycli/main.py b/mycli/main.py index c06bb0f0..da33d665 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1053,7 +1053,8 @@ def get_last_query(self): help='Execute command and quit.') @click.option('--init-command', type=str, help='SQL statement to execute after connecting.') -@click.option('--charset', help='Character set for MySQL Client.') +@click.option('--charset', type=str, + help='Character set for MySQL session.') @click.argument('database', default='', nargs=1) def cli(database, user, host, port, socket, password, dbname, version, verbose, prompt, logfile, defaults_group_suffix, From 0b927d7b5fe0dc6aef81fe7916fbdee946846d61 Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Fri, 1 Jan 2021 09:52:28 -0500 Subject: [PATCH 122/202] add clip special command --- changelog.md | 1 + mycli/AUTHORS | 1 + mycli/main.py | 27 +++++++++++++ mycli/packages/special/iocommands.py | 42 ++++++++++++++++++++ mycli/packages/special/main.py | 2 + setup.py | 1 + test/features/fixture_data/help_commands.txt | 1 + 7 files changed, 75 insertions(+) diff --git a/changelog.md b/changelog.md index b296c87b..f3b005f7 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ Features: * Add an option `--init-command` to execute SQL after connecting (Thanks: [KITAGAWA Yasutaka]). * Use InputMode.REPLACE_SINGLE +* Add a `\clip` special command to copy queries to the system clipboard. Bug Fixes: ---------- diff --git a/mycli/AUTHORS b/mycli/AUTHORS index 58e1b82e..a5e8d689 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -78,6 +78,7 @@ Contributors: * bitkeen * Morgan Mitchell * Massimiliano Torromeo + * Roland Walker Creator: -------- diff --git a/mycli/main.py b/mycli/main.py index dffd7246..b4f81900 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -511,6 +511,24 @@ def handle_editor_command(self, text): continue return text + def handle_clip_command(self, text): + """A clip command is any query that is prefixed or suffixed by a + '\clip'. + + :param text: Document + :return: Boolean + + """ + + if special.clip_command(text): + query = (special.get_clip_query(text) or + self.get_last_query()) + message = special.copy_query_to_clipboard(sql=query) + if message: + raise RuntimeError(message) + return True + return False + def run_cli(self): iterations = 0 sqlexecute = self.sqlexecute @@ -580,6 +598,15 @@ def one_iteration(text=None): self.echo(str(e), err=True, fg='red') return + try: + if self.handle_clip_command(text): + return + except RuntimeError as e: + logger.error("sql: %r, error: %r", text, e) + logger.error("traceback: %r", traceback.format_exc()) + self.echo(str(e), err=True, fg='red') + return + if not text.strip(): return diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index 11dca8d0..e0f6d130 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -8,6 +8,7 @@ from time import sleep import click +import pyperclip import sqlparse from . import export @@ -159,6 +160,47 @@ def open_external_editor(filename=None, sql=None): return (query, message) +@export +def clip_command(command): + """Is this a clip command? + + :param command: string + + """ + # It is possible to have `\clip` or `SELECT * FROM \clip`. So we check + # for both conditions. + return command.strip().endswith('\\clip') or command.strip().startswith('\\clip') + + +@export +def get_clip_query(sql): + """Get the query part of a clip command.""" + sql = sql.strip() + + # The reason we can't simply do .strip('\clip') is that it strips characters, + # not a substring. So it'll strip "c" in the end of the sql also! + pattern = re.compile('(^\\\clip|\\\clip$)') + while pattern.search(sql): + sql = pattern.sub('', sql) + + return sql + + +@export +def copy_query_to_clipboard(sql=None): + """Send query to the clipboard.""" + + sql = sql or '' + message = None + + try: + pyperclip.copy(u'{sql}'.format(sql=sql)) + except RuntimeError as e: + message = 'Error clipping query: %s.' % e.strerror + + return message + + @special_command('\\f', '\\f [name [args..]]', 'List or execute favorite queries.', arg_type=PARSED_QUERY, case_sensitive=True) def execute_favorite_query(cur, arg, **_): """Returns (title, rows, headers, status)""" diff --git a/mycli/packages/special/main.py b/mycli/packages/special/main.py index dddba66a..ab04f30d 100644 --- a/mycli/packages/special/main.py +++ b/mycli/packages/special/main.py @@ -112,6 +112,8 @@ def quit(*_args): @special_command('\\e', '\\e', 'Edit command with editor (uses $EDITOR).', arg_type=NO_QUERY, case_sensitive=True) +@special_command('\\clip', '\\clip', 'Copy query to the system clipboard.', + arg_type=NO_QUERY, case_sensitive=True) @special_command('\\G', '\\G', 'Display current query results vertically.', arg_type=NO_QUERY, case_sensitive=True) def stub(): diff --git a/setup.py b/setup.py index 8c68ddb2..51322bd5 100755 --- a/setup.py +++ b/setup.py @@ -25,6 +25,7 @@ 'configobj >= 5.0.5', 'cryptography >= 1.0.0', 'cli_helpers[styles] >= 2.0.1', + 'pyperclip >= 1.8.1' ] diff --git a/test/features/fixture_data/help_commands.txt b/test/features/fixture_data/help_commands.txt index 657db7da..7bd72e90 100644 --- a/test/features/fixture_data/help_commands.txt +++ b/test/features/fixture_data/help_commands.txt @@ -2,6 +2,7 @@ | Command | Shortcut | Description | +-------------+----------------------------+------------------------------------------------------------+ | \G | \G | Display current query results vertically. | +| \clip | \clip | Copy query to the system clipboard. | | \dt | \dt[+] [table] | List or describe tables. | | \e | \e | Edit command with editor (uses $EDITOR). | | \f | \f [name [args..]] | List or execute favorite queries. | From 337a65c160ef9bf9d0592b70d02c06f211b631e9 Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Fri, 1 Jan 2021 10:04:49 -0500 Subject: [PATCH 123/202] send diagnostic socket message to STDERR --- changelog.md | 1 + mycli/AUTHORS | 1 + mycli/main.py | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index b296c87b..d544f337 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,7 @@ Bug Fixes: ---------- * Fixed compatibility with sqlparse 0.4 (Thanks: [mtorromeo]). * Fixed iPython magic (Thanks: [mwcm]). +* Send "Connecting to socket" message to the standard error. 1.22.2 ====== diff --git a/mycli/AUTHORS b/mycli/AUTHORS index 58e1b82e..a5e8d689 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -78,6 +78,7 @@ Contributors: * bitkeen * Morgan Mitchell * Massimiliano Torromeo + * Roland Walker Creator: -------- diff --git a/mycli/main.py b/mycli/main.py index dffd7246..9c2b7a04 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -438,7 +438,7 @@ def _connect(): if not WIN and socket: socket_owner = getpwuid(os.stat(socket).st_uid).pw_name self.echo( - f"Connecting to socket {socket}, owned by user {socket_owner}") + f"Connecting to socket {socket}, owned by user {socket_owner}", err=True) try: _connect() except OperationalError as e: From b111ca9bf42dd892cce798b2d9ac8ca7d300acc4 Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Fri, 1 Jan 2021 11:14:55 -0500 Subject: [PATCH 124/202] prefer raw strings for regular expressions --- mycli/AUTHORS | 1 + mycli/main.py | 2 +- mycli/packages/parseutils.py | 2 +- mycli/packages/special/iocommands.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mycli/AUTHORS b/mycli/AUTHORS index 58e1b82e..a5e8d689 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -78,6 +78,7 @@ Contributors: * bitkeen * Morgan Mitchell * Massimiliano Torromeo + * Roland Walker Creator: -------- diff --git a/mycli/main.py b/mycli/main.py index dffd7246..f9013129 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1291,7 +1291,7 @@ def is_select(status): def thanks_picker(files=()): contents = [] for line in fileinput.input(files=files): - m = re.match('^ *\* (.*)', line) + m = re.match(r'^ *\* (.*)', line) if m: contents.append(m.group(1)) return choice(contents) diff --git a/mycli/packages/parseutils.py b/mycli/packages/parseutils.py index e3b383e5..6150a021 100644 --- a/mycli/packages/parseutils.py +++ b/mycli/packages/parseutils.py @@ -11,7 +11,7 @@ # This matches everything except spaces, parens, colon, comma, and period 'most_punctuations': re.compile(r'([^\.():,\s]+)$'), # This matches everything except a space. - 'all_punctuations': re.compile('([^\s]+)$'), + 'all_punctuations': re.compile(r'([^\s]+)$'), } def last_word(text, include='alphanum_underscore'): diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index 11dca8d0..b5301fe5 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -115,7 +115,7 @@ def get_editor_query(sql): # The reason we can't simply do .strip('\e') is that it strips characters, # not a substring. So it'll strip "e" in the end of the sql also! # Ex: "select * from style\e" -> "select * from styl". - pattern = re.compile('(^\\\e|\\\e$)') + pattern = re.compile(r'(^\\e|\\e$)') while pattern.search(sql): sql = pattern.sub('', sql) From 30bc8b725a6c81d7f71f51c3d69c5c5422bfb950 Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Fri, 1 Jan 2021 10:26:47 -0500 Subject: [PATCH 125/202] fix \once -o to overwrite output whole instead of overwriting line-by-line --- changelog.md | 1 + mycli/AUTHORS | 1 + mycli/packages/special/iocommands.py | 23 +++++++++++------------ test/test_special_iocommands.py | 8 ++++---- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/changelog.md b/changelog.md index b296c87b..c7f2ad99 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,7 @@ Bug Fixes: ---------- * Fixed compatibility with sqlparse 0.4 (Thanks: [mtorromeo]). * Fixed iPython magic (Thanks: [mwcm]). +* Fix \once -o to overwrite output whole, instead of line-by-line. 1.22.2 ====== diff --git a/mycli/AUTHORS b/mycli/AUTHORS index 58e1b82e..a5e8d689 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -78,6 +78,7 @@ Contributors: * bitkeen * Morgan Mitchell * Massimiliano Torromeo + * Roland Walker Creator: -------- diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index 11dca8d0..359e6161 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -337,7 +337,11 @@ def write_tee(output): def set_once(arg, **_): global once_file, written_to_once_file - once_file = parseargfile(arg) + try: + once_file = open(**parseargfile(arg)) + except (IOError, OSError) as e: + raise OSError("Cannot write to file '{}': {}".format( + e.filename, e.strerror)) written_to_once_file = False return [(None, None, None, "")] @@ -347,23 +351,18 @@ def set_once(arg, **_): def write_once(output): global once_file, written_to_once_file if output and once_file: - try: - f = open(**once_file) - except (IOError, OSError) as e: - once_file = None - raise OSError("Cannot write to file '{}': {}".format( - e.filename, e.strerror)) - with f: - click.echo(output, file=f, nl=False) - click.echo(u"\n", file=f, nl=False) + click.echo(output, file=once_file, nl=False) + click.echo(u"\n", file=once_file, nl=False) + once_file.flush() written_to_once_file = True @export def unset_once_if_written(): """Unset the once file, if it has been written to.""" - global once_file - if written_to_once_file: + global once_file, written_to_once_file + if written_to_once_file and once_file: + once_file.close() once_file = None diff --git a/test/test_special_iocommands.py b/test/test_special_iocommands.py index b8b3acb3..de7b0c84 100644 --- a/test/test_special_iocommands.py +++ b/test/test_special_iocommands.py @@ -93,9 +93,8 @@ def test_once_command(): with pytest.raises(TypeError): mycli.packages.special.execute(None, u"\\once") - mycli.packages.special.execute(None, u"\\once /proc/access-denied") with pytest.raises(OSError): - mycli.packages.special.write_once(u"hello world") + mycli.packages.special.execute(None, u"\\once /proc/access-denied") mycli.packages.special.write_once(u"hello world") # write without file set with tempfile.NamedTemporaryFile() as f: @@ -104,9 +103,10 @@ def test_once_command(): assert f.read() == b"hello world\n" mycli.packages.special.execute(None, u"\\once -o " + f.name) - mycli.packages.special.write_once(u"hello world") + mycli.packages.special.write_once(u"hello world line 1") + mycli.packages.special.write_once(u"hello world line 2") f.seek(0) - assert f.read() == b"hello world\n" + assert f.read() == b"hello world line 1\nhello world line 2\n" def test_parseargfile(): From 8dae78a66379c82f5b170fa3dc1182e5b140f069 Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Fri, 1 Jan 2021 16:59:52 -0500 Subject: [PATCH 126/202] respect empty string value for prompt_continuation --- changelog.md | 1 + mycli/AUTHORS | 1 + mycli/main.py | 4 +++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index b296c87b..25b3762a 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,7 @@ Bug Fixes: ---------- * Fixed compatibility with sqlparse 0.4 (Thanks: [mtorromeo]). * Fixed iPython magic (Thanks: [mwcm]). +* Respect `prompt_continuation = ''` in `.myclirc` 1.22.2 ====== diff --git a/mycli/AUTHORS b/mycli/AUTHORS index 58e1b82e..a5e8d689 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -78,6 +78,7 @@ Contributors: * bitkeen * Morgan Mitchell * Massimiliano Torromeo + * Roland Walker Creator: -------- diff --git a/mycli/main.py b/mycli/main.py index dffd7246..6e936375 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -551,7 +551,9 @@ def get_message(): return [('class:prompt', prompt)] def get_continuation(width, *_): - if self.multiline_continuation_char: + if self.multiline_continuation_char == '': + continuation = '' + elif self.multiline_continuation_char: left_padding = width - len(self.multiline_continuation_char) continuation = " " * \ max((left_padding - 1), 0) + \ From fb32555c7e1126b71a1e3fae8c7ed5f502540102 Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Sat, 2 Jan 2021 10:57:34 -0500 Subject: [PATCH 127/202] customization of Pygments SQL highlight styles --- changelog.md | 1 + mycli/AUTHORS | 1 + mycli/clistyle.py | 33 +++++++++++++++++++++++++++++++++ mycli/myclirc | 30 ++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+) diff --git a/changelog.md b/changelog.md index b296c87b..86415ade 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ Features: * Add an option `--init-command` to execute SQL after connecting (Thanks: [KITAGAWA Yasutaka]). * Use InputMode.REPLACE_SINGLE +* Allow customization of Pygments SQL syntax-highlighting styles. Bug Fixes: ---------- diff --git a/mycli/AUTHORS b/mycli/AUTHORS index 58e1b82e..a5e8d689 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -78,6 +78,7 @@ Contributors: * bitkeen * Morgan Mitchell * Massimiliano Torromeo + * Roland Walker Creator: -------- diff --git a/mycli/clistyle.py b/mycli/clistyle.py index 293f0f43..b0ac9922 100644 --- a/mycli/clistyle.py +++ b/mycli/clistyle.py @@ -44,6 +44,36 @@ v: k for k, v in TOKEN_TO_PROMPT_STYLE.items() } +# all tokens that the Pygments MySQL lexer can produce +OVERRIDE_STYLE_TO_TOKEN = { + 'sql.comment': Token.Comment, + 'sql.comment.multi-line': Token.Comment.Multiline, + 'sql.comment.single-line': Token.Comment.Single, + 'sql.comment.optimizer-hint': Token.Comment.Special, + 'sql.escape': Token.Error, + 'sql.keyword': Token.Keyword, + 'sql.datatype': Token.Keyword.Type, + 'sql.literal': Token.Literal, + 'sql.literal.date': Token.Literal.Date, + 'sql.symbol': Token.Name, + 'sql.quoted-schema-object': Token.Name.Quoted, + 'sql.quoted-schema-object.escape': Token.Name.Quoted.Escape, + 'sql.constant': Token.Name.Constant, + 'sql.function': Token.Name.Function, + 'sql.variable': Token.Name.Variable, + 'sql.number': Token.Number, + 'sql.number.binary': Token.Number.Bin, + 'sql.number.float': Token.Number.Float, + 'sql.number.hex': Token.Number.Hex, + 'sql.number.integer': Token.Number.Integer, + 'sql.operator': Token.Operator, + 'sql.punctuation': Token.Punctuation, + 'sql.string': Token.String, + 'sql.string.double-quouted': Token.String.Double, + 'sql.string.escape': Token.String.Escape, + 'sql.string.single-quoted': Token.String.Single, + 'sql.whitespace': Token.Text, +} def parse_pygments_style(token_name, style_object, style_dict): """Parse token type and style string. @@ -108,6 +138,9 @@ def style_factory_output(name, cli_style): elif token in PROMPT_STYLE_TO_TOKEN: token_type = PROMPT_STYLE_TO_TOKEN[token] style.update({token_type: cli_style[token]}) + elif token in OVERRIDE_STYLE_TO_TOKEN: + token_type = OVERRIDE_STYLE_TO_TOKEN[token] + style.update({token_type: cli_style[token]}) else: # TODO: cli helpers will have to switch to ptk.Style logger.error('Unhandled style / class name: %s', token) diff --git a/mycli/myclirc b/mycli/myclirc index ba3ea1eb..8e861ce4 100644 --- a/mycli/myclirc +++ b/mycli/myclirc @@ -41,6 +41,7 @@ table_format = ascii # friendly, monokai, paraiso, colorful, murphy, bw, pastie, paraiso, trac, default, # fruity. # Screenshots at http://mycli.net/syntax +# Can be further modified in [colors] syntax_style = default # Keybindings: Possible values: emacs, vi. @@ -113,6 +114,35 @@ output.odd-row = "" output.even-row = "" output.null = "#808080" +# SQL syntax highlighting overrides +# sql.comment = 'italic #408080' +# sql.comment.multi-line = '' +# sql.comment.single-line = '' +# sql.comment.optimizer-hint = '' +# sql.escape = 'border:#FF0000' +# sql.keyword = 'bold #008000' +# sql.datatype = 'nobold #B00040' +# sql.literal = '' +# sql.literal.date = '' +# sql.symbol = '' +# sql.quoted-schema-object = '' +# sql.quoted-schema-object.escape = '' +# sql.constant = '#880000' +# sql.function = '#0000FF' +# sql.variable = '#19177C' +# sql.number = '#666666' +# sql.number.binary = '' +# sql.number.float = '' +# sql.number.hex = '' +# sql.number.integer = '' +# sql.operator = '#666666' +# sql.punctuation = '' +# sql.string = '#BA2121' +# sql.string.double-quouted = '' +# sql.string.escape = 'bold #BB6622' +# sql.string.single-quoted = '' +# sql.whitespace = '' + # Favorite queries. [favorite_queries] From f485dc39f2678f7766adc075f49b96f1185c6a7c Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Sat, 2 Jan 2021 13:41:08 -0500 Subject: [PATCH 128/202] support ANSI escape sequences in the prompt --- changelog.md | 1 + mycli/AUTHORS | 1 + mycli/main.py | 4 +++- mycli/myclirc | 1 + 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index b296c87b..1a6007ec 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ Features: * Add an option `--init-command` to execute SQL after connecting (Thanks: [KITAGAWA Yasutaka]). * Use InputMode.REPLACE_SINGLE +* Add support for ANSI escape sequences for coloring the prompt. Bug Fixes: ---------- diff --git a/mycli/AUTHORS b/mycli/AUTHORS index 58e1b82e..a5e8d689 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -78,6 +78,7 @@ Contributors: * bitkeen * Morgan Mitchell * Massimiliano Torromeo + * Roland Walker Creator: -------- diff --git a/mycli/main.py b/mycli/main.py index dffd7246..c465c472 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -28,6 +28,7 @@ from prompt_toolkit.shortcuts import PromptSession, CompleteStyle from prompt_toolkit.document import Document from prompt_toolkit.filters import HasFocus, IsDone +from prompt_toolkit.formatted_text import ANSI from prompt_toolkit.layout.processors import (HighlightMatchingBracketProcessor, ConditionalProcessor) from prompt_toolkit.lexers import PygmentsLexer @@ -548,7 +549,8 @@ def get_message(): prompt = self.get_prompt(self.prompt_format) if self.prompt_format == self.default_prompt and len(prompt) > self.max_len_prompt: prompt = self.get_prompt('\\d> ') - return [('class:prompt', prompt)] + prompt = prompt.replace("\\x1b", "\x1b") + return ANSI(prompt) def get_continuation(width, *_): if self.multiline_continuation_char: diff --git a/mycli/myclirc b/mycli/myclirc index ba3ea1eb..80155804 100644 --- a/mycli/myclirc +++ b/mycli/myclirc @@ -65,6 +65,7 @@ wider_completion_menu = False # \t - Product type (Percona, MySQL, MariaDB) # \A - DSN alias name (from the [alias_dsn] section) # \u - Username +# \x1b[...m - insert ANSI escape sequence prompt = '\t \u@\h:\d> ' prompt_continuation = '->' From 709fa1d5e52295aa9203fc6aa4cd03487abb0ae6 Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Sat, 19 Dec 2020 11:31:01 -0500 Subject: [PATCH 129/202] add pipe_once special command --- changelog.md | 1 + mycli/AUTHORS | 1 + mycli/main.py | 2 + mycli/packages/special/iocommands.py | 49 ++++++++++++++++++++ test/features/fixture_data/help_commands.txt | 1 + test/test_special_iocommands.py | 14 ++++++ 6 files changed, 68 insertions(+) diff --git a/changelog.md b/changelog.md index b296c87b..fadd4352 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ Features: * Add an option `--init-command` to execute SQL after connecting (Thanks: [KITAGAWA Yasutaka]). * Use InputMode.REPLACE_SINGLE +* Add a special command `\pipe_once` to pipe output to a subprocess. Bug Fixes: ---------- diff --git a/mycli/AUTHORS b/mycli/AUTHORS index 58e1b82e..a5e8d689 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -78,6 +78,7 @@ Contributors: * bitkeen * Morgan Mitchell * Massimiliano Torromeo + * Roland Walker Creator: -------- diff --git a/mycli/main.py b/mycli/main.py index dffd7246..a98cd8fb 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -654,6 +654,7 @@ def one_iteration(text=None): result_count += 1 mutating = mutating or destroy or is_mutating(status) special.unset_once_if_written() + special.unset_pipe_once_if_written() except EOFError as e: raise e except KeyboardInterrupt: @@ -814,6 +815,7 @@ def output(self, output, status=None): self.log_output(line) special.write_tee(line) special.write_once(line) + special.write_pipe_once(line) if fits or output_via_pager: # buffering diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index 11dca8d0..202e6e46 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -23,6 +23,8 @@ tee_file = None once_file = None written_to_once_file = False +pipe_once_process = None +written_to_pipe_once_process = False delimiter_command = DelimiterCommand() @@ -367,6 +369,53 @@ def unset_once_if_written(): once_file = None +@special_command('\\pipe_once', '\\| command', + 'Send next result to a subprocess.', + aliases=('\\|', )) +def set_pipe_once(arg, **_): + global pipe_once_process, written_to_pipe_once_process + pipe_once_cmd = shlex.split(arg) + if len(pipe_once_cmd) == 0: + raise OSError("pipe_once requires a command") + written_to_pipe_once_process = False + pipe_once_process = subprocess.Popen(pipe_once_cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + bufsize=1, + encoding='UTF-8', + universal_newlines=True) + return [(None, None, None, "")] + + +@export +def write_pipe_once(output): + global pipe_once_process, written_to_pipe_once_process + if output and pipe_once_process: + try: + click.echo(output, file=pipe_once_process.stdin, nl=False) + click.echo(u"\n", file=pipe_once_process.stdin, nl=False) + except (IOError, OSError) as e: + pipe_once_process.terminate() + raise OSError( + "Failed writing to pipe_once subprocess: {}".format(e.strerror)) + written_to_pipe_once_process = True + + +@export +def unset_pipe_once_if_written(): + """Unset the pipe_once cmd, if it has been written to.""" + global pipe_once_process, written_to_pipe_once_process + if written_to_pipe_once_process: + (stdout_data, stderr_data) = pipe_once_process.communicate() + if len(stdout_data) > 0: + print(stdout_data.rstrip(u"\n")) + if len(stderr_data) > 0: + print(stderr_data.rstrip(u"\n")) + pipe_once_process = None + written_to_pipe_once_process = False + + @special_command( 'watch', 'watch [seconds] [-c] query', diff --git a/test/features/fixture_data/help_commands.txt b/test/features/fixture_data/help_commands.txt index 657db7da..98ade846 100644 --- a/test/features/fixture_data/help_commands.txt +++ b/test/features/fixture_data/help_commands.txt @@ -9,6 +9,7 @@ | \fs | \fs name query | Save a favorite query. | | \l | \l | List databases. | | \once | \o [-o] filename | Append next result to an output file (overwrite using -o). | +| \pipe_once | \| command | Send next result to a subprocess. | | \timing | \t | Toggle timing of commands. | | connect | \r | Reconnect to the database. Optional database argument. | | exit | \q | Exit. | diff --git a/test/test_special_iocommands.py b/test/test_special_iocommands.py index b8b3acb3..e45f2400 100644 --- a/test/test_special_iocommands.py +++ b/test/test_special_iocommands.py @@ -109,6 +109,20 @@ def test_once_command(): assert f.read() == b"hello world\n" +def test_pipe_once_command(): + with pytest.raises(IOError): + mycli.packages.special.execute(None, u"\\pipe_once") + + with pytest.raises(OSError): + mycli.packages.special.execute( + None, u"\\pipe_once /proc/access-denied") + + mycli.packages.special.execute(None, u"\\pipe_once wc") + mycli.packages.special.write_once(u"hello world") + mycli.packages.special.unset_pipe_once_if_written() + # how to assert on wc output? + + def test_parseargfile(): """Test that parseargfile expands the user directory.""" expected = {'file': os.path.join(os.path.expanduser('~'), 'filename'), From 6e3d005cbbe7c15abb8a492ab3f093e2840aec57 Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Mon, 4 Jan 2021 17:22:02 -0500 Subject: [PATCH 130/202] quieten test suite warnings * more careful backslash escapes/raw strings * use @pytest.fixture() instead of @pytest.yield_fixture() --- mycli/main.py | 4 ++-- mycli/packages/parseutils.py | 2 +- mycli/packages/special/iocommands.py | 4 ++-- mycli/sqlcompleter.py | 2 +- test/conftest.py | 2 +- test/test_sqlexecute.py | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index 2c54db82..ade2ca62 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -482,7 +482,7 @@ def _connect(): exit(1) def handle_editor_command(self, text): - """Editor command is any query that is prefixed or suffixed by a '\e'. + r"""Editor command is any query that is prefixed or suffixed by a '\e'. The reason for a while loop is because a user might edit a query multiple times. For eg: @@ -513,7 +513,7 @@ def handle_editor_command(self, text): return text def handle_clip_command(self, text): - """A clip command is any query that is prefixed or suffixed by a + r"""A clip command is any query that is prefixed or suffixed by a '\clip'. :param text: Document diff --git a/mycli/packages/parseutils.py b/mycli/packages/parseutils.py index 6150a021..268e04e4 100644 --- a/mycli/packages/parseutils.py +++ b/mycli/packages/parseutils.py @@ -15,7 +15,7 @@ } def last_word(text, include='alphanum_underscore'): - """ + r""" Find the last word in a sentence. >>> last_word('abc') diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index e58a875f..58066b82 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -181,7 +181,7 @@ def get_clip_query(sql): # The reason we can't simply do .strip('\clip') is that it strips characters, # not a substring. So it'll strip "c" in the end of the sql also! - pattern = re.compile('(^\\\clip|\\\clip$)') + pattern = re.compile(r'(^\\clip|\\clip$)') while pattern.search(sql): sql = pattern.sub('', sql) @@ -257,7 +257,7 @@ def subst_favorite_query_args(query, args): query = query.replace(subst_var, val) - match = re.search('\\$\d+', query) + match = re.search(r'\$\d+', query) if match: return[None, 'missing substitution for ' + match.group(0) + ' in query:\n ' + query] diff --git a/mycli/sqlcompleter.py b/mycli/sqlcompleter.py index 20611be6..73b9b449 100644 --- a/mycli/sqlcompleter.py +++ b/mycli/sqlcompleter.py @@ -59,7 +59,7 @@ def __init__(self, smart_completion=True, supported_formats=(), keyword_casing=' self.reserved_words = set() for x in self.keywords: self.reserved_words.update(x.split()) - self.name_pattern = compile("^[_a-z][_a-z0-9\$]*$") + self.name_pattern = compile(r"^[_a-z][_a-z0-9\$]*$") self.special_commands = [] self.table_formats = supported_formats diff --git a/test/conftest.py b/test/conftest.py index cf6d721b..d7d10ce3 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -4,7 +4,7 @@ import mycli.sqlexecute -@pytest.yield_fixture(scope="function") +@pytest.fixture(scope="function") def connection(): create_db('_test_db') connection = db_connection('_test_db') diff --git a/test/test_sqlexecute.py b/test/test_sqlexecute.py index c2d38be7..5168bf6f 100644 --- a/test/test_sqlexecute.py +++ b/test/test_sqlexecute.py @@ -166,7 +166,7 @@ def test_favorite_query_expanded_output(executor): results = run(executor, "\\fs test-ae select * from test") assert_result_equal(results, status='Saved.') - results = run(executor, "\\f test-ae \G") + results = run(executor, "\\f test-ae \\G") assert is_expanded_output() is True assert_result_equal(results, title='> select * from test', headers=['a'], rows=[('abc',)], auto_status=False) From 08d1533935a2658ef2f67148995d4135326f619c Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Mon, 4 Jan 2021 18:38:59 -0500 Subject: [PATCH 131/202] switch to GitHub Actions for CI --- .github/workflows/ci.yml | 60 ++++++++++++++++++++++++++++++++++++++++ .travis.yml | 33 ---------------------- 2 files changed, 60 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..aad35d98 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,60 @@ +name: mycli + +on: + push: + branches-ignore: + - 'master' + paths-ignore: + - '**.md' + +jobs: + linux: + + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: [3.6, 3.7, 3.8] + + steps: + + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Start MySQL + run: | + sudo /etc/init.d/mysql start + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + pip install --no-cache-dir -e . + + - name: Wait for MySQL connection + run: | + while ! mysqladmin ping --host=localhost --port=3306 --user=root --password=root --silent; do + sleep 5 + done + + - name: Pytest / behave + env: + PYTEST_PASSWORD: root + run: | + ./setup.py test --pytest-args="--cov-report= --cov=mycli" + + - name: Lint + env: + GIT_BRANCH: ${{ github.ref }} + run: | + ./setup.py lint --branch="$GIT_BRANCH" + + - name: Coverage + run: | + coverage combine + coverage report + codecov diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 182dea73..00000000 --- a/.travis.yml +++ /dev/null @@ -1,33 +0,0 @@ -language: python -python: - - "3.6" - - "3.7" - - "3.8" - -matrix: - include: - - python: 3.7 - dist: xenial - sudo: true - -install: - - pip install -r requirements-dev.txt - - pip install --no-cache-dir -e . - - sudo rm -f /etc/mysql/conf.d/performance-schema.cnf - - sudo service mysql restart - -script: - - ./setup.py test --pytest-args="--cov-report= --cov=mycli" - - coverage combine - - coverage report - - ./setup.py lint --branch=$TRAVIS_BRANCH - -after_success: - - codecov - -notifications: - webhooks: - urls: - - YOUR_WEBHOOK_URL - on_success: change # options: [always|never|change] default: always - on_failure: always # options: [always|never|change] default: always From 048994ae8be73293a34a54a27ce46d82d63bed4a Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Tue, 5 Jan 2021 05:30:31 -0500 Subject: [PATCH 132/202] make the test suite respect $PYTEST_PORT env var as documented --- test/features/db_utils.py | 18 ++++++++++++------ test/features/environment.py | 12 +++++++++--- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/test/features/db_utils.py b/test/features/db_utils.py index c29dedb3..be550e9f 100644 --- a/test/features/db_utils.py +++ b/test/features/db_utils.py @@ -1,11 +1,12 @@ import pymysql -def create_db(hostname='localhost', username=None, password=None, - dbname=None): +def create_db(hostname='localhost', port=3306, username=None, + password=None, dbname=None): """Create test database. :param hostname: string + :param port: int :param username: string :param password: string :param dbname: string @@ -14,6 +15,7 @@ def create_db(hostname='localhost', username=None, password=None, """ cn = pymysql.connect( host=hostname, + port=port, user=username, password=password, charset='utf8mb4', @@ -26,14 +28,15 @@ def create_db(hostname='localhost', username=None, password=None, cn.close() - cn = create_cn(hostname, password, username, dbname) + cn = create_cn(hostname, port, password, username, dbname) return cn -def create_cn(hostname, password, username, dbname): +def create_cn(hostname, port, password, username, dbname): """Open connection to database. :param hostname: + :param port: :param password: :param username: :param dbname: string @@ -42,6 +45,7 @@ def create_cn(hostname, password, username, dbname): """ cn = pymysql.connect( host=hostname, + port=port, user=username, password=password, db=dbname, @@ -52,11 +56,12 @@ def create_cn(hostname, password, username, dbname): return cn -def drop_db(hostname='localhost', username=None, password=None, - dbname=None): +def drop_db(hostname='localhost', port=3306, username=None, + password=None, dbname=None): """Drop database. :param hostname: string + :param port: int :param username: string :param password: string :param dbname: string @@ -64,6 +69,7 @@ def drop_db(hostname='localhost', username=None, password=None, """ cn = pymysql.connect( host=hostname, + port=port, user=username, password=password, db=dbname, diff --git a/test/features/environment.py b/test/features/environment.py index cb351403..2ecf3074 100644 --- a/test/features/environment.py +++ b/test/features/environment.py @@ -42,6 +42,10 @@ def before_all(context): 'my_test_host', os.getenv('PYTEST_HOST', 'localhost') ), + 'port': context.config.userdata.get( + 'my_test_port', + int(os.getenv('PYTEST_PORT', '3306')) + ), 'user': context.config.userdata.get( 'my_test_user', os.getenv('PYTEST_USER', 'root') @@ -72,7 +76,8 @@ def before_all(context): context.conf['myclirc'] = os.path.join(context.package_root, 'test', 'myclirc') - context.cn = dbutils.create_db(context.conf['host'], context.conf['user'], + context.cn = dbutils.create_db(context.conf['host'], context.conf['port'], + context.conf['user'], context.conf['pass'], context.conf['dbname']) @@ -82,8 +87,9 @@ def before_all(context): def after_all(context): """Unset env parameters.""" dbutils.close_cn(context.cn) - dbutils.drop_db(context.conf['host'], context.conf['user'], - context.conf['pass'], context.conf['dbname']) + dbutils.drop_db(context.conf['host'], context.conf['port'], + context.conf['user'], context.conf['pass'], + context.conf['dbname']) # Restore env vars. #for k, v in context.pgenv.items(): From 764f140799dabc44e3de9e98a5e07c6644df7d70 Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Tue, 5 Jan 2021 05:56:13 -0500 Subject: [PATCH 133/202] stop editor test from creating file "select" * open_external_editor() was ignoring $EDITOR env var when $VISUAL was set * the "select" in "select 1" was interpreted as a filename when the string was passed as a positional param --- test/test_special_iocommands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_special_iocommands.py b/test/test_special_iocommands.py index cdd55399..73bfbabe 100644 --- a/test/test_special_iocommands.py +++ b/test/test_special_iocommands.py @@ -49,7 +49,8 @@ def test_editor_command(): assert mycli.packages.special.get_filename(r'\e filename') == "filename" os.environ['EDITOR'] = 'true' - mycli.packages.special.open_external_editor(r'select 1') == "select 1" + os.environ['VISUAL'] = 'true' + mycli.packages.special.open_external_editor(sql=r'select 1') == "select 1" def test_tee_command(): From 370c9303323a37a50f4737c4e93f13bcc3a335b8 Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Tue, 5 Jan 2021 06:17:49 -0500 Subject: [PATCH 134/202] restore test/myclirc from repo after test writes --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 51322bd5..4aa7f91a 100755 --- a/setup.py +++ b/setup.py @@ -77,6 +77,7 @@ def run_tests(self): 'behave test/features ' + self.behave_args, shell=True ) + subprocess.run(['git', 'checkout', '--', 'test/myclirc'], check=False) sys.exit(unit_test_errno or cli_errno) From a2a9a8aeb6da733946b2f4eca4e6bddb84fa24f9 Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Tue, 5 Jan 2021 06:46:52 -0500 Subject: [PATCH 135/202] restore working local --socket= --- changelog.md | 2 ++ mycli/AUTHORS | 1 + mycli/main.py | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 6c54ef70..eedca52f 100644 --- a/changelog.md +++ b/changelog.md @@ -19,6 +19,7 @@ Bug Fixes: * Send "Connecting to socket" message to the standard error. * Respect empty string for prompt_continuation via `prompt_continuation = ''` in `.myclirc` * Fix \once -o to overwrite output whole, instead of line-by-line. +* Restore working local `--socket=` (Thanks: [xeron]). 1.22.2 ====== @@ -801,3 +802,4 @@ Bug Fixes: [laixintao]: https://github.com/laixintao [mtorromeo]: https://github.com/mtorromeo [mwcm]: https://github.com/mwcm +[xeron]: https://github.com/xeron diff --git a/mycli/AUTHORS b/mycli/AUTHORS index a5e8d689..c111e4e6 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -79,6 +79,7 @@ Contributors: * Morgan Mitchell * Massimiliano Torromeo * Roland Walker + * xeron Creator: -------- diff --git a/mycli/main.py b/mycli/main.py index 2c54db82..72c7fa77 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -388,7 +388,7 @@ def connect(self, database='', user='', passwd='', host='', port='', database = database or cnf['database'] # Socket interface not supported for SSH connections - if port or host or ssh_host or ssh_port: + if (port and host) or (ssh_host and ssh_port): socket = '' else: socket = socket or cnf['socket'] or guess_socket_location() From 21cda96909308652aca00c88f0ec4f9cb5d54bfc Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Tue, 5 Jan 2021 07:13:11 -0500 Subject: [PATCH 136/202] stop test suite commands leaking into user history --- test/features/environment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/features/environment.py b/test/features/environment.py index cb351403..f065c172 100644 --- a/test/features/environment.py +++ b/test/features/environment.py @@ -18,6 +18,7 @@ def before_all(context): os.environ['EDITOR'] = 'ex' os.environ['LC_ALL'] = 'en_US.UTF-8' os.environ['PROMPT_TOOLKIT_NO_CPR'] = '1' + os.environ['MYCLI_HISTFILE'] = os.devnull test_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) login_path_file = os.path.join(test_dir, 'mylogin.cnf') From abf8834aecffb73ab63b8dffd4d86c36c2c0e97b Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Tue, 5 Jan 2021 07:41:32 -0500 Subject: [PATCH 137/202] dispatch lines ending with \e or \clip, like \G --- changelog.md | 1 + mycli/clibuffer.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/changelog.md b/changelog.md index 6c54ef70..3a861f0b 100644 --- a/changelog.md +++ b/changelog.md @@ -19,6 +19,7 @@ Bug Fixes: * Send "Connecting to socket" message to the standard error. * Respect empty string for prompt_continuation via `prompt_continuation = ''` in `.myclirc` * Fix \once -o to overwrite output whole, instead of line-by-line. +* Dispatch lines ending with `\e` or `\clip` on return, even in multiline mode. 1.22.2 ====== diff --git a/mycli/clibuffer.py b/mycli/clibuffer.py index c9d29d1d..c0cb5c1b 100644 --- a/mycli/clibuffer.py +++ b/mycli/clibuffer.py @@ -39,6 +39,8 @@ def _multiline_exception(text): text.endswith('\\g') or text.endswith('\\G') or + text.endswith(r'\e') or + text.endswith(r'\clip') or # Exit doesn't need semi-column` (text == 'exit') or From 4d813919b4e98842a08010516663ccb8042fcd98 Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Tue, 5 Jan 2021 15:21:06 -0500 Subject: [PATCH 138/202] add Python 3.9 and "on pull_request" to CI --- .github/workflows/ci.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aad35d98..413b7495 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,7 @@ name: mycli on: - push: - branches-ignore: - - 'master' + pull_request: paths-ignore: - '**.md' @@ -14,7 +12,7 @@ jobs: strategy: matrix: - python-version: [3.6, 3.7, 3.8] + python-version: [3.6, 3.7, 3.8, 3.9] steps: @@ -48,10 +46,8 @@ jobs: ./setup.py test --pytest-args="--cov-report= --cov=mycli" - name: Lint - env: - GIT_BRANCH: ${{ github.ref }} run: | - ./setup.py lint --branch="$GIT_BRANCH" + ./setup.py lint --branch=HEAD - name: Coverage run: | From 7273ea54062ebb787fee76bce2df9bedcf3c2d75 Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Thu, 7 Jan 2021 09:51:06 -0500 Subject: [PATCH 139/202] allow backtick quoting around "use" argument --- changelog.md | 1 + mycli/main.py | 3 +++ test/features/crud_database.feature | 4 ++++ test/features/steps/crud_database.py | 8 ++++++++ 4 files changed, 16 insertions(+) diff --git a/changelog.md b/changelog.md index 02f53cc7..b0cee239 100644 --- a/changelog.md +++ b/changelog.md @@ -21,6 +21,7 @@ Bug Fixes: * Fix \once -o to overwrite output whole, instead of line-by-line. * Dispatch lines ending with `\e` or `\clip` on return, even in multiline mode. * Restore working local `--socket=` (Thanks: [xeron]). +* Allow backtick quoting around the database argument to the `use` command. 1.22.2 ====== diff --git a/mycli/main.py b/mycli/main.py index c66d048a..3e4917eb 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -239,6 +239,9 @@ def change_db(self, arg, **_): ) return + if arg.startswith('`') and arg.endswith('`'): + arg = re.sub(r'^`(.*)`$', r'\1', arg) + arg = re.sub(r'``', r'`', arg) self.sqlexecute.change_db(arg) yield (None, None, None, 'You are now connected to database "%s" as ' diff --git a/test/features/crud_database.feature b/test/features/crud_database.feature index 0c298b69..f4a7a7f1 100644 --- a/test/features/crud_database.feature +++ b/test/features/crud_database.feature @@ -16,6 +16,10 @@ Feature: manipulate databases: when we connect to dbserver then we see database connected + Scenario: connect and disconnect from quoted test database + When we connect to quoted test database + then we see database connected + Scenario: create and drop default database When we create database then we see database created diff --git a/test/features/steps/crud_database.py b/test/features/steps/crud_database.py index be6dec05..841f37d0 100644 --- a/test/features/steps/crud_database.py +++ b/test/features/steps/crud_database.py @@ -37,6 +37,14 @@ def step_db_connect_test(context): context.cli.sendline('use {0};'.format(db_name)) +@when('we connect to quoted test database') +def step_db_connect_quoted_tmp(context): + """Send connect to database.""" + db_name = context.conf['dbname'] + context.currentdb = db_name + context.cli.sendline('use `{0}`;'.format(db_name)) + + @when('we connect to tmp database') def step_db_connect_tmp(context): """Send connect to database.""" From 970331d5795b22d188dcfc3615c38e8f3c751f17 Mon Sep 17 00:00:00 2001 From: 0xflotus <0xflotus@gmail.com> Date: Thu, 7 Jan 2021 18:08:00 +0100 Subject: [PATCH 140/202] fix: small errors --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d5a06879..a2f08626 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,8 @@ $ sudo apt-get install mycli # Only on debian or ubuntu --ssh-password TEXT Password to connect to ssh server. --ssh-key-filename TEXT Private key filename (identify file) for the ssh connection. - --ssh-config-path TEXT Path to ssh configuation. - --ssh-config-host TEXT Host for ssh server in ssh configuations (requires paramiko). + --ssh-config-path TEXT Path to ssh configuration. + --ssh-config-host TEXT Host for ssh server in ssh configurations (requires paramiko). --ssl-ca PATH CA file in PEM format. --ssl-capath TEXT CA directory. --ssl-cert PATH X509 cert in PEM format. @@ -113,7 +113,7 @@ Features * Support for multiline queries. * Favorite queries with optional positional parameters. Save a query using `\fs alias query` and execute it with `\f alias` whenever you need. -* Timing of sql statments and table rendering. +* Timing of sql statements and table rendering. * Config file is automatically created at ``~/.myclirc`` at first launch. * Log every query and its results to a file (disabled by default). * Pretty prints tabular data (with colors!) From bbabf45725e8490599c04bd255732b3105416b35 Mon Sep 17 00:00:00 2001 From: 0xflotus <0xflotus@gmail.com> Date: Thu, 7 Jan 2021 18:13:03 +0100 Subject: [PATCH 141/202] add to authors (#1) * Update changelog.md * Update AUTHORS --- changelog.md | 1 + mycli/AUTHORS | 1 + 2 files changed, 2 insertions(+) diff --git a/changelog.md b/changelog.md index 02f53cc7..359f882a 100644 --- a/changelog.md +++ b/changelog.md @@ -21,6 +21,7 @@ Bug Fixes: * Fix \once -o to overwrite output whole, instead of line-by-line. * Dispatch lines ending with `\e` or `\clip` on return, even in multiline mode. * Restore working local `--socket=` (Thanks: [xeron]). +* Fixed some typo errors in `README.md`. 1.22.2 ====== diff --git a/mycli/AUTHORS b/mycli/AUTHORS index c111e4e6..c9f52fb8 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -80,6 +80,7 @@ Contributors: * Massimiliano Torromeo * Roland Walker * xeron + * 0xflotus Creator: -------- From 1af59cacafcad130bcc8d7d95062e5eaa113c37e Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Thu, 7 Jan 2021 16:44:37 -0500 Subject: [PATCH 142/202] avoid open('/dev/tty') until absolutely necessary --- changelog.md | 1 + mycli/main.py | 17 +++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/changelog.md b/changelog.md index 02f53cc7..3e2dfebd 100644 --- a/changelog.md +++ b/changelog.md @@ -21,6 +21,7 @@ Bug Fixes: * Fix \once -o to overwrite output whole, instead of line-by-line. * Dispatch lines ending with `\e` or `\clip` on return, even in multiline mode. * Restore working local `--socket=` (Thanks: [xeron]). +* Avoid opening `/dev/tty` when `--no-warn` is given. 1.22.2 ====== diff --git a/mycli/main.py b/mycli/main.py index c66d048a..627fcb9c 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -21,7 +21,7 @@ from cli_helpers.utils import strip_ansi import click import sqlparse -from mycli.packages.parseutils import is_dropping_database +from mycli.packages.parseutils import is_dropping_database, is_destructive from prompt_toolkit.completion import DynamicCompleter from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode from prompt_toolkit.key_binding.bindings.named_commands import register as prompt_register @@ -1254,14 +1254,15 @@ def cli(database, user, host, port, socket, password, dbname, click.secho('Sorry... :(', err=True, fg='red') exit(1) - try: - sys.stdin = open('/dev/tty') - except (IOError, OSError): - mycli.logger.warning('Unable to open TTY as stdin.') + if mycli.destructive_warning and is_destructive(stdin_text): + try: + sys.stdin = open('/dev/tty') + warn_confirmed = confirm_destructive_query(stdin_text) + except (IOError, OSError): + mycli.logger.warning('Unable to open TTY as stdin.') + if not warn_confirmed: + exit(0) - if (mycli.destructive_warning and - confirm_destructive_query(stdin_text) is False): - exit(0) try: new_line = True From a252ba02c57c75e6c5da8b4a3e11bf24833bc765 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Tue, 12 Jan 2021 18:14:43 -0800 Subject: [PATCH 143/202] Releasing version 1.23.0 --- mycli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/__init__.py b/mycli/__init__.py index 53bfe2e0..33353682 100644 --- a/mycli/__init__.py +++ b/mycli/__init__.py @@ -1 +1 @@ -__version__ = '1.22.2' +__version__ = '1.23.0' From b882ac3acf7ea04deded8857cf1e85ffe3735460 Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Fri, 15 Jan 2021 10:10:41 -0500 Subject: [PATCH 144/202] allow --host without --port to make TCP connection --- changelog.md | 9 ++++++++- mycli/main.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index acf434b3..eb37445f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,11 @@ -TBD +1.23.1 +=== + +Bug Fixes: +---------- +* Allow `--host` without `--port` to make a TCP connection. + +1.23.0 === Features: diff --git a/mycli/main.py b/mycli/main.py index cbc18582..7c3c3d6a 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -391,7 +391,7 @@ def connect(self, database='', user='', passwd='', host='', port='', database = database or cnf['database'] # Socket interface not supported for SSH connections - if (port and host) or (ssh_host and ssh_port): + if port or (host and host != 'localhost') or (ssh_host and ssh_port): socket = '' else: socket = socket or cnf['socket'] or guess_socket_location() From 609c3ec5c868a7273530d6d7a076273abe652ce8 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Fri, 15 Jan 2021 07:40:16 -0800 Subject: [PATCH 145/202] Releasing version 1.23.1 --- mycli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/__init__.py b/mycli/__init__.py index 33353682..a039e2fd 100644 --- a/mycli/__init__.py +++ b/mycli/__init__.py @@ -1 +1 @@ -__version__ = '1.23.0' +__version__ = '1.23.1' From 042ddade88ebc38a3ec529ca24335899b6a6809b Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sat, 16 Jan 2021 08:13:55 -0800 Subject: [PATCH 146/202] Coerce port to int. --- mycli/main.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mycli/main.py b/mycli/main.py index 7c3c3d6a..32718db6 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -397,7 +397,11 @@ def connect(self, database='', user='', passwd='', host='', port='', socket = socket or cnf['socket'] or guess_socket_location() user = user or cnf['user'] or os.getenv('USER') host = host or cnf['host'] - port = port or cnf['port'] + try: + port = port or int(cnf['port']) + except ValueError as e: + self.echo("Error: Invalid port number: '{0}'.".format(cnf['port']), + err=True, fg='red') ssl = ssl or {} passwd = passwd if isinstance(passwd, str) else cnf['password'] From 17906f9af635fa714e9b792c4a9eb2b3a40b89a0 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sat, 16 Jan 2021 08:15:33 -0800 Subject: [PATCH 147/202] Changelog. --- changelog.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/changelog.md b/changelog.md index eb37445f..fe6e2683 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,10 @@ +1.23.2 +=== + +Bug Fixes: +---------- +* Ensure `--port` is always an int. + 1.23.1 === From 55f0fac471ea2be76e00a35a1f385c0327710580 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sat, 16 Jan 2021 08:54:09 -0800 Subject: [PATCH 148/202] Make int parsing of port more robust. --- mycli/main.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index 32718db6..f2b2fd8e 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -397,11 +397,7 @@ def connect(self, database='', user='', passwd='', host='', port='', socket = socket or cnf['socket'] or guess_socket_location() user = user or cnf['user'] or os.getenv('USER') host = host or cnf['host'] - try: - port = port or int(cnf['port']) - except ValueError as e: - self.echo("Error: Invalid port number: '{0}'.".format(cnf['port']), - err=True, fg='red') + port = int(port or cnf['port'] or 3306) ssl = ssl or {} passwd = passwd if isinstance(passwd, str) else cnf['password'] From a1d6b855c6d12a3f3891cfe152968ba70af4e7cc Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Sat, 16 Jan 2021 12:11:04 -0500 Subject: [PATCH 149/202] make FileNotFound exception reachable --- changelog.md | 7 +++++++ mycli/main.py | 6 +++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index eb37445f..edf1cd4b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,10 @@ +TBD +=== + +Bug Fixes: +---------- +* Allow `FileNotFound` exception for SSH config files. + 1.23.1 === diff --git a/mycli/main.py b/mycli/main.py index 7c3c3d6a..f5debcd2 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1350,6 +1350,9 @@ def read_ssh_config(ssh_config_path): try: with open(ssh_config_path) as f: ssh_config.parse(f) + except FileNotFoundError as e: + click.secho(str(e), err=True, fg='red') + sys.exit(1) # Paramiko prior to version 2.7 raises Exception on parse errors. # In 2.7 it has become paramiko.ssh_exception.SSHException, # but let's catch everything for compatibility @@ -1359,9 +1362,6 @@ def read_ssh_config(ssh_config_path): err=True, fg='red' ) sys.exit(1) - except FileNotFoundError as e: - click.secho(str(e), err=True, fg='red') - sys.exit(1) else: return ssh_config From e4d0e5b244245faa6b75825d65c4c72aeeefe968 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sat, 16 Jan 2021 10:22:32 -0800 Subject: [PATCH 150/202] Releasing version 1.23.2 --- mycli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/__init__.py b/mycli/__init__.py index a039e2fd..375471f7 100644 --- a/mycli/__init__.py +++ b/mycli/__init__.py @@ -1 +1 @@ -__version__ = '1.23.1' +__version__ = '1.23.2' From 10e4d7d334aedc1fd3b404d8e175188e6344a9fc Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Sat, 16 Jan 2021 15:34:12 -0500 Subject: [PATCH 151/202] fix a couple of irregular indentations --- mycli/packages/special/iocommands.py | 2 +- mycli/sqlcompleter.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index 58066b82..01f3c7ba 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -302,7 +302,7 @@ def execute_system_command(arg, **_): usage = "Syntax: system [command].\n" if not arg: - return [(None, None, None, usage)] + return [(None, None, None, usage)] try: command = arg.strip() diff --git a/mycli/sqlcompleter.py b/mycli/sqlcompleter.py index 73b9b449..3656aa69 100644 --- a/mycli/sqlcompleter.py +++ b/mycli/sqlcompleter.py @@ -72,7 +72,7 @@ def escape_name(self, name): if name and ((not self.name_pattern.match(name)) or (name.upper() in self.reserved_words) or (name.upper() in self.functions)): - name = '`%s`' % name + name = '`%s`' % name return name From 685e5ba33b51c9fcdf42b2eeb875b9afac6b07ff Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Sat, 16 Jan 2021 15:39:28 -0500 Subject: [PATCH 152/202] fix printf-style format string --- mycli/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/config.py b/mycli/config.py index e0f2d1fc..9c592fb5 100644 --- a/mycli/config.py +++ b/mycli/config.py @@ -244,7 +244,7 @@ def str_to_bool(s): elif s.lower() in false_values: return False else: - raise ValueError('not a recognized boolean value: %s'.format(s)) + raise ValueError('not a recognized boolean value: {0}'.format(s)) def strip_matching_quotes(s): From 5a8b3d6ee95fa44930c040c0ce457a9ea7c2ad93 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sun, 17 Jan 2021 04:33:26 +0300 Subject: [PATCH 153/202] CI: test with different OS and MySQL versions --- .github/workflows/ci.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 413b7495..ccb15aa6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,13 +7,20 @@ on: jobs: linux: - - runs-on: ubuntu-latest - strategy: matrix: python-version: [3.6, 3.7, 3.8, 3.9] - + include: + - python-version: 3.6 + os: ubuntu-16.04 # MySQL 5.7.32 + - python-version: 3.7 + os: ubuntu-18.04 # MySQL 5.7.32 + - python-version: 3.8 + os: ubuntu-18.04 # MySQL 5.7.32 + - python-version: 3.9 + os: ubuntu-20.04 # MySQL 8.0.22 + + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 From 734599444c90c928df1516d4ffee1a2798c832a9 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sun, 17 Jan 2021 14:49:33 +0300 Subject: [PATCH 154/202] use importlib.resources instead of __file__ --- changelog.md | 7 +++++++ mycli/config.py | 16 ++++++++++++++-- mycli/main.py | 43 ++++++++++++++++++++++++++++--------------- setup.py | 3 +++ 4 files changed, 52 insertions(+), 17 deletions(-) diff --git a/changelog.md b/changelog.md index 7b958bad..bd3b89a5 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,10 @@ +TODO: +=== + +Internal +-------- +* Use importlib, instead of file links, to locate resources + 1.23.2 === diff --git a/mycli/config.py b/mycli/config.py index 9c592fb5..5e4208b8 100644 --- a/mycli/config.py +++ b/mycli/config.py @@ -1,7 +1,7 @@ import io import shutil from copy import copy -from io import BytesIO, TextIOWrapper +from io import BytesIO, TextIOWrapper, StringIO import logging import os from os.path import exists @@ -13,6 +13,12 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.backends import default_backend +try: + import importlib.resources as resources +except ImportError: + # Python < 3.7 + import importlib_resources as resources + try: basestring except NameError: @@ -95,7 +101,7 @@ def get_included_configs(config_file: Union[str, io.TextIOWrapper]) -> list: def read_config_files(files, list_values=True): """Read and merge a list of config files.""" - config = ConfigObj(list_values=list_values) + config = create_default_config(list_values=list_values) _files = copy(files) while _files: _file = _files.pop(0) @@ -112,6 +118,12 @@ def read_config_files(files, list_values=True): return config +def create_default_config(list_values=True): + import mycli + default_config_file = resources.open_text(mycli, 'myclirc') + return read_config_file(default_config_file, list_values=list_values) + + def write_default_config(source, destination, overwrite=False): destination = os.path.expanduser(destination) if not overwrite and exists(destination): diff --git a/mycli/main.py b/mycli/main.py index 4ed2364d..207bd7a1 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1,10 +1,10 @@ +from io import open, StringIO import os import sys import traceback import logging import threading import re -import fileinput from collections import namedtuple try: from pwd import getpwuid @@ -13,7 +13,6 @@ from time import time from datetime import datetime from random import choice -from io import open from pymysql import OperationalError from cli_helpers.tabular_output import TabularOutputFormatter @@ -51,7 +50,7 @@ strip_matching_quotes) from .key_bindings import mycli_bindings from .lexer import MyCliLexer -from .__init__ import __version__ +from . import __version__ from .compat import WIN from .packages.filepaths import dir_path_exists, guess_socket_location @@ -66,6 +65,11 @@ from urllib.parse import urlparse from urllib.parse import unquote +try: + import importlib.resources as resources +except ImportError: + # Python < 3.7 + import importlib_resources as resources try: import paramiko @@ -75,8 +79,6 @@ # Query tuples are used for maintaining history Query = namedtuple('Query', ['query', 'successful', 'mutating']) -PACKAGE_ROOT = os.path.abspath(os.path.dirname(__file__)) - class MyCli(object): @@ -95,14 +97,22 @@ class MyCli(object): # check XDG_CONFIG_HOME exists and not an empty string if os.environ.get("XDG_CONFIG_HOME"): xdg_config_home = os.environ.get("XDG_CONFIG_HOME") + elif WIN: + xdg_config_home = "~/AppData/Local" else: xdg_config_home = "~/.config" + + default_user_config_file = os.path.join( + os.path.expanduser(xdg_config_home), + "mycli", + "myclirc" + ) + system_config_files = [ '/etc/myclirc', - os.path.join(os.path.expanduser(xdg_config_home), "mycli", "myclirc") + default_user_config_file ] - default_config_file = os.path.join(PACKAGE_ROOT, 'myclirc') pwd_config_file = os.path.join(os.getcwd(), ".myclirc") def __init__(self, sqlexecute=None, prompt=None, @@ -122,7 +132,7 @@ def __init__(self, sqlexecute=None, prompt=None, self.cnf_files = [defaults_file] # Load config. - config_files = ([self.default_config_file] + self.system_config_files + + config_files = (self.system_config_files + [myclirc] + [self.pwd_config_file]) c = self.config = read_config_files(config_files) self.multi_line = c['main'].as_bool('multi_line') @@ -154,7 +164,7 @@ def __init__(self, sqlexecute=None, prompt=None, # Write user config if system config wasn't the last config loaded. if c.filename not in self.system_config_files and not os.path.exists(myclirc): - write_default_config(self.default_config_file, myclirc) + write_default_config(self.default_user_config_file, myclirc) # audit log if self.logfile is None and 'audit_log' in c['main']: @@ -542,9 +552,6 @@ def run_cli(self): if self.smart_completion: self.refresh_completions() - author_file = os.path.join(PACKAGE_ROOT, 'AUTHORS') - sponsor_file = os.path.join(PACKAGE_ROOT, 'SPONSORS') - history_file = os.path.expanduser( os.environ.get('MYCLI_HISTFILE', '~/.mycli-history')) if dir_path_exists(history_file): @@ -564,7 +571,7 @@ def run_cli(self): print('Chat: https://gitter.im/dbcli/mycli') print('Mail: https://groups.google.com/forum/#!forum/mycli-users') print('Home: http://mycli.net') - print('Thanks to the contributor -', thanks_picker([author_file, sponsor_file])) + print('Thanks to the contributor -', thanks_picker()) def get_message(): prompt = self.get_prompt(self.prompt_format) @@ -1328,9 +1335,15 @@ def is_select(status): return status.split(None, 1)[0].lower() == 'select' -def thanks_picker(files=()): +def thanks_picker(): + import mycli + lines = ( + resources.read_text(mycli, 'AUTHORS') + + resources.read_text(mycli, 'SPONSORS') + ).split('\n') + contents = [] - for line in fileinput.input(files=files): + for line in lines: m = re.match(r'^ *\* (.*)', line) if m: contents.append(m.group(1)) diff --git a/setup.py b/setup.py index 4aa7f91a..ce32977f 100755 --- a/setup.py +++ b/setup.py @@ -28,6 +28,9 @@ 'pyperclip >= 1.8.1' ] +if sys.version_info.minor < 9: + install_requirements.append('importlib_resources >= 5.0.0') + class lint(Command): description = 'check code against PEP 8 (and fix violations)' From 01caf67ec32498ceb0e29cb372119d9609d40e61 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sun, 17 Jan 2021 15:09:47 +0300 Subject: [PATCH 155/202] fixed tests --- test/test_main.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/test_main.py b/test/test_main.py index 707c359b..91a366ba 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -3,7 +3,7 @@ import click from click.testing import CliRunner -from mycli.main import MyCli, cli, thanks_picker, PACKAGE_ROOT +from mycli.main import MyCli, cli, thanks_picker from mycli.packages.special.main import COMMANDS as SPECIAL_COMMANDS from .utils import USER, HOST, PORT, PASSWORD, dbtest, run @@ -140,10 +140,7 @@ def test_batch_mode_csv(executor): def test_thanks_picker_utf8(): - author_file = os.path.join(PACKAGE_ROOT, 'AUTHORS') - sponsor_file = os.path.join(PACKAGE_ROOT, 'SPONSORS') - - name = thanks_picker((author_file, sponsor_file)) + name = thanks_picker() assert name and isinstance(name, str) From f4d87ded35513e5651b0e497b9d8d0c1327ffea1 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sun, 17 Jan 2021 15:22:21 +0300 Subject: [PATCH 156/202] create xdf_config_home if it doesn't --- mycli/main.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index 207bd7a1..6a04b922 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -96,17 +96,16 @@ class MyCli(object): # check XDG_CONFIG_HOME exists and not an empty string if os.environ.get("XDG_CONFIG_HOME"): - xdg_config_home = os.environ.get("XDG_CONFIG_HOME") + xdg_config_home = os.path.expanduser(os.environ.get("XDG_CONFIG_HOME")) elif WIN: - xdg_config_home = "~/AppData/Local" + xdg_config_home = os.path.expanduser("~/AppData/Local") else: - xdg_config_home = "~/.config" + xdg_config_home = os.path.expanduser("~/.config") - default_user_config_file = os.path.join( - os.path.expanduser(xdg_config_home), - "mycli", - "myclirc" - ) + if not os.path.exists(xdg_config_home): + os.mkdir(xdg_config_home) + + default_user_config_file = os.path.join(xdg_config_home, "mycli", "myclirc") system_config_files = [ '/etc/myclirc', From 16630be5ab83a8a72176a4f4d6a85860d30a8238 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sun, 17 Jan 2021 15:38:32 +0300 Subject: [PATCH 157/202] moved default config to packages --- mycli/main.py | 18 +++--------------- mycli/packages/filepaths.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index 6a04b922..2dd261e3 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1,4 +1,4 @@ -from io import open, StringIO +from io import open import os import sys import traceback @@ -52,7 +52,7 @@ from .lexer import MyCliLexer from . import __version__ from .compat import WIN -from .packages.filepaths import dir_path_exists, guess_socket_location +from .packages.filepaths import dir_path_exists, guess_socket_location, get_default_config_path import itertools @@ -94,18 +94,7 @@ class MyCli(object): os.path.expanduser('~/.my.cnf'), ] - # check XDG_CONFIG_HOME exists and not an empty string - if os.environ.get("XDG_CONFIG_HOME"): - xdg_config_home = os.path.expanduser(os.environ.get("XDG_CONFIG_HOME")) - elif WIN: - xdg_config_home = os.path.expanduser("~/AppData/Local") - else: - xdg_config_home = os.path.expanduser("~/.config") - - if not os.path.exists(xdg_config_home): - os.mkdir(xdg_config_home) - - default_user_config_file = os.path.join(xdg_config_home, "mycli", "myclirc") + default_user_config_file = get_default_config_path() system_config_files = [ '/etc/myclirc', @@ -324,7 +313,6 @@ def initialize_logging(self): root_logger.debug('Initializing mycli logging.') root_logger.debug('Log file %r.', log_file) - def read_my_cnf_files(self, files, keys): """ Reads a list of config files and merges them. The last one will win. diff --git a/mycli/packages/filepaths.py b/mycli/packages/filepaths.py index 79fe26dc..1a0b6b03 100644 --- a/mycli/packages/filepaths.py +++ b/mycli/packages/filepaths.py @@ -1,6 +1,7 @@ import os import platform +from mycli.compat import WIN if os.name == "posix": if platform.system() == "Darwin": @@ -104,3 +105,19 @@ def guess_socket_location(): return os.path.join(r, filename) dirs[:] = [d for d in dirs if d.startswith("mysql")] return None + + +def get_default_config_path(): + # check XDG_CONFIG_HOME exists and not an empty string + if os.environ.get("XDG_CONFIG_HOME"): + xdg_config_home = os.path.expanduser(os.environ.get("XDG_CONFIG_HOME")) + elif WIN: + xdg_config_home = os.path.expanduser("~/AppData/Local") + else: + xdg_config_home = os.path.expanduser("~/.config/") + + config_root = os.path.join(xdg_config_home, 'mycli') + if not os.path.exists(config_root): + os.makedirs(config_root) + + return os.path.join(config_root, 'myclirc') From b6b4f9ffcf96bd7a2abc51678562197f59317b4e Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sun, 17 Jan 2021 15:53:18 +0300 Subject: [PATCH 158/202] fixed writing default config --- mycli/config.py | 7 +++++-- mycli/main.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/mycli/config.py b/mycli/config.py index 5e4208b8..2cc43dc4 100644 --- a/mycli/config.py +++ b/mycli/config.py @@ -124,12 +124,15 @@ def create_default_config(list_values=True): return read_config_file(default_config_file, list_values=list_values) -def write_default_config(source, destination, overwrite=False): +def write_default_config(destination, overwrite=False): + import mycli + default_config = resources.read_text(mycli, 'myclirc') destination = os.path.expanduser(destination) if not overwrite and exists(destination): return - shutil.copyfile(source, destination) + with open(destination, 'w') as f: + f.write(default_config) def get_mylogin_cnf_path(): diff --git a/mycli/main.py b/mycli/main.py index 2dd261e3..71258288 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -152,7 +152,7 @@ def __init__(self, sqlexecute=None, prompt=None, # Write user config if system config wasn't the last config loaded. if c.filename not in self.system_config_files and not os.path.exists(myclirc): - write_default_config(self.default_user_config_file, myclirc) + write_default_config(myclirc) # audit log if self.logfile is None and 'audit_log' in c['main']: From 8ee6848271667287787e608815e2596d02b10700 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sun, 17 Jan 2021 15:57:51 +0300 Subject: [PATCH 159/202] undo unrelated changes --- mycli/main.py | 11 +++++++---- mycli/packages/filepaths.py | 17 ----------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index 71258288..6382d4c5 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -52,7 +52,7 @@ from .lexer import MyCliLexer from . import __version__ from .compat import WIN -from .packages.filepaths import dir_path_exists, guess_socket_location, get_default_config_path +from .packages.filepaths import dir_path_exists, guess_socket_location import itertools @@ -94,11 +94,14 @@ class MyCli(object): os.path.expanduser('~/.my.cnf'), ] - default_user_config_file = get_default_config_path() - + # check XDG_CONFIG_HOME exists and not an empty string + if os.environ.get("XDG_CONFIG_HOME"): + xdg_config_home = os.environ.get("XDG_CONFIG_HOME") + else: + xdg_config_home = "~/.config" system_config_files = [ '/etc/myclirc', - default_user_config_file + os.path.join(os.path.expanduser(xdg_config_home), "mycli", "myclirc") ] pwd_config_file = os.path.join(os.getcwd(), ".myclirc") diff --git a/mycli/packages/filepaths.py b/mycli/packages/filepaths.py index 1a0b6b03..79fe26dc 100644 --- a/mycli/packages/filepaths.py +++ b/mycli/packages/filepaths.py @@ -1,7 +1,6 @@ import os import platform -from mycli.compat import WIN if os.name == "posix": if platform.system() == "Darwin": @@ -105,19 +104,3 @@ def guess_socket_location(): return os.path.join(r, filename) dirs[:] = [d for d in dirs if d.startswith("mysql")] return None - - -def get_default_config_path(): - # check XDG_CONFIG_HOME exists and not an empty string - if os.environ.get("XDG_CONFIG_HOME"): - xdg_config_home = os.path.expanduser(os.environ.get("XDG_CONFIG_HOME")) - elif WIN: - xdg_config_home = os.path.expanduser("~/AppData/Local") - else: - xdg_config_home = os.path.expanduser("~/.config/") - - config_root = os.path.join(xdg_config_home, 'mycli') - if not os.path.exists(config_root): - os.makedirs(config_root) - - return os.path.join(config_root, 'myclirc') From 552900b0f5235b35199440aa35493b9f1f295726 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Wed, 17 Feb 2021 19:54:59 +0300 Subject: [PATCH 160/202] update contact info and README --- README.md | 21 ++++++++++++++++----- mycli/main.py | 9 ++++++--- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c709eb89..444afb3e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # mycli -[![Build Status](https://travis-ci.org/dbcli/mycli.svg?branch=master)](https://travis-ci.org/dbcli/mycli) +[![Build Status](https://github.com/dbcli/mycli/workflows/mycli/badge.svg)](https://github.com/dbcli/mycli/actions?query=workflow%3Amycli) [![PyPI](https://img.shields.io/pypi/v/mycli.svg?style=plastic)](https://pypi.python.org/pypi/mycli) -[![Join the chat at https://gitter.im/dbcli/mycli](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/dbcli/mycli?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) A command line client for MySQL that can do auto-completion and syntax highlighting. @@ -53,6 +52,7 @@ $ sudo apt-get install mycli # Only on debian or ubuntu -h, --host TEXT Host address of the database. -P, --port INTEGER Port number to use for connection. Honors $MYSQL_TCP_PORT. + -u, --user TEXT User name to connect to the database. -S, --socket TEXT The socket file to use for connection. -p, --password TEXT Password to connect to the database. @@ -63,8 +63,11 @@ $ sudo apt-get install mycli # Only on debian or ubuntu --ssh-password TEXT Password to connect to ssh server. --ssh-key-filename TEXT Private key filename (identify file) for the ssh connection. + --ssh-config-path TEXT Path to ssh configuration. - --ssh-config-host TEXT Host for ssh server in ssh configurations (requires paramiko). + --ssh-config-host TEXT Host to connect to ssh server reading from ssh + configuration. + --ssl-ca PATH CA file in PEM format. --ssl-capath TEXT CA directory. --ssl-cert PATH X509 cert in PEM format. @@ -73,33 +76,41 @@ $ sudo apt-get install mycli # Only on debian or ubuntu --ssl-verify-server-cert Verify server's "Common Name" in its cert against hostname used when connecting. This option is disabled by default. + -V, --version Output mycli's version. -v, --verbose Verbose output. -D, --database TEXT Database to use. -d, --dsn TEXT Use DSN configured into the [alias_dsn] section of myclirc file. + --list-dsn list of DSN configured into the [alias_dsn] section of myclirc file. - --list-ssh-config list ssh configurations in the ssh config (requires paramiko). + + --list-ssh-config list ssh configurations in the ssh config + (requires paramiko). + -R, --prompt TEXT Prompt format (Default: "\t \u@\h:\d> "). -l, --logfile FILENAME Log every query and its results to a file. --defaults-group-suffix TEXT Read MySQL config groups with the specified suffix. + --defaults-file PATH Only read MySQL options from the given file. --myclirc PATH Location of myclirc file. --auto-vertical-output Automatically switch to vertical output mode if the result is wider than the terminal width. + -t, --table Display batch output in table format. --csv Display batch output in CSV format. --warn / --no-warn Warn before running a destructive query. --local-infile BOOLEAN Enable/disable LOAD DATA LOCAL INFILE. - --login-path TEXT Read this path from the login file. + -g, --login-path TEXT Read this path from the login file. -e, --execute TEXT Execute command and quit. --init-command TEXT SQL statement to execute after connecting. --charset TEXT Character set for MySQL session. --help Show this message and exit. + Features -------- diff --git a/mycli/main.py b/mycli/main.py index 4069ebe3..07388512 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -77,6 +77,11 @@ PACKAGE_ROOT = os.path.abspath(os.path.dirname(__file__)) +SUPPORT_INFO = ( + 'Home: http://mycli.net\n' + 'Bug tracker: https://github.com/dbcli/mycli/issues' +) + class MyCli(object): @@ -561,9 +566,7 @@ def run_cli(self): if not self.less_chatty: print(' '.join(sqlexecute.server_type())) print('mycli', __version__) - print('Chat: https://gitter.im/dbcli/mycli') - print('Mail: https://groups.google.com/forum/#!forum/mycli-users') - print('Home: http://mycli.net') + print(SUPPORT_INFO) print('Thanks to the contributor -', thanks_picker([author_file, sponsor_file])) def get_message(): From 87be5c3b14472e109ad26afb6f93b0397908110e Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Fri, 19 Feb 2021 02:13:48 +0300 Subject: [PATCH 161/202] a little code cleanup based on lgtm.org suggestions --- mycli/clibuffer.py | 1 - mycli/config.py | 4 +--- mycli/main.py | 4 ++-- mycli/packages/completion_engine.py | 2 -- mycli/packages/parseutils.py | 3 +-- mycli/packages/tabular_output/sql_format.py | 1 - mycli/sqlexecute.py | 1 - release.py | 1 - 8 files changed, 4 insertions(+), 13 deletions(-) diff --git a/mycli/clibuffer.py b/mycli/clibuffer.py index c0cb5c1b..81353b63 100644 --- a/mycli/clibuffer.py +++ b/mycli/clibuffer.py @@ -1,7 +1,6 @@ from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import Condition from prompt_toolkit.application import get_app -from .packages.parseutils import is_open_quote from .packages import special diff --git a/mycli/config.py b/mycli/config.py index 9c592fb5..0ff1a6e1 100644 --- a/mycli/config.py +++ b/mycli/config.py @@ -1,4 +1,3 @@ -import io import shutil from copy import copy from io import BytesIO, TextIOWrapper @@ -61,7 +60,7 @@ def read_config_file(f, list_values=True): return config -def get_included_configs(config_file: Union[str, io.TextIOWrapper]) -> list: +def get_included_configs(config_file: Union[str, TextIOWrapper]) -> list: """Get a list of configuration files that are included into config_path with !includedir directive. @@ -268,7 +267,6 @@ def _get_decryptor(key): def _remove_pad(line): """Remove the pad from the *line*.""" - pad_length = ord(line[-1:]) try: # Determine pad length. pad_length = ord(line[-1:]) diff --git a/mycli/main.py b/mycli/main.py index 07388512..921c28a0 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -865,8 +865,8 @@ def output(self, output, status=None): if not output_via_pager: # doesn't fit, flush buffer - for line in buf: - click.secho(line) + for buf_line in buf: + click.secho(buf_line) buf = [] else: click.secho(line) diff --git a/mycli/packages/completion_engine.py b/mycli/packages/completion_engine.py index 3cff2ccc..c7db06cb 100644 --- a/mycli/packages/completion_engine.py +++ b/mycli/packages/completion_engine.py @@ -1,5 +1,3 @@ -import os -import sys import sqlparse from sqlparse.sql import Comparison, Identifier, Where from .parseutils import last_word, extract_tables, find_prev_keyword diff --git a/mycli/packages/parseutils.py b/mycli/packages/parseutils.py index 268e04e4..fd203e3f 100644 --- a/mycli/packages/parseutils.py +++ b/mycli/packages/parseutils.py @@ -263,5 +263,4 @@ def normalize_db_name(db): ) if database_token is not None and normalize_db_name(database_token.get_name()) == dbname: result = keywords[0].normalized == "DROP" - else: - return result + return result diff --git a/mycli/packages/tabular_output/sql_format.py b/mycli/packages/tabular_output/sql_format.py index 730e6332..e6587bd3 100644 --- a/mycli/packages/tabular_output/sql_format.py +++ b/mycli/packages/tabular_output/sql_format.py @@ -1,6 +1,5 @@ """Format adapter for sql.""" -from cli_helpers.utils import filter_dict_by_key from mycli.packages.parseutils import extract_tables supported_formats = ('sql-insert', 'sql-update', 'sql-update-1', diff --git a/mycli/sqlexecute.py b/mycli/sqlexecute.py index 7534982d..46cf07c2 100644 --- a/mycli/sqlexecute.py +++ b/mycli/sqlexecute.py @@ -1,6 +1,5 @@ import logging import pymysql -import sqlparse from .packages import special from pymysql.constants import FIELD_TYPE from pymysql.converters import (convert_datetime, diff --git a/release.py b/release.py index 30c41b3f..3f18f03f 100755 --- a/release.py +++ b/release.py @@ -1,6 +1,5 @@ """A script to publish a release of mycli to PyPI.""" -import io from optparse import OptionParser import re import subprocess From 7b5fdc206aa3b72b3c68c4f30b5d74e24c963dba Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Fri, 19 Feb 2021 02:42:50 +0300 Subject: [PATCH 162/202] add lgtm.com badge to readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 444afb3e..8da08a44 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build Status](https://github.com/dbcli/mycli/workflows/mycli/badge.svg)](https://github.com/dbcli/mycli/actions?query=workflow%3Amycli) [![PyPI](https://img.shields.io/pypi/v/mycli.svg?style=plastic)](https://pypi.python.org/pypi/mycli) +[![LGTM](https://img.shields.io/lgtm/grade/python/github/dbcli/mycli.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/dbcli/mycli/context:python) A command line client for MySQL that can do auto-completion and syntax highlighting. From 01d8d89928c6cfb627a70323420cd30caedfc180 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Fri, 19 Feb 2021 17:44:03 +0300 Subject: [PATCH 163/202] remove unused function is_open_quote() --- changelog.md | 4 ++++ mycli/clibuffer.py | 1 - mycli/packages/parseutils.py | 8 -------- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/changelog.md b/changelog.md index c80c8596..f9ada63b 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,10 @@ Features: --------- * Add `-g` shortcut to option `--login-path`. +Internal: +--------- +* Remove unused function is_open_quote() + 1.23.2 === diff --git a/mycli/clibuffer.py b/mycli/clibuffer.py index c0cb5c1b..81353b63 100644 --- a/mycli/clibuffer.py +++ b/mycli/clibuffer.py @@ -1,7 +1,6 @@ from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import Condition from prompt_toolkit.application import get_app -from .packages.parseutils import is_open_quote from .packages import special diff --git a/mycli/packages/parseutils.py b/mycli/packages/parseutils.py index 268e04e4..0a5cb601 100644 --- a/mycli/packages/parseutils.py +++ b/mycli/packages/parseutils.py @@ -226,14 +226,6 @@ def is_destructive(queries): return False -def is_open_quote(sql): - """Returns true if the query contains an unclosed quote.""" - - # parsed can contain one or more semi-colon separated commands - parsed = sqlparse.parse(sql) - return any(_parsed_is_open_quote(p) for p in parsed) - - if __name__ == '__main__': sql = 'select * from (select t. from tabl t' print (extract_tables(sql)) From 08cf9460f2c362ad29de02e072d7f619ee7213c6 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sat, 20 Feb 2021 20:08:02 -0800 Subject: [PATCH 164/202] Ignore the paramiko_stub file while collecting pytests. --- pytest.ini | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..5422131c --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --ignore=mycli/packages/paramiko_stub/__init__.py From 51df80d25d7e7bd098bcda198b00e7e6fed7f706 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Mon, 22 Feb 2021 21:35:54 +0300 Subject: [PATCH 165/202] Get server version from pymysql without additional queries (#940) * Get server version from pymysql without additional queries * fix prompt * fixed prompt: don't include server version * version parser changes and more test cases * set ServerSpecies.Unknown to MySQL * handle alpha-beta release labels in the server version parser --- changelog.md | 5 +++ mycli/main.py | 4 +- mycli/sqlexecute.py | 98 +++++++++++++++++++++++++---------------- test/test_main.py | 2 + test/test_sqlexecute.py | 22 +++++++++ 5 files changed, 92 insertions(+), 39 deletions(-) diff --git a/changelog.md b/changelog.md index f9ada63b..e942ecd2 100644 --- a/changelog.md +++ b/changelog.md @@ -59,6 +59,11 @@ Bug Fixes: * Avoid opening `/dev/tty` when `--no-warn` is given. * Fixed some typo errors in `README.md`. +Internal: +--------- + +* Use server info from pymysql connection object. + 1.22.2 ====== diff --git a/mycli/main.py b/mycli/main.py index 921c28a0..feaa78c2 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -564,7 +564,7 @@ def run_cli(self): key_bindings = mycli_bindings(self) if not self.less_chatty: - print(' '.join(sqlexecute.server_type())) + print(sqlexecute.server_info) print('mycli', __version__) print(SUPPORT_INFO) print('Thanks to the contributor -', thanks_picker([author_file, sponsor_file])) @@ -936,7 +936,7 @@ def get_prompt(self, string): string = string.replace('\\u', sqlexecute.user or '(none)') string = string.replace('\\h', host or '(none)') string = string.replace('\\d', sqlexecute.dbname or '(none)') - string = string.replace('\\t', sqlexecute.server_type()[0] or 'mycli') + string = string.replace('\\t', sqlexecute.server_info.species.name) string = string.replace('\\n', "\n") string = string.replace('\\D', now.strftime('%a %b %d %H:%M:%S %Y')) string = string.replace('\\m', now.strftime('%M')) diff --git a/mycli/sqlexecute.py b/mycli/sqlexecute.py index 46cf07c2..ed7d2f04 100644 --- a/mycli/sqlexecute.py +++ b/mycli/sqlexecute.py @@ -1,4 +1,7 @@ +import enum import logging +import re + import pymysql from .packages import special from pymysql.constants import FIELD_TYPE @@ -17,17 +20,68 @@ FIELD_TYPE.NULL: type(None) }) + +class ServerSpecies(enum.Enum): + MySQL = 'MySQL' + MariaDB = 'MariaDB' + Percona = 'Percona' + Unknown = 'MySQL' + + +class ServerInfo: + def __init__(self, species, version_str): + self.species = species + self.version_str = version_str + self.version = self.calc_mysql_version_value(version_str) + + @staticmethod + def calc_mysql_version_value(version_str) -> int: + if not version_str or not isinstance(version_str, str): + return 0 + try: + major, minor, patch = version_str.split('.') + except ValueError: + return 0 + else: + return int(major) * 10_000 + int(minor) * 100 + int(patch) + + @classmethod + def from_version_string(cls, version_string): + if not version_string: + return cls(ServerSpecies.Unknown, '') + + re_species = ( + (r'(?P[0-9\.]+)-MariaDB', ServerSpecies.MariaDB), + (r'(?P[0-9\.]+)[a-z0-9]*-(?P[0-9]+$)', + ServerSpecies.Percona), + (r'(?P[0-9\.]+)[a-z0-9]*-(?P[A-Za-z0-9_]+)', + ServerSpecies.MySQL), + ) + for regexp, species in re_species: + match = re.search(regexp, version_string) + if match is not None: + parsed_version = match.group('version') + detected_species = species + break + else: + detected_species = ServerSpecies.Unknown + parsed_version = '' + + return cls(detected_species, parsed_version) + + def __str__(self): + if self.species: + return f'{self.species.value} {self.version_str}' + else: + return self.version_str + + class SQLExecute(object): databases_query = '''SHOW DATABASES''' tables_query = '''SHOW TABLES''' - version_query = '''SELECT @@VERSION''' - - version_comment_query = '''SELECT @@VERSION_COMMENT''' - version_comment_query_mysql4 = '''SHOW VARIABLES LIKE "version_comment"''' - show_candidates_query = '''SELECT name from mysql.help_topic WHERE name like "SHOW %"''' users_query = '''SELECT CONCAT("'", user, "'@'",host,"'") FROM mysql.user''' @@ -51,7 +105,7 @@ def __init__(self, database, user, password, host, port, socket, charset, self.charset = charset self.local_infile = local_infile self.ssl = ssl - self._server_type = None + self.server_info = None self.connection_id = None self.ssh_user = ssh_user self.ssh_host = ssh_host @@ -156,6 +210,7 @@ def connect(self, database=None, user=None, password=None, host=None, self.init_command = init_command # retrieve connection id self.reset_connection_id() + self.server_info = ServerInfo.from_version_string(conn.server_version) def run(self, statement): """Execute the sql in the database and return the results. The results @@ -272,37 +327,6 @@ def users(self): for row in cur: yield row - def server_type(self): - if self._server_type: - return self._server_type - with self.conn.cursor() as cur: - _logger.debug('Version Query. sql: %r', self.version_query) - cur.execute(self.version_query) - version = cur.fetchone()[0] - if version[0] == '4': - _logger.debug('Version Comment. sql: %r', - self.version_comment_query_mysql4) - cur.execute(self.version_comment_query_mysql4) - version_comment = cur.fetchone()[1].lower() - if isinstance(version_comment, bytes): - # with python3 this query returns bytes - version_comment = version_comment.decode('utf-8') - else: - _logger.debug('Version Comment. sql: %r', - self.version_comment_query) - cur.execute(self.version_comment_query) - version_comment = cur.fetchone()[0].lower() - - if 'mariadb' in version_comment: - product_type = 'mariadb' - elif 'percona' in version_comment: - product_type = 'percona' - else: - product_type = 'mysql' - - self._server_type = (product_type, version) - return self._server_type - def get_connection_id(self): if not self.connection_id: self.reset_connection_id() diff --git a/test/test_main.py b/test/test_main.py index 707c359b..acc75ca9 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -5,6 +5,7 @@ from mycli.main import MyCli, cli, thanks_picker, PACKAGE_ROOT from mycli.packages.special.main import COMMANDS as SPECIAL_COMMANDS +from mycli.sqlexecute import ServerInfo from .utils import USER, HOST, PORT, PASSWORD, dbtest, run from textwrap import dedent @@ -177,6 +178,7 @@ class TestExecute(): host = 'test' user = 'test' dbname = 'test' + server_info = ServerInfo.from_version_string('unknown') port = 0 def server_type(self): diff --git a/test/test_sqlexecute.py b/test/test_sqlexecute.py index 5168bf6f..0f38a97e 100644 --- a/test/test_sqlexecute.py +++ b/test/test_sqlexecute.py @@ -3,6 +3,7 @@ import pytest import pymysql +from mycli.sqlexecute import ServerInfo, ServerSpecies from .utils import run, dbtest, set_expanded_output, is_expanded_output @@ -270,3 +271,24 @@ def test_multiple_results(executor): 'status': '1 row in set'} ] assert results == expected + + +@pytest.mark.parametrize( + 'version_string, species, parsed_version_string, version', + ( + ('5.7.32-35', 'Percona', '5.7.32', 50732), + ('5.7.32-0ubuntu0.18.04.1', 'MySQL', '5.7.32', 50732), + ('10.5.8-MariaDB-1:10.5.8+maria~focal', 'MariaDB', '10.5.8', 100508), + ('5.5.5-10.5.8-MariaDB-1:10.5.8+maria~focal', 'MariaDB', '10.5.8', 100508), + ('5.0.16-pro-nt-log', 'MySQL', '5.0.16', 50016), + ('5.1.5a-alpha', 'MySQL', '5.1.5', 50105), + ('unexpected version string', None, '', 0), + ('', None, '', 0), + (None, None, '', 0), + ) +) +def test_version_parsing(version_string, species, parsed_version_string, version): + server_info = ServerInfo.from_version_string(version_string) + assert (server_info.species and server_info.species.name) == species or ServerSpecies.Unknown + assert server_info.version_str == parsed_version_string + assert server_info.version == version From 2b31a3ca59b5084c0dc73483f50b1b4ebafa8a49 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Mon, 22 Feb 2021 23:12:24 +0300 Subject: [PATCH 166/202] fix changelog (#965) --- changelog.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/changelog.md b/changelog.md index e942ecd2..b6a9fd94 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,7 @@ TBD Bug Fixes: ---------- * Allow `FileNotFound` exception for SSH config files. +* Fix startup error on MySQL < 5.0.22 Features: --------- @@ -59,11 +60,6 @@ Bug Fixes: * Avoid opening `/dev/tty` when `--no-warn` is given. * Fixed some typo errors in `README.md`. -Internal: ---------- - -* Use server info from pymysql connection object. - 1.22.2 ====== From bbbbb0d2128e76cad156a13c9631c7a8fab93868 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Tue, 23 Feb 2021 01:34:43 +0300 Subject: [PATCH 167/202] test host-port argument combinations (#948) * test host-port argument combinations * CI: PYTEST_HOST=127.0.0.1 --- .github/workflows/ci.yml | 1 + changelog.md | 5 +-- test/features/connection.feature | 23 +++++++++++++ test/features/steps/auto_vertical.py | 3 +- test/features/steps/connection.py | 27 +++++++++++++++ test/features/steps/utils.py | 12 +++++++ test/features/steps/wrappers.py | 51 ++++++++++++++++++++-------- 7 files changed, 104 insertions(+), 18 deletions(-) create mode 100644 test/features/connection.feature create mode 100644 test/features/steps/connection.py create mode 100644 test/features/steps/utils.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ccb15aa6..0a144725 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,7 @@ jobs: - name: Pytest / behave env: PYTEST_PASSWORD: root + PYTEST_HOST: 127.0.0.1 run: | ./setup.py test --pytest-args="--cov-report= --cov=mycli" diff --git a/changelog.md b/changelog.md index b6a9fd94..42bbb8af 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,5 @@ -TBD -======= +TODO +==== Bug Fixes: ---------- @@ -13,6 +13,7 @@ Features: Internal: --------- * Remove unused function is_open_quote() +* Test various host-port combinations in command line arguments 1.23.2 diff --git a/test/features/connection.feature b/test/features/connection.feature new file mode 100644 index 00000000..04d041d3 --- /dev/null +++ b/test/features/connection.feature @@ -0,0 +1,23 @@ +Feature: connect to a database: + + @requires_local_db + Scenario: run mycli on localhost without port + When we run mycli with arguments "host=localhost" without arguments "port" + When we query "status" + Then status contains "via UNIX socket" + + Scenario: run mycli on TCP host without port + When we run mycli without arguments "port" + When we query "status" + Then status contains "via TCP/IP" + + Scenario: run mycli with port but without host + When we run mycli without arguments "host" + When we query "status" + Then status contains "via TCP/IP" + + @requires_local_db + Scenario: run mycli without host and port + When we run mycli without arguments "host port" + When we query "status" + Then status contains "via UNIX socket" diff --git a/test/features/steps/auto_vertical.py b/test/features/steps/auto_vertical.py index 974740d7..e1cb26f8 100644 --- a/test/features/steps/auto_vertical.py +++ b/test/features/steps/auto_vertical.py @@ -3,11 +3,12 @@ from behave import then, when import wrappers +from utils import parse_cli_args_to_dict @when('we run dbcli with {arg}') def step_run_cli_with_arg(context, arg): - wrappers.run_cli(context, run_args=arg.split('=')) + wrappers.run_cli(context, run_args=parse_cli_args_to_dict(arg)) @when('we execute a small query') diff --git a/test/features/steps/connection.py b/test/features/steps/connection.py new file mode 100644 index 00000000..f4a4929a --- /dev/null +++ b/test/features/steps/connection.py @@ -0,0 +1,27 @@ +import shlex +from behave import when, then + +import wrappers +from test.features.steps.utils import parse_cli_args_to_dict + + +@when('we run mycli with arguments "{exact_args}" without arguments "{excluded_args}"') +@when('we run mycli without arguments "{excluded_args}"') +def step_run_cli_without_args(context, excluded_args, exact_args=''): + wrappers.run_cli( + context, + run_args=parse_cli_args_to_dict(exact_args), + exclude_args=parse_cli_args_to_dict(excluded_args).keys() + ) + + +@then('status contains "{expression}"') +def status_contains(context, expression): + wrappers.expect_exact(context, f'{expression}', timeout=5) + + # Normally, the shutdown after scenario waits for the prompt. + # But we may have changed the prompt, depending on parameters, + # so let's wait for its last character + context.cli.expect_exact('>') + context.atprompt = True + diff --git a/test/features/steps/utils.py b/test/features/steps/utils.py new file mode 100644 index 00000000..1ae63d2b --- /dev/null +++ b/test/features/steps/utils.py @@ -0,0 +1,12 @@ +import shlex + + +def parse_cli_args_to_dict(cli_args: str): + args_dict = {} + for arg in shlex.split(cli_args): + if '=' in arg: + key, value = arg.split('=') + args_dict[key] = value + else: + args_dict[arg] = None + return args_dict diff --git a/test/features/steps/wrappers.py b/test/features/steps/wrappers.py index de833dd2..780a1c79 100644 --- a/test/features/steps/wrappers.py +++ b/test/features/steps/wrappers.py @@ -3,6 +3,7 @@ import sys import textwrap + try: from StringIO import StringIO except ImportError: @@ -46,21 +47,41 @@ def expect_pager(context, expected, timeout): context.conf['pager_boundary'], expected), timeout=timeout) -def run_cli(context, run_args=None): +def run_cli(context, run_args=None, exclude_args=None): """Run the process using pexpect.""" - run_args = run_args or [] - if context.conf.get('host', None): - run_args.extend(('-h', context.conf['host'])) - if context.conf.get('user', None): - run_args.extend(('-u', context.conf['user'])) - if context.conf.get('pass', None): - run_args.extend(('-p', context.conf['pass'])) - if context.conf.get('dbname', None): - run_args.extend(('-D', context.conf['dbname'])) - if context.conf.get('defaults-file', None): - run_args.extend(('--defaults-file', context.conf['defaults-file'])) - if context.conf.get('myclirc', None): - run_args.extend(('--myclirc', context.conf['myclirc'])) + run_args = run_args or {} + rendered_args = [] + exclude_args = set(exclude_args) if exclude_args else set() + + conf = dict(**context.conf) + conf.update(run_args) + + def add_arg(name, key, value): + if name not in exclude_args: + if value is not None: + rendered_args.extend((key, value)) + else: + rendered_args.append(key) + + if conf.get('host', None): + add_arg('host', '-h', conf['host']) + if conf.get('user', None): + add_arg('user', '-u', conf['user']) + if conf.get('pass', None): + add_arg('pass', '-p', conf['pass']) + if conf.get('port', None): + add_arg('port', '-P', str(conf['port'])) + if conf.get('dbname', None): + add_arg('dbname', '-D', conf['dbname']) + if conf.get('defaults-file', None): + add_arg('defaults_file', '--defaults-file', conf['defaults-file']) + if conf.get('myclirc', None): + add_arg('myclirc', '--myclirc', conf['myclirc']) + + for arg_name, arg_value in conf.items(): + if arg_name.startswith('-'): + add_arg(arg_name, arg_name, arg_value) + try: cli_cmd = context.conf['cli_command'] except KeyError: @@ -73,7 +94,7 @@ def run_cli(context, run_args=None): '"' ).format(sys.executable) - cmd_parts = [cli_cmd] + run_args + cmd_parts = [cli_cmd] + rendered_args cmd = ' '.join(cmd_parts) context.cli = pexpect.spawnu(cmd, cwd=context.package_root) context.logfile = StringIO() From 4ac38ecf359a1d0738dc2b5919af701387aef62b Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Tue, 23 Feb 2021 15:31:24 +0300 Subject: [PATCH 168/202] Check error code, not message for OperationalError (#964) * Check error code, not message for OperationalError * changelog * merge fix --- changelog.md | 1 + mycli/main.py | 4 ++-- mycli/sqlexecute.py | 3 +++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 42bbb8af..01733229 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,7 @@ Bug Fixes: ---------- * Allow `FileNotFound` exception for SSH config files. * Fix startup error on MySQL < 5.0.22 +* Check error code rather than message for Access Denied error Features: --------- diff --git a/mycli/main.py b/mycli/main.py index feaa78c2..63135a5c 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -43,7 +43,7 @@ from .sqlcompleter import SQLCompleter from .clitoolbar import create_toolbar_tokens_func from .clistyle import style_factory, style_factory_output -from .sqlexecute import FIELD_TYPES, SQLExecute +from .sqlexecute import FIELD_TYPES, SQLExecute, ERROR_CODE_ACCESS_DENIED from .clibuffer import cli_is_multiline from .completion_refresher import CompletionRefresher from .config import (write_default_config, get_mylogin_cnf_path, @@ -432,7 +432,7 @@ def _connect(): ssh_password, ssh_key_filename, init_command ) except OperationalError as e: - if ('Access denied for user' in e.args[1]): + if e.args[0] == ERROR_CODE_ACCESS_DENIED: new_passwd = click.prompt('Password', hide_input=True, show_default=False, type=str, err=True) self.sqlexecute = SQLExecute( diff --git a/mycli/sqlexecute.py b/mycli/sqlexecute.py index ed7d2f04..94614387 100644 --- a/mycli/sqlexecute.py +++ b/mycli/sqlexecute.py @@ -21,6 +21,9 @@ }) +ERROR_CODE_ACCESS_DENIED = 1045 + + class ServerSpecies(enum.Enum): MySQL = 'MySQL' MariaDB = 'MariaDB' From e2ff2cc09ccde085eb11fb4e0c4c1c2742195468 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sun, 17 Jan 2021 11:01:07 +0300 Subject: [PATCH 169/202] switch to pyaes for password decoding --- changelog.md | 6 +++--- mycli/config.py | 17 +++++------------ setup.py | 4 ++-- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/changelog.md b/changelog.md index 01733229..07af7371 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,5 @@ -TODO -==== +TBD +=== Bug Fixes: ---------- @@ -15,7 +15,7 @@ Internal: --------- * Remove unused function is_open_quote() * Test various host-port combinations in command line arguments - +* switched from Cryptography to pyaes for decrypting mylogin.cnf 1.23.2 === diff --git a/mycli/config.py b/mycli/config.py index 0ff1a6e1..53a026b4 100644 --- a/mycli/config.py +++ b/mycli/config.py @@ -9,8 +9,7 @@ from typing import Union from configobj import ConfigObj, ConfigObjError -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes -from cryptography.hazmat.backends import default_backend +import pyaes try: basestring @@ -200,11 +199,9 @@ def read_and_decrypt_mylogin_cnf(f): return None rkey = struct.pack('16B', *rkey) - # Create a decryptor object using the key. - decryptor = _get_decryptor(rkey) - # Create a bytes buffer to hold the plaintext. plaintext = BytesIO() + aes = pyaes.AESModeOfOperationECB(rkey) while True: # Read the length of the ciphertext. @@ -215,7 +212,9 @@ def read_and_decrypt_mylogin_cnf(f): # Read cipher_len bytes from the file and decrypt. cipher = f.read(cipher_len) - plain = _remove_pad(decryptor.update(cipher)) + plain = _remove_pad( + b''.join([aes.decrypt(cipher[i: i + 16]) for i in range(0, cipher_len, 16)]) + ) if plain is False: continue plaintext.write(plain) @@ -259,12 +258,6 @@ def strip_matching_quotes(s): return s -def _get_decryptor(key): - """Get the AES decryptor.""" - c = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend()) - return c.decryptor() - - def _remove_pad(line): """Remove the pad from the *line*.""" try: diff --git a/setup.py b/setup.py index 4aa7f91a..540600cd 100755 --- a/setup.py +++ b/setup.py @@ -23,9 +23,9 @@ 'PyMySQL >= 0.9.2', 'sqlparse>=0.3.0,<0.4.0', 'configobj >= 5.0.5', - 'cryptography >= 1.0.0', 'cli_helpers[styles] >= 2.0.1', - 'pyperclip >= 1.8.1' + 'pyperclip >= 1.8.1', + 'pyaes >= 1.6.1' ] From 11225caba46448f14a505e66add665fb0f319e7e Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Tue, 23 Feb 2021 22:06:13 +0300 Subject: [PATCH 170/202] fixed LGTM issues --- mycli/config.py | 3 +-- mycli/main.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/mycli/config.py b/mycli/config.py index 55d230dd..ce30f2ee 100644 --- a/mycli/config.py +++ b/mycli/config.py @@ -1,6 +1,5 @@ -import shutil from copy import copy -from io import BytesIO, TextIOWrapper, StringIO +from io import BytesIO, TextIOWrapper import logging import os from os.path import exists diff --git a/mycli/main.py b/mycli/main.py index 6ab0f498..c1685a20 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -5,7 +5,6 @@ import logging import threading import re -import fileinput from collections import namedtuple try: from pwd import getpwuid @@ -566,7 +565,7 @@ def run_cli(self): print(sqlexecute.server_info) print('mycli', __version__) print(SUPPORT_INFO) - print('Thanks to the contributor -', thanks_picker([author_file, sponsor_file])) + print('Thanks to the contributor -', thanks_picker()) def get_message(): prompt = self.get_prompt(self.prompt_format) From ed346a68b02ffcc7fc7e8a9a3d1a33c0706a307d Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Tue, 23 Feb 2021 22:07:11 +0300 Subject: [PATCH 171/202] fixed linter warning --- mycli/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mycli/config.py b/mycli/config.py index ce30f2ee..0b67bd44 100644 --- a/mycli/config.py +++ b/mycli/config.py @@ -227,7 +227,8 @@ def read_and_decrypt_mylogin_cnf(f): # Read cipher_len bytes from the file and decrypt. cipher = f.read(cipher_len) plain = _remove_pad( - b''.join([aes.decrypt(cipher[i: i + 16]) for i in range(0, cipher_len, 16)]) + b''.join([aes.decrypt(cipher[i: i + 16]) + for i in range(0, cipher_len, 16)]) ) if plain is False: continue From 72bb7a2a8c47f42e3e1bb1f125aaa37cdd84f12e Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Wed, 24 Feb 2021 14:44:00 -0500 Subject: [PATCH 172/202] Make Alt-Enter context-sensitive, inserting a LF (#924) ... linefeed in single-line mode, but dispatching the command in multi- line mode. --- changelog.md | 1 + mycli/key_bindings.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 9253cff4..037ff4b9 100644 --- a/changelog.md +++ b/changelog.md @@ -10,6 +10,7 @@ Bug Fixes: Features: --------- * Add `-g` shortcut to option `--login-path`. +* Alt-Enter dispatches the command in multi-line mode. Internal: --------- diff --git a/mycli/key_bindings.py b/mycli/key_bindings.py index 57b917bf..4a24c82b 100644 --- a/mycli/key_bindings.py +++ b/mycli/key_bindings.py @@ -78,8 +78,12 @@ def _(event): @kb.add('escape', 'enter') def _(event): - """Introduces a line break regardless of multi-line mode or not.""" + """Introduces a line break in multi-line mode, or dispatches the + command in single-line mode.""" _logger.debug('Detected alt-enter key.') - event.app.current_buffer.insert_text('\n') + if mycli.multi_line: + event.app.current_buffer.validate_and_handle() + else: + event.app.current_buffer.insert_text('\n') return kb From 51b8ee3c1146026b2c58b897a7ef5ad0c10a0836 Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Sun, 28 Feb 2021 00:55:53 +0300 Subject: [PATCH 173/202] fixed login with .my.cnf and .mylogin.cnf --- changelog.md | 1 + mycli/config.py | 58 +++++++++++++++++++++++++++++-- mycli/main.py | 48 +++++++++++++++++-------- test/features/connection.feature | 12 +++++++ test/features/environment.py | 48 +++++++++++++++++++++---- test/features/steps/connection.py | 44 +++++++++++++++++++++++ test/features/steps/wrappers.py | 4 ++- 7 files changed, 191 insertions(+), 24 deletions(-) diff --git a/changelog.md b/changelog.md index 9253cff4..136145aa 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ Bug Fixes: * Allow `FileNotFound` exception for SSH config files. * Fix startup error on MySQL < 5.0.22 * Check error code rather than message for Access Denied error +* Fix login with ~/.my.cnf files Features: --------- diff --git a/mycli/config.py b/mycli/config.py index 0b67bd44..5d711093 100644 --- a/mycli/config.py +++ b/mycli/config.py @@ -5,7 +5,7 @@ from os.path import exists import struct import sys -from typing import Union +from typing import Union, IO from configobj import ConfigObj, ConfigObjError import pyaes @@ -52,9 +52,9 @@ def read_config_file(f, list_values=True): config = ConfigObj(f, interpolation=False, encoding='utf8', list_values=list_values) except ConfigObjError as e: - log(logger, logging.ERROR, "Unable to parse line {0} of config file " + log(logger, logging.WARNING, "Unable to parse line {0} of config file " "'{1}'.".format(e.line_number, f)) - log(logger, logging.ERROR, "Using successfully parsed config values.") + log(logger, logging.WARNING, "Using successfully parsed config values.") return e.config except (IOError, OSError) as e: log(logger, logging.WARNING, "You don't have permission to read " @@ -172,6 +172,58 @@ def open_mylogin_cnf(name): return TextIOWrapper(plaintext) +# TODO reuse code between encryption an decryption +def encrypt_mylogin_cnf(plaintext: IO[str]): + """Encryption of .mylogin.cnf file, analogous to calling + mysql_config_editor. + + Code is based on the python implementation by Kristian Koehntopp + https://github.com/isotopp/mysql-config-coder + + """ + def realkey(key): + """Create the AES key from the login key.""" + rkey = bytearray(16) + for i in range(len(key)): + rkey[i % 16] ^= key[i] + return bytes(rkey) + + def encode_line(plaintext, real_key, buf_len): + aes = pyaes.AESModeOfOperationECB(real_key) + text_len = len(plaintext) + pad_len = buf_len - text_len + pad_chr = bytes(chr(pad_len), "utf8") + plaintext = plaintext.encode() + pad_chr * pad_len + encrypted_text = b''.join( + [aes.encrypt(plaintext[i: i + 16]) + for i in range(0, len(plaintext), 16)] + ) + return encrypted_text + + LOGIN_KEY_LENGTH = 20 + key = os.urandom(LOGIN_KEY_LENGTH) + real_key = realkey(key) + + outfile = BytesIO() + + outfile.write(struct.pack("i", 0)) + outfile.write(key) + + while True: + line = plaintext.readline() + if not line: + break + real_len = len(line) + pad_len = (int(real_len / 16) + 1) * 16 + + outfile.write(struct.pack("i", pad_len)) + x = encode_line(line, real_key, pad_len) + outfile.write(x) + + outfile.seek(0) + return outfile + + def read_and_decrypt_mylogin_cnf(f): """Read and decrypt the contents of .mylogin.cnf. diff --git a/mycli/main.py b/mycli/main.py index c1685a20..2e2b8422 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1,3 +1,4 @@ +from collections import defaultdict from io import open import os import sys @@ -332,20 +333,33 @@ def read_my_cnf_files(self, files, keys): cnf = read_config_files(files, list_values=False) sections = ['client', 'mysqld'] + key_transformations = { + 'mysqld': { + 'socket': 'default_socket', + 'port': 'default_port', + }, + } + if self.login_path and self.login_path != 'client': sections.append(self.login_path) if self.defaults_suffix: sections.extend([sect + self.defaults_suffix for sect in sections]) - def get(key): - result = None - for sect in cnf: - if sect in sections and key in cnf[sect]: - result = strip_matching_quotes(cnf[sect][key]) - return result + configuration = defaultdict(lambda: None) + for key in keys: + for section in cnf: + if ( + section not in sections or + key not in cnf[section] + ): + continue + new_key = key_transformations.get(section, {}).get(key) or key + configuration[new_key] = strip_matching_quotes( + cnf[section][key]) + + return configuration - return {x: get(x) for x in keys} def merge_ssl_with_cnf(self, ssl, cnf): """Merge SSL configuration dict with cnf dict""" @@ -381,6 +395,7 @@ def connect(self, database='', user='', passwd='', host='', port='', 'host': None, 'port': None, 'socket': None, + 'default_socket': None, 'default-character-set': None, 'local-infile': None, 'loose-local-infile': None, @@ -394,18 +409,23 @@ def connect(self, database='', user='', passwd='', host='', port='', cnf = self.read_my_cnf_files(self.cnf_files, cnf.keys()) # Fall back to config values only if user did not specify a value. - database = database or cnf['database'] - # Socket interface not supported for SSH connections - if port or (host and host != 'localhost') or (ssh_host and ssh_port): - socket = '' - else: - socket = socket or cnf['socket'] or guess_socket_location() user = user or cnf['user'] or os.getenv('USER') host = host or cnf['host'] - port = int(port or cnf['port'] or 3306) + port = port or cnf['port'] ssl = ssl or {} + port = port and int(port) + if not port: + port = 3306 + if not host or host == 'localhost': + socket = ( + cnf['socket'] or + cnf['default_socket'] or + guess_socket_location() + ) + + passwd = passwd if isinstance(passwd, str) else cnf['password'] charset = charset or cnf['default-character-set'] or 'utf8' diff --git a/test/features/connection.feature b/test/features/connection.feature index 04d041d3..b06935ea 100644 --- a/test/features/connection.feature +++ b/test/features/connection.feature @@ -21,3 +21,15 @@ Feature: connect to a database: When we run mycli without arguments "host port" When we query "status" Then status contains "via UNIX socket" + + Scenario: run mycli with my.cnf configuration + When we create my.cnf file + When we run mycli without arguments "host port user pass defaults_file" + Then we are logged in + + Scenario: run mycli with mylogin.cnf configuration + When we create mylogin.cnf file + When we run mycli with arguments "login_path=test_login_path" without arguments "host port user pass defaults_file" + Then we are logged in + + diff --git a/test/features/environment.py b/test/features/environment.py index 98c20049..1ea0f086 100644 --- a/test/features/environment.py +++ b/test/features/environment.py @@ -1,4 +1,5 @@ import os +import shutil import sys from tempfile import mkstemp @@ -11,6 +12,24 @@ test_log_file = os.path.join(os.environ['HOME'], '.mycli.test.log') +SELF_CONNECTING_FEATURES = ( + 'test/features/connection.feature', +) + + +MY_CNF_PATH = os.path.expanduser('~/.my.cnf') +MY_CNF_BACKUP_PATH = f'{MY_CNF_PATH}.backup' +MYLOGIN_CNF_PATH = os.path.expanduser('~/.mylogin.cnf') +MYLOGIN_CNF_BACKUP_PATH = f'{MYLOGIN_CNF_PATH}.backup' + + +def get_db_name_from_context(context): + return context.config.userdata.get( + 'my_test_db', None + ) or "mycli_behave_tests" + + + def before_all(context): """Set env parameters.""" os.environ['LINES'] = "100" @@ -22,7 +41,7 @@ def before_all(context): test_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) login_path_file = os.path.join(test_dir, 'mylogin.cnf') - os.environ['MYSQL_TEST_LOGIN_FILE'] = login_path_file +# os.environ['MYSQL_TEST_LOGIN_FILE'] = login_path_file context.package_root = os.path.abspath( os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) @@ -33,8 +52,7 @@ def before_all(context): context.exit_sent = False vi = '_'.join([str(x) for x in sys.version_info[:3]]) - db_name = context.config.userdata.get( - 'my_test_db', None) or "mycli_behave_tests" + db_name = get_db_name_from_context(context) db_name_full = '{0}_{1}'.format(db_name, vi) # Store get params from config/environment variables @@ -104,11 +122,18 @@ def before_step(context, _): context.atprompt = False -def before_scenario(context, _): +def before_scenario(context, arg): with open(test_log_file, 'w') as f: f.write('') - run_cli(context) - wait_prompt(context) + if arg.location.filename not in SELF_CONNECTING_FEATURES: + run_cli(context) + wait_prompt(context) + + if os.path.exists(MY_CNF_PATH): + shutil.move(MY_CNF_PATH, MY_CNF_BACKUP_PATH) + + if os.path.exists(MYLOGIN_CNF_PATH): + shutil.move(MYLOGIN_CNF_PATH, MYLOGIN_CNF_BACKUP_PATH) def after_scenario(context, _): @@ -134,6 +159,17 @@ def after_scenario(context, _): context.cli.sendcontrol('d') context.cli.expect_exact(pexpect.EOF, timeout=5) + if os.path.exists(MY_CNF_BACKUP_PATH): + shutil.move(MY_CNF_BACKUP_PATH, MY_CNF_PATH) + + if os.path.exists(MYLOGIN_CNF_BACKUP_PATH): + shutil.move(MYLOGIN_CNF_BACKUP_PATH, MYLOGIN_CNF_PATH) + elif os.path.exists(MYLOGIN_CNF_PATH): + # This file was moved in `before_scenario`. + # If it exists now, it has been created during a test + os.remove(MYLOGIN_CNF_PATH) + + # TODO: uncomment to debug a failure # def after_step(context, step): # if step.status == "failed": diff --git a/test/features/steps/connection.py b/test/features/steps/connection.py index f4a4929a..e16dd867 100644 --- a/test/features/steps/connection.py +++ b/test/features/steps/connection.py @@ -1,8 +1,18 @@ +import io +import os import shlex + from behave import when, then +import pexpect import wrappers from test.features.steps.utils import parse_cli_args_to_dict +from test.features.environment import MY_CNF_PATH, MYLOGIN_CNF_PATH, get_db_name_from_context +from test.utils import HOST, PORT, USER, PASSWORD +from mycli.config import encrypt_mylogin_cnf + + +TEST_LOGIN_PATH = 'test_login_path' @when('we run mycli with arguments "{exact_args}" without arguments "{excluded_args}"') @@ -25,3 +35,37 @@ def status_contains(context, expression): context.cli.expect_exact('>') context.atprompt = True + +@when('we create my.cnf file') +def step_create_my_cnf_file(context): + my_cnf = ( + '[client]\n' + f'host = {HOST}\n' + f'port = {PORT}\n' + f'user = {USER}\n' + f'password = {PASSWORD}\n' + ) + with open(MY_CNF_PATH, 'w') as f: + f.write(my_cnf) + + +@when('we create mylogin.cnf file') +def step_create_mylogin_cnf_file(context): + os.environ.pop('MYSQL_TEST_LOGIN_FILE', None) + mylogin_cnf = ( + f'[{TEST_LOGIN_PATH}]\n' + f'host = {HOST}\n' + f'port = {PORT}\n' + f'user = {USER}\n' + f'password = {PASSWORD}\n' + ) + with open(MYLOGIN_CNF_PATH, 'wb') as f: + input_file = io.StringIO(mylogin_cnf) + f.write(encrypt_mylogin_cnf(input_file).read()) + + +@then('we are logged in') +def we_are_logged_in(context): + db_name = get_db_name_from_context(context) + context.cli.expect_exact(f'{db_name}>', timeout=5) + context.atprompt = True diff --git a/test/features/steps/wrappers.py b/test/features/steps/wrappers.py index 780a1c79..6408f235 100644 --- a/test/features/steps/wrappers.py +++ b/test/features/steps/wrappers.py @@ -14,7 +14,7 @@ def expect_exact(context, expected, timeout): timedout = False try: context.cli.expect_exact(expected, timeout=timeout) - except pexpect.exceptions.TIMEOUT: + except pexpect.TIMEOUT: timedout = True if timedout: # Strip color codes out of the output. @@ -77,6 +77,8 @@ def add_arg(name, key, value): add_arg('defaults_file', '--defaults-file', conf['defaults-file']) if conf.get('myclirc', None): add_arg('myclirc', '--myclirc', conf['myclirc']) + if conf.get('login_path'): + add_arg('login_path', '--login-path', conf['login_path']) for arg_name, arg_value in conf.items(): if arg_name.startswith('-'): From 05c87d8f29d3caaf6a437db336feb0735d4d085e Mon Sep 17 00:00:00 2001 From: jerome provensal <552382+jeromegit@users.noreply.github.com> Date: Mon, 1 Mar 2021 08:54:15 -0800 Subject: [PATCH 174/202] Allow to pass file instead of password (#913) * allow to pass a file or FIFO as password with --password /my/file/path as suggested in this best-practice https://www.netmeister.org/blog/passing-passwords.html article * allow to pass a file or FIFO as password with --password /my/file/path as suggested in this best-practice https://www.netmeister.org/blog/passing-passwords.html article (including change to changelog and AUTHORS) * A few changes based on input received after the pull request Co-authored-by: Georgy Frolov --- README.md | 3 ++- changelog.md | 1 + mycli/AUTHORS | 1 + mycli/main.py | 33 ++++++++++++++++++++++++++++----- mycli/packages/parseutils.py | 3 ++- 5 files changed, 34 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8da08a44..46b5fd3f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![Build Status](https://github.com/dbcli/mycli/workflows/mycli/badge.svg)](https://github.com/dbcli/mycli/actions?query=workflow%3Amycli) [![PyPI](https://img.shields.io/pypi/v/mycli.svg?style=plastic)](https://pypi.python.org/pypi/mycli) -[![LGTM](https://img.shields.io/lgtm/grade/python/github/dbcli/mycli.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/dbcli/mycli/context:python) A command line client for MySQL that can do auto-completion and syntax highlighting. @@ -109,6 +108,8 @@ $ sudo apt-get install mycli # Only on debian or ubuntu -e, --execute TEXT Execute command and quit. --init-command TEXT SQL statement to execute after connecting. --charset TEXT Character set for MySQL session. + --password-file PATH File or FIFO path containing the password + to connect to the db if not specified otherwise --help Show this message and exit. diff --git a/changelog.md b/changelog.md index 5aff432b..965ef97b 100644 --- a/changelog.md +++ b/changelog.md @@ -12,6 +12,7 @@ Features: --------- * Add `-g` shortcut to option `--login-path`. * Alt-Enter dispatches the command in multi-line mode. +* Allow to pass a file or FIFO path with --password-file when password is not specified or is failing (as suggested in this best-practice https://www.netmeister.org/blog/passing-passwords.html) Internal: --------- diff --git a/mycli/AUTHORS b/mycli/AUTHORS index c871f510..8cdea919 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -84,6 +84,7 @@ Contributors: * xeron * 0xflotus * Seamile + * Jerome Provensal Creator: -------- diff --git a/mycli/main.py b/mycli/main.py index 2e2b8422..3f08e9c3 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -6,6 +6,8 @@ import logging import threading import re +import stat +import fileinput from collections import namedtuple try: from pwd import getpwuid @@ -387,7 +389,7 @@ def merge_ssl_with_cnf(self, ssl, cnf): def connect(self, database='', user='', passwd='', host='', port='', socket='', charset='', local_infile='', ssl='', ssh_user='', ssh_host='', ssh_port='', - ssh_password='', ssh_key_filename='', init_command=''): + ssh_password='', ssh_key_filename='', init_command='', password_file=''): cnf = {'database': None, 'user': None, @@ -443,6 +445,10 @@ def connect(self, database='', user='', passwd='', host='', port='', if not any(v for v in ssl.values()): ssl = None + # if the passwd is not specfied try to set it using the password_file option + password_from_file = self.get_password_from_file(password_file) + passwd = passwd or password_from_file + # Connect to the database. def _connect(): @@ -454,8 +460,11 @@ def _connect(): ) except OperationalError as e: if e.args[0] == ERROR_CODE_ACCESS_DENIED: - new_passwd = click.prompt('Password', hide_input=True, - show_default=False, type=str, err=True) + if password_from_file: + new_passwd = password_from_file + else: + new_passwd = click.prompt('Password', hide_input=True, + show_default=False, type=str, err=True) self.sqlexecute = SQLExecute( database, user, new_passwd, host, port, socket, charset, local_infile, ssl, ssh_user, ssh_host, @@ -510,6 +519,17 @@ def _connect(): self.echo(str(e), err=True, fg='red') exit(1) + def get_password_from_file(self, password_file): + password_from_file = None + if password_file: + if (os.path.isfile(password_file) or stat.S_ISFIFO(os.stat(password_file).st_mode)) \ + and os.access(password_file, os.R_OK): + with open(password_file) as fp: + password_from_file = fp.readline() + password_from_file = password_from_file.rstrip().lstrip() + + return password_from_file + def handle_editor_command(self, text): r"""Editor command is any query that is prefixed or suffixed by a '\e'. The reason for a while loop is because a user might edit a query @@ -1112,6 +1132,8 @@ def get_last_query(self): help='SQL statement to execute after connecting.') @click.option('--charset', type=str, help='Character set for MySQL session.') +@click.option('--password-file', type=click.Path(), + help='File or FIFO path containing the password to connect to the db if not specified otherwise.') @click.argument('database', default='', nargs=1) def cli(database, user, host, port, socket, password, dbname, version, verbose, prompt, logfile, defaults_group_suffix, @@ -1120,7 +1142,7 @@ def cli(database, user, host, port, socket, password, dbname, ssl_verify_server_cert, table, csv, warn, execute, myclirc, dsn, list_dsn, ssh_user, ssh_host, ssh_port, ssh_password, ssh_key_filename, list_ssh_config, ssh_config_path, ssh_config_host, - init_command, charset): + init_command, charset, password_file): """A MySQL terminal client with auto-completion and syntax highlighting. \b @@ -1246,7 +1268,8 @@ def cli(database, user, host, port, socket, password, dbname, ssh_password=ssh_password, ssh_key_filename=ssh_key_filename, init_command=init_command, - charset=charset + charset=charset, + password_file=password_file ) mycli.logger.debug('Launch Params: \n' diff --git a/mycli/packages/parseutils.py b/mycli/packages/parseutils.py index 90a868ed..fa5f2c9e 100644 --- a/mycli/packages/parseutils.py +++ b/mycli/packages/parseutils.py @@ -12,7 +12,8 @@ 'most_punctuations': re.compile(r'([^\.():,\s]+)$'), # This matches everything except a space. 'all_punctuations': re.compile(r'([^\s]+)$'), - } +} + def last_word(text, include='alphanum_underscore'): r""" From 0ff0b835f7d4b37f9de23e70b1dc73732ee1afcc Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Mon, 1 Mar 2021 20:30:35 +0300 Subject: [PATCH 175/202] readme badges --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 46b5fd3f..cc04a910 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # mycli [![Build Status](https://github.com/dbcli/mycli/workflows/mycli/badge.svg)](https://github.com/dbcli/mycli/actions?query=workflow%3Amycli) -[![PyPI](https://img.shields.io/pypi/v/mycli.svg?style=plastic)](https://pypi.python.org/pypi/mycli) +[![PyPI](https://img.shields.io/pypi/v/mycli.svg)](https://pypi.python.org/pypi/mycli) +[![LGTM](https://img.shields.io/lgtm/grade/python/github/dbcli/mycli.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/dbcli/mycli/context:python) A command line client for MySQL that can do auto-completion and syntax highlighting. From 41c53ad9c3af54111df695c6cf3420d06c2627f9 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Wed, 3 Mar 2021 14:09:40 -0800 Subject: [PATCH 176/202] Update changelog for 1.24.0 release. --- changelog.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/changelog.md b/changelog.md index 965ef97b..cfca6526 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,11 @@ -TBD -=== +TBD: +==== + +* + + +1.24.0 +====== Bug Fixes: ---------- @@ -23,21 +29,21 @@ Internal: 1.23.2 -=== +====== Bug Fixes: ---------- * Ensure `--port` is always an int. 1.23.1 -=== +====== Bug Fixes: ---------- * Allow `--host` without `--port` to make a TCP connection. 1.23.0 -=== +====== Bug Fixes: ---------- From 2516255fffbdfb9ea1e349960c37b2c8a72454e2 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Wed, 3 Mar 2021 14:10:35 -0800 Subject: [PATCH 177/202] Releasing version 1.24.0 --- mycli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/__init__.py b/mycli/__init__.py index 375471f7..6376baeb 100644 --- a/mycli/__init__.py +++ b/mycli/__init__.py @@ -1 +1 @@ -__version__ = '1.23.2' +__version__ = '1.24.0' From fb7de05e28458a05514fac225a9706699ee9f0d3 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Wed, 3 Mar 2021 14:10:35 -0800 Subject: [PATCH 178/202] Releasing version 1.24.0 --- changelog.md | 4 +++- mycli/__init__.py | 2 +- setup.py | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index cfca6526..79fc78a0 100644 --- a/changelog.md +++ b/changelog.md @@ -1,7 +1,9 @@ TBD: ==== -* +Bug Fixes: +--------- +* Restore dependency on cryptography for the interactive password prompt 1.24.0 diff --git a/mycli/__init__.py b/mycli/__init__.py index 375471f7..6376baeb 100644 --- a/mycli/__init__.py +++ b/mycli/__init__.py @@ -1 +1 @@ -__version__ = '1.23.2' +__version__ = '1.24.0' diff --git a/setup.py b/setup.py index 5acbae7c..1d619f33 100755 --- a/setup.py +++ b/setup.py @@ -18,6 +18,7 @@ install_requirements = [ 'click >= 7.0', + 'cryptography >= 1.0.0', 'Pygments >= 1.6', 'prompt_toolkit>=3.0.6,<4.0.0', 'PyMySQL >= 0.9.2', From 8760e633776ad76903d938b662a6905ed21d7086 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Wed, 3 Mar 2021 14:53:19 -0800 Subject: [PATCH 179/202] Update changelog for 1.24.1 release. --- changelog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/changelog.md b/changelog.md index 79fc78a0..95e594f1 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,11 @@ TBD: ==== +* + +1.24.1: +======= + Bug Fixes: --------- * Restore dependency on cryptography for the interactive password prompt From 226dcf10fe007d993a9bb99bafdc06f3a0b96d9c Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Wed, 3 Mar 2021 14:53:37 -0800 Subject: [PATCH 180/202] Releasing version 1.24.1 --- mycli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/__init__.py b/mycli/__init__.py index 6376baeb..785c3b89 100644 --- a/mycli/__init__.py +++ b/mycli/__init__.py @@ -1 +1 @@ -__version__ = '1.24.0' +__version__ = '1.24.1' From 96e6c951f221d10be27d95fc5fbcdc8ce3710a7b Mon Sep 17 00:00:00 2001 From: Georgy Frolov Date: Fri, 12 Mar 2021 21:15:06 +0300 Subject: [PATCH 181/202] upgrade sqlparse (#969) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1d619f33..d049bd58 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ 'Pygments >= 1.6', 'prompt_toolkit>=3.0.6,<4.0.0', 'PyMySQL >= 0.9.2', - 'sqlparse>=0.3.0,<0.4.0', + 'sqlparse>=0.3.0,<0.5.0', 'configobj >= 5.0.5', 'cli_helpers[styles] >= 2.0.1', 'pyperclip >= 1.8.1', From ba7baaa9998bc78576321afda0369b8f8c5b0805 Mon Sep 17 00:00:00 2001 From: Carlos Afonso Date: Wed, 31 Mar 2021 12:35:06 -0300 Subject: [PATCH 182/202] Fix failing autocompletion for multiple JOINs in the same query --- mycli/packages/parseutils.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mycli/packages/parseutils.py b/mycli/packages/parseutils.py index fa5f2c9e..d47f59a5 100644 --- a/mycli/packages/parseutils.py +++ b/mycli/packages/parseutils.py @@ -81,6 +81,13 @@ def extract_from_part(parsed, stop_at_punctuation=True): yield x elif stop_at_punctuation and item.ttype is Punctuation: return + # Multiple JOINs in the same query won't work properly since + # "ON" is a keyword and will trigger the next elif condition. + # So instead of stooping the loop when finding an "ON" skip it + # eg: 'SELECT * FROM abc JOIN def ON abc.id = def.abc_id JOIN ghi' + elif item.ttype is Keyword and item.value.upper() == 'ON': + tbl_prefix_seen = False + continue # An incomplete nested select won't be recognized correctly as a # sub-select. eg: 'SELECT * FROM (SELECT id FROM user'. This causes # the second FROM to trigger this elif condition resulting in a From 14c331e198a09077fafa82cc724c382daec22d57 Mon Sep 17 00:00:00 2001 From: Carlos Afonso Date: Wed, 31 Mar 2021 12:35:10 -0300 Subject: [PATCH 183/202] Add test for multiple join autocompletion --- test/test_completion_engine.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/test_completion_engine.py b/test/test_completion_engine.py index 9e7c608b..8b06ed38 100644 --- a/test/test_completion_engine.py +++ b/test/test_completion_engine.py @@ -393,6 +393,17 @@ def test_join_using_suggests_common_columns(col_list): 'tables': [(None, 'abc', None), (None, 'def', None)], 'drop_unique': True}] +@pytest.mark.parametrize('sql', [ + 'SELECT * FROM abc a JOIN def d ON a.id = d.id JOIN ghi g ON g.', + 'SELECT * FROM abc a JOIN def d ON a.id = d.id AND a.id2 = d.id2 JOIN ghi g ON d.id = g.id AND g.', +]) +def test_two_join_alias_dot_suggests_cols1(sql): + suggestions = suggest_type(sql, sql) + assert sorted_dicts(suggestions) == sorted_dicts([ + {'type': 'column', 'tables': [(None, 'ghi', 'g')]}, + {'type': 'table', 'schema': 'g'}, + {'type': 'view', 'schema': 'g'}, + {'type': 'function', 'schema': 'g'}]) def test_2_statements_2nd_current(): suggestions = suggest_type('select * from a; select * from ', From e31b64c0c8377bb9f4edd7897a04dd4902e70326 Mon Sep 17 00:00:00 2001 From: Carlos Afonso Date: Wed, 31 Mar 2021 12:41:18 -0300 Subject: [PATCH 184/202] Update changelog.md and add me to AUTHORS --- changelog.md | 5 ++++- mycli/AUTHORS | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 95e594f1..d9f0dd6c 100644 --- a/changelog.md +++ b/changelog.md @@ -1,7 +1,9 @@ TBD: ==== -* +Bug Fixes: +--------- +* Fix autocompletion for more than one JOIN 1.24.1: ======= @@ -844,6 +846,7 @@ Bug Fixes: [spacewander]: https://github.com/spacewander [Thomas Roten]: https://github.com/tsroten [Artem Bezsmertnyi]: https://github.com/mrdeathless +[Carlos Afonso]: https://github.com/afonsocarlos [Mikhail Borisov]: https://github.com/borman [Casper Langemeijer]: Casper Langemeijer [Lennart Weller]: https://github.com/lhw diff --git a/mycli/AUTHORS b/mycli/AUTHORS index 8cdea919..a4e3ae57 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -24,6 +24,7 @@ Contributors: * Casper Langemeijer * Jonathan Slenders * Artem Bezsmertnyi + * Carlos Afonso * Mikhail Borisov * Heath Naylor * Phil Cohen From 9fad9ae138a67eba9c5d96b17ea10ddebf213c34 Mon Sep 17 00:00:00 2001 From: Carlos Afonso Date: Wed, 31 Mar 2021 12:42:49 -0300 Subject: [PATCH 185/202] Sort Contributors alphabetically --- changelog.md | 35 ++++++++-------- mycli/AUTHORS | 114 +++++++++++++++++++++++++------------------------- 2 files changed, 74 insertions(+), 75 deletions(-) diff --git a/changelog.md b/changelog.md index d9f0dd6c..5a608ccc 100644 --- a/changelog.md +++ b/changelog.md @@ -43,7 +43,7 @@ Internal: Bug Fixes: ---------- * Ensure `--port` is always an int. - + 1.23.1 ====== @@ -836,32 +836,31 @@ Bug Fixes: ---------- * Fixed the installation issues with PyMySQL dependency on case-sensitive file systems. -[Daniel West]: http://github.com/danieljwest -[Irina Truong]: https://github.com/j-bennet [Amjith Ramanujam]: https://blog.amjith.com -[Kacper Kwapisz]: https://github.com/KKKas -[Martijn Engler]: https://github.com/martijnengler -[Matheus Rosa]: https://github.com/mdsrosa -[Shoma Suzuki]: https://github.com/shoma -[spacewander]: https://github.com/spacewander -[Thomas Roten]: https://github.com/tsroten [Artem Bezsmertnyi]: https://github.com/mrdeathless [Carlos Afonso]: https://github.com/afonsocarlos -[Mikhail Borisov]: https://github.com/borman -[Casper Langemeijer]: Casper Langemeijer -[Lennart Weller]: https://github.com/lhw -[Phil Cohen]: https://github.com/phlipper -[Terseus]: https://github.com/Terseus -[William GARCIA]: https://github.com/willgarcia -[Jonathan Slenders]: https://github.com/jonathanslenders [Casper Langemeijer]: https://github.com/langemeijer -[Scrappy Soft]: https://github.com/scrappysoft +[Daniel West]: http://github.com/danieljwest [Dick Marinus]: https://github.com/meeuw [François Pietka]: https://github.com/fpietka [Frederic Aoustin]: https://github.com/fraoustin [Georgy Frolov]: https://github.com/pasenor -[Zach DeCook]: https://zachdecook.com +[Irina Truong]: https://github.com/j-bennet +[Jonathan Slenders]: https://github.com/jonathanslenders +[Kacper Kwapisz]: https://github.com/KKKas [laixintao]: https://github.com/laixintao +[Lennart Weller]: https://github.com/lhw +[Martijn Engler]: https://github.com/martijnengler +[Matheus Rosa]: https://github.com/mdsrosa +[Mikhail Borisov]: https://github.com/borman [mtorromeo]: https://github.com/mtorromeo [mwcm]: https://github.com/mwcm +[Phil Cohen]: https://github.com/phlipper +[Scrappy Soft]: https://github.com/scrappysoft +[Shoma Suzuki]: https://github.com/shoma +[spacewander]: https://github.com/spacewander +[Terseus]: https://github.com/Terseus +[Thomas Roten]: https://github.com/tsroten +[William GARCIA]: https://github.com/willgarcia [xeron]: https://github.com/xeron +[Zach DeCook]: https://zachdecook.com diff --git a/mycli/AUTHORS b/mycli/AUTHORS index a4e3ae57..a5232adb 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -15,77 +15,77 @@ Core Developers: Contributors: ------------- - * Steve Robbins - * Shoma Suzuki - * Daniel West - * Scrappy Soft - * Daniel Black - * Jonathan Bruno - * Casper Langemeijer - * Jonathan Slenders + * 0xflotus + * Abirami P + * Adam Chainz + * Aljosha Papsch + * Andy Teijelo Pérez + * Angelo Lupo * Artem Bezsmertnyi + * bitkeen + * bjarnagin + * caitinggui * Carlos Afonso - * Mikhail Borisov + * Casper Langemeijer + * chainkite + * Colin Caine + * cxbig + * Daniel Black + * Daniel West + * Daniël van Eeden + * François Pietka + * Frederic Aoustin + * Georgy Frolov * Heath Naylor - * Phil Cohen - * spacewander - * Adam Chainz + * Huachao Mao + * Jakub Boukal + * jbruno + * Jerome Provensal + * Jialong Liu * Johannes Hoff + * John Sterling + * Jonathan Bruno + * Jonathan Lloyd + * Jonathan Slenders * Kacper Kwapisz + * kevinhwang91 + * KITAGAWA Yasutaka + * Klaus Wünschel + * laixintao * Lennart Weller * Martijn Engler + * Massimiliano Torromeo + * Michał Górny + * Mike Palandra + * Mikhail Borisov + * Morgan Mitchell + * mrdeathless + * Nathan Huang + * Nicolas Palumbo + * Phil Cohen + * QiaoHou Peng + * Roland Walker + * Ryan Smith + * Scrappy Soft + * Seamile + * Shoma Suzuki + * spacewander + * Steve Robbins + * Takeshi D. Itoh + * Terje Røsten * Terseus * Tyler Kuipers + * ushuz * William GARCIA + * xeron + * Yang Zou * Yasuhiro Matsumoto - * bjarnagin - * jbruno - * mrdeathless - * Abirami P - * John Sterling - * Jialong Liu - * Zhidong - * Daniël van Eeden + * Zach DeCook + * Zane C. Bowers-Hadley * zer09 - * cxbig - * chainkite - * Michał Górny - * Terje Røsten - * Ryan Smith - * Klaus Wünschel - * François Pietka - * Colin Caine - * Frederic Aoustin - * caitinggui - * ushuz * Zhaolong Zhu + * Zhidong * Zhongyang Guan - * Huachao Mao - * QiaoHou Peng - * Yang Zou - * Angelo Lupo - * Aljosha Papsch - * Zane C. Bowers-Hadley - * Mike Palandra - * Georgy Frolov - * Jonathan Lloyd - * Nathan Huang - * Jakub Boukal - * Takeshi D. Itoh - * laixintao - * Zach DeCook - * kevinhwang91 - * KITAGAWA Yasutaka - * Nicolas Palumbo - * Andy Teijelo Pérez - * bitkeen - * Morgan Mitchell - * Massimiliano Torromeo - * Roland Walker - * xeron - * 0xflotus - * Seamile - * Jerome Provensal Creator: -------- From 4114bcf90d888357e3adc4de108988c606fbb32e Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Fri, 23 Apr 2021 21:27:24 +0200 Subject: [PATCH 186/202] deprecate python mock --- changelog.md | 4 ++++ requirements-dev.txt | 1 - test/test_completion_refresher.py | 2 +- test/test_naive_completion.py | 2 +- test/test_smart_completion_public_schema_only.py | 4 ++-- test/test_special_iocommands.py | 2 +- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/changelog.md b/changelog.md index 95e594f1..18f775c8 100644 --- a/changelog.md +++ b/changelog.md @@ -10,6 +10,10 @@ Bug Fixes: --------- * Restore dependency on cryptography for the interactive password prompt +Internal: +--------- +* Deprecate Python mock + 1.24.0 ====== diff --git a/requirements-dev.txt b/requirements-dev.txt index 7a38ed5c..9c403160 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,3 @@ -mock pytest!=3.3.0 pytest-cov==2.4.0 tox diff --git a/test/test_completion_refresher.py b/test/test_completion_refresher.py index 1ed63774..cdc2fb5e 100644 --- a/test/test_completion_refresher.py +++ b/test/test_completion_refresher.py @@ -1,6 +1,6 @@ import time import pytest -from mock import Mock, patch +from unittest.mock import Mock, patch @pytest.fixture diff --git a/test/test_naive_completion.py b/test/test_naive_completion.py index 14c1bf5a..32b2abdf 100644 --- a/test/test_naive_completion.py +++ b/test/test_naive_completion.py @@ -11,7 +11,7 @@ def completer(): @pytest.fixture def complete_event(): - from mock import Mock + from unittest.mock import Mock return Mock() diff --git a/test/test_smart_completion_public_schema_only.py b/test/test_smart_completion_public_schema_only.py index b66c696b..e7d460a8 100644 --- a/test/test_smart_completion_public_schema_only.py +++ b/test/test_smart_completion_public_schema_only.py @@ -1,5 +1,5 @@ import pytest -from mock import patch +from unittest.mock import patch from prompt_toolkit.completion import Completion from prompt_toolkit.document import Document import mycli.packages.special.main as special @@ -35,7 +35,7 @@ def completer(): @pytest.fixture def complete_event(): - from mock import Mock + from unittest.mock import Mock return Mock() diff --git a/test/test_special_iocommands.py b/test/test_special_iocommands.py index 73bfbabe..8b6be337 100644 --- a/test/test_special_iocommands.py +++ b/test/test_special_iocommands.py @@ -2,7 +2,7 @@ import stat import tempfile from time import time -from mock import patch +from unittest.mock import patch import pytest from pymysql import ProgrammingError From 4ab908e1f27b664d2ff78201a8b2d434f2c7ffba Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Mon, 26 Apr 2021 12:21:54 +0000 Subject: [PATCH 187/202] Set daemon attribute instead of using setDaemon method that was deprecated in Python 3.10 --- mycli/completion_refresher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/completion_refresher.py b/mycli/completion_refresher.py index e6c8dd07..124068a9 100644 --- a/mycli/completion_refresher.py +++ b/mycli/completion_refresher.py @@ -36,7 +36,7 @@ def refresh(self, executor, callbacks, completer_options=None): target=self._bg_refresh, args=(executor, callbacks, completer_options), name='completion_refresh') - self._completer_thread.setDaemon(True) + self._completer_thread.daemon = True self._completer_thread.start() return [(None, None, None, 'Auto-completion refresh started in the background.')] From 32759643d8da42dff47a32f35cde1b50159013e7 Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Mon, 26 Apr 2021 12:25:49 +0000 Subject: [PATCH 188/202] Add myself to authors. --- mycli/AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/mycli/AUTHORS b/mycli/AUTHORS index 8cdea919..7baed189 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -85,6 +85,7 @@ Contributors: * 0xflotus * Seamile * Jerome Provensal + * Karthikeyan Singaravelan Creator: -------- From d62eefdc819a11ecdb97d93dd7ad1922d28a3795 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sat, 8 May 2021 23:30:04 -0700 Subject: [PATCH 189/202] Remove the weird dash char in myclirc. (#985) --- mycli/myclirc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mycli/myclirc b/mycli/myclirc index 0bde2007..c89caa05 100644 --- a/mycli/myclirc +++ b/mycli/myclirc @@ -60,8 +60,8 @@ wider_completion_menu = False # \n - Newline # \P - AM/PM # \p - Port -# \R - The current time, in 24-hour military time (0–23) -# \r - The current time, standard 12-hour time (1–12) +# \R - The current time, in 24-hour military time (0-23) +# \r - The current time, standard 12-hour time (1-12) # \s - Seconds of the current time # \t - Product type (Percona, MySQL, MariaDB) # \A - DSN alias name (from the [alias_dsn] section) From 59d377458118d40ef53b663d688648ad7db6d344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Eeden?= Date: Mon, 15 Nov 2021 09:26:52 +0100 Subject: [PATCH 190/202] Improve TiDB compatibility --- changelog.md | 1 + mycli/packages/special/dbcommands.py | 36 +++++++++++++++------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/changelog.md b/changelog.md index 0d7ea49d..4097dfe5 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,7 @@ TBD: Bug Fixes: --------- * Fix autocompletion for more than one JOIN +* Fix the status command when connected to TiDB or other servers that don't implement 'Threads\_connected' 1.24.1: ======= diff --git a/mycli/packages/special/dbcommands.py b/mycli/packages/special/dbcommands.py index ed90e4c3..45d70690 100644 --- a/mycli/packages/special/dbcommands.py +++ b/mycli/packages/special/dbcommands.py @@ -135,23 +135,25 @@ def status(cur, **_): else: output.append(('UNIX socket:', variables['socket'])) - output.append(('Uptime:', format_uptime(status['Uptime']))) - - # Print the current server statistics. - stats = [] - stats.append('Connections: {0}'.format(status['Threads_connected'])) - if 'Queries' in status: - stats.append('Queries: {0}'.format(status['Queries'])) - stats.append('Slow queries: {0}'.format(status['Slow_queries'])) - stats.append('Opens: {0}'.format(status['Opened_tables'])) - stats.append('Flush tables: {0}'.format(status['Flush_commands'])) - stats.append('Open tables: {0}'.format(status['Open_tables'])) - if 'Queries' in status: - queries_per_second = int(status['Queries']) / int(status['Uptime']) - stats.append('Queries per second avg: {:.3f}'.format( - queries_per_second)) - stats = ' '.join(stats) - footer.append('\n' + stats) + if 'Uptime' in status: + output.append(('Uptime:', format_uptime(status['Uptime']))) + + if 'Threads_connected' in status: + # Print the current server statistics. + stats = [] + stats.append('Connections: {0}'.format(status['Threads_connected'])) + if 'Queries' in status: + stats.append('Queries: {0}'.format(status['Queries'])) + stats.append('Slow queries: {0}'.format(status['Slow_queries'])) + stats.append('Opens: {0}'.format(status['Opened_tables'])) + stats.append('Flush tables: {0}'.format(status['Flush_commands'])) + stats.append('Open tables: {0}'.format(status['Open_tables'])) + if 'Queries' in status: + queries_per_second = int(status['Queries']) / int(status['Uptime']) + stats.append('Queries per second avg: {:.3f}'.format( + queries_per_second)) + stats = ' '.join(stats) + footer.append('\n' + stats) footer.append('--------------') return [('\n'.join(title), output, '', '\n'.join(footer))] From bcb19d4a7ccc2a3f92e59e31f27faebf83451510 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Mon, 10 Jan 2022 12:59:11 -0800 Subject: [PATCH 191/202] Workaround pygments release breaking tabular output Related pygments issue: https://github.com/pygments/pygments/issues/2027 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d049bd58..d90bcd21 100755 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ install_requirements = [ 'click >= 7.0', 'cryptography >= 1.0.0', - 'Pygments >= 1.6', + 'Pygments>=1.6,<=2.11.1' 'prompt_toolkit>=3.0.6,<4.0.0', 'PyMySQL >= 0.9.2', 'sqlparse>=0.3.0,<0.5.0', From d86db8fbf2c290fc63a1279688ee6e8141e02e93 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Mon, 10 Jan 2022 13:02:05 -0800 Subject: [PATCH 192/202] Missing comma. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d90bcd21..abcf52be 100755 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ install_requirements = [ 'click >= 7.0', 'cryptography >= 1.0.0', - 'Pygments>=1.6,<=2.11.1' + 'Pygments>=1.6,<=2.11.1', 'prompt_toolkit>=3.0.6,<4.0.0', 'PyMySQL >= 0.9.2', 'sqlparse>=0.3.0,<0.5.0', From f0acf0a1f0188ea2e18ac4b07265848993dc2e00 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Tue, 11 Jan 2022 08:33:34 -0800 Subject: [PATCH 193/202] Update changelog for 1.24.2 --- changelog.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index 4097dfe5..ad870c03 100644 --- a/changelog.md +++ b/changelog.md @@ -1,10 +1,11 @@ -TBD: -==== +1.24.2 (2022/01/11) +=================== Bug Fixes: ---------- +---------- * Fix autocompletion for more than one JOIN * Fix the status command when connected to TiDB or other servers that don't implement 'Threads\_connected' +* Pin pygments version to avoid a breaking change 1.24.1: ======= From 832086bea3488b41118aa6e391f0d779b55be001 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Tue, 11 Jan 2022 08:34:01 -0800 Subject: [PATCH 194/202] Releasing version 1.24.2 --- mycli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/__init__.py b/mycli/__init__.py index 785c3b89..b27b4ede 100644 --- a/mycli/__init__.py +++ b/mycli/__init__.py @@ -1 +1 @@ -__version__ = '1.24.1' +__version__ = '1.24.2' From 025f4269234d82b8885bbf188111910c1f580be3 Mon Sep 17 00:00:00 2001 From: chenyijian Date: Mon, 17 Jan 2022 13:37:47 +0800 Subject: [PATCH 195/202] fix ipython magic command param position bug --- mycli/magic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/magic.py b/mycli/magic.py index b1a3268a..aad229a5 100644 --- a/mycli/magic.py +++ b/mycli/magic.py @@ -30,7 +30,7 @@ def mycli_line_magic(line): u = conn.session.engine.url _logger.debug('New mycli: %r', str(u)) - mycli.connect(u.database, u.host, u.username, u.port, u.password) + mycli.connect(host=u.host, port=u.port, passwd=u.password, database=u.database, user=u.username, init_command=None) conn._mycli = mycli # For convenience, print the connection alias From ba1e7a866b2157320614370b54e4c77c05e42684 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Mon, 17 Jan 2022 19:18:37 -0800 Subject: [PATCH 196/202] Upgrade cli_helpers to workaround the Pygments regression. --- changelog.md | 8 ++++++++ setup.py | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index ad870c03..cc2d9661 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,11 @@ +TBD +=== + +Bug Fixes: +---------- +* Upgrade cli_helpers to workaround Pygments regression. + + 1.24.2 (2022/01/11) =================== diff --git a/setup.py b/setup.py index abcf52be..f79bcd77 100755 --- a/setup.py +++ b/setup.py @@ -19,12 +19,13 @@ install_requirements = [ 'click >= 7.0', 'cryptography >= 1.0.0', - 'Pygments>=1.6,<=2.11.1', + # 'Pygments>=1.6,<=2.11.1', + 'Pygments>=1.6', 'prompt_toolkit>=3.0.6,<4.0.0', 'PyMySQL >= 0.9.2', 'sqlparse>=0.3.0,<0.5.0', 'configobj >= 5.0.5', - 'cli_helpers[styles] >= 2.0.1', + 'cli_helpers[styles] >= 2.2.1', 'pyperclip >= 1.8.1', 'pyaes >= 1.6.1' ] From 08e245154d1d3fbb71208e322e5706cf93374e9d Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Thu, 20 Jan 2022 17:28:43 -0800 Subject: [PATCH 197/202] Update changelog for 1.24.3 --- changelog.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index cc2d9661..340b283a 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,5 @@ -TBD -=== +1.24.3 (2022/01/20) +=================== Bug Fixes: ---------- From 634dc0ce28354e244eb11d8a9bf532f171a584b4 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Thu, 20 Jan 2022 17:29:09 -0800 Subject: [PATCH 198/202] Releasing version 1.24.3 --- mycli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/__init__.py b/mycli/__init__.py index b27b4ede..f7704b33 100644 --- a/mycli/__init__.py +++ b/mycli/__init__.py @@ -1 +1 @@ -__version__ = '1.24.2' +__version__ = '1.24.3' From 808d1ddc94351919e93867a79f7db46e8455c58f Mon Sep 17 00:00:00 2001 From: Arvind Mishra Date: Wed, 30 Mar 2022 04:06:18 +0530 Subject: [PATCH 199/202] Fix incompatibility with python-click >= 8.1.0 (#1042) Fixes for python-click>= 8.1.0 Change CI VM from 16.04 to 18.04 as Github no longer supports 16.04 Co-authored-by: arvindmishra --- .github/workflows/ci.yml | 2 +- changelog.md | 12 ++++++++++++ mycli/AUTHORS | 1 + mycli/main.py | 3 ++- test/test_main.py | 7 ++++--- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a144725..b678f571 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: python-version: [3.6, 3.7, 3.8, 3.9] include: - python-version: 3.6 - os: ubuntu-16.04 # MySQL 5.7.32 + os: ubuntu-18.04 # MySQL 5.7.32 - python-version: 3.7 os: ubuntu-18.04 # MySQL 5.7.32 - python-version: 3.8 diff --git a/changelog.md b/changelog.md index 340b283a..ded947dc 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,15 @@ +Internal: +--------- +* Upgrade Ubuntu VM for runners as Github has deprecated it + +1.24.4 (2022/03/30) +=================== + +Bug Fixes: +---------- +* Change in main.py - Replace the `click.get_terminal_size()` with `shutil.get_terminal_size()` + + 1.24.3 (2022/01/20) =================== diff --git a/mycli/AUTHORS b/mycli/AUTHORS index 308e9624..d1f3a280 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -87,6 +87,7 @@ Contributors: * Zhaolong Zhu * Zhidong * Zhongyang Guan + * Arvind Mishra Created by: ----------- diff --git a/mycli/main.py b/mycli/main.py index 3f08e9c3..c13ed780 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -2,6 +2,7 @@ from io import open import os import sys +import shutil import traceback import logging import threading @@ -1054,7 +1055,7 @@ def get_reserved_space(self): """Get the number of lines to reserve for the completion menu.""" reserved_space_ratio = .45 max_reserved_space = 8 - _, height = click.get_terminal_size() + _, height = shutil.get_terminal_size() return min(int(round(height * reserved_space_ratio)), max_reserved_space) def get_last_query(self): diff --git a/test/test_main.py b/test/test_main.py index 00fdc1bd..7731603e 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -1,4 +1,5 @@ import os +import shutil import click from click.testing import CliRunner @@ -258,13 +259,13 @@ def test_reserved_space_is_integer(): def stub_terminal_size(): return (5, 5) - old_func = click.get_terminal_size + old_func = shutil.get_terminal_size - click.get_terminal_size = stub_terminal_size + shutil.get_terminal_size = stub_terminal_size mycli = MyCli() assert isinstance(mycli.get_reserved_space(), int) - click.get_terminal_size = old_func + shutil.get_terminal_size = old_func def test_list_dsn(): From 1aa7c11d010fcf24ece78d42e27b70ad536c75f3 Mon Sep 17 00:00:00 2001 From: Irina Truong Date: Tue, 29 Mar 2022 15:57:09 -0700 Subject: [PATCH 200/202] Changelog update before releasing 1.24.4. (#1043) --- changelog.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index ded947dc..b5522d2e 100644 --- a/changelog.md +++ b/changelog.md @@ -1,10 +1,10 @@ +1.24.4 (2022/03/30) +=================== + Internal: --------- * Upgrade Ubuntu VM for runners as Github has deprecated it -1.24.4 (2022/03/30) -=================== - Bug Fixes: ---------- * Change in main.py - Replace the `click.get_terminal_size()` with `shutil.get_terminal_size()` From 0471faa5b24c39ba6e688e0124d11e5810f5c332 Mon Sep 17 00:00:00 2001 From: Irina Truong Date: Tue, 29 Mar 2022 16:01:55 -0700 Subject: [PATCH 201/202] Releasing version 1.24.4 --- mycli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/__init__.py b/mycli/__init__.py index f7704b33..e10d6ee2 100644 --- a/mycli/__init__.py +++ b/mycli/__init__.py @@ -1 +1 @@ -__version__ = '1.24.3' +__version__ = '1.24.4' From b42d15419b9cbcf11b04a1524a2d1320333f20a4 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Tue, 29 Mar 2022 17:27:34 -0700 Subject: [PATCH 202/202] Change the branch name in release script. --- release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.py b/release.py index 3f18f03f..39df8a3a 100755 --- a/release.py +++ b/release.py @@ -72,7 +72,7 @@ def upload_distribution_files(): def push_to_github(): - run_step('git', 'push', 'origin', 'master') + run_step('git', 'push', 'origin', 'main') def push_tags_to_github():