1
0
Fork 0

Merging upstream version 1.27.2.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-09 19:13:22 +01:00
parent d3f72a1e51
commit cc1aa7d50e
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
15 changed files with 248 additions and 118 deletions

View file

@ -10,28 +10,31 @@ jobs:
strategy:
matrix:
python-version: [
'3.7',
'3.8',
'3.9',
'3.10',
'3.11',
'3.12',
]
include:
- python-version: '3.7'
os: ubuntu-18.04 # MySQL 5.7.32
- python-version: '3.8'
os: ubuntu-18.04 # MySQL 5.7.32
os: ubuntu-20.04 # MySQL 8.0.36
- python-version: '3.9'
os: ubuntu-20.04 # MySQL 8.0.22
os: ubuntu-20.04 # MySQL 8.0.36
- python-version: '3.10'
os: ubuntu-22.04 # MySQL 8.0.28
os: ubuntu-22.04 # MySQL 8.0.36
- python-version: '3.11'
os: ubuntu-22.04 # MySQL 8.0.36
- python-version: '3.12'
os: ubuntu-22.04 # MySQL 8.0.36
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

View file

@ -1,41 +0,0 @@
name: "CodeQL"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: "12 18 * * 1"
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ python ]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
queries: +security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{ matrix.language }}"

3
.gitignore vendored
View file

@ -12,3 +12,6 @@
.cache/
.coverage
.coverage.*
.venv/
venv/

View file

@ -1,8 +1,6 @@
# 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)](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.
@ -76,6 +74,9 @@ $ sudo apt-get install mycli # Only on debian or ubuntu
--ssl-cert PATH X509 cert in PEM format.
--ssl-key PATH X509 key in PEM format.
--ssl-cipher TEXT SSL cipher to use.
--tls-version [TLSv1|TLSv1.1|TLSv1.2|TLSv1.3]
TLS protocol version for secure connection.
--ssl-verify-server-cert Verify server's "Common Name" in its cert
against hostname used when connecting. This
option is disabled by default.
@ -178,29 +179,10 @@ Fedora has a package available for mycli, install it using dnf:
$ sudo dnf install mycli
```
### RHEL, Centos
I haven't built an RPM package for mycli for RHEL or Centos yet. So please use `pip` to install `mycli`. You can install pip on your system using:
```
$ sudo yum install python3-pip
```
Once that is installed, you can install mycli as follows:
```
$ sudo pip3 install mycli
```
### Windows
Follow the instructions on this blogpost: https://www.codewall.co.uk/installing-using-mycli-on-windows/
### Cygwin
1. Make sure the following Cygwin packages are installed:
`python3`, `python3-pip`.
2. Install mycli: `pip3 install mycli`
### Thanks:

View file

@ -1,3 +1,46 @@
Upcoming Release (TBD)
======================
Bug Fixes:
----------
Internal:
---------
Features:
---------
1.27.2 (2024/04/03)
===================
Bug Fixes:
----------
* Don't use default prompt when one is not supplied to the --prompt option.
1.27.1 (2024/03/28)
===================
Bug Fixes:
----------
* Don't install tests.
* Do not ignore the socket passed with the -S option, even when no port is passed
* Fix unexpected exception when using dsn without username & password (Thanks: [Will Wang])
* Let the `--prompt` option act normally with its predefined default value
Internal:
---------
* paramiko is newer than 2.11.0 now, remove version pinning `cryptography`.
* Drop support for Python 3.7
1.27.0 (2023/08/11)
===================
@ -14,6 +57,7 @@ Bug Fixes:
* Remove vi-mode bindings for prettify/unprettify.
* Honor `\G` when executing from commandline with `-e`.
* Correctly report the version of TiDB.
* Revised `botton` spelling mistakes with `bottom` in `mycli/clitoolbar.py`
1.26.1 (2022/09/01)
@ -35,6 +79,10 @@ Features:
* Add prettify/unprettify keybindings to format the current statement using `sqlglot`.
Features:
---------
* Add `--tls-version` option to control the tls version used.
Internal:
---------
* Pin `cryptography` to suppress `paramiko` warning, helping CI complete and presumably affecting some users.
@ -950,3 +998,4 @@ Bug Fixes:
[William GARCIA]: https://github.com/willgarcia
[xeron]: https://github.com/xeron
[Zach DeCook]: https://zachdecook.com
[Will Wang]: https://github.com/willww64

View file

@ -1,11 +1,7 @@
Project Lead:
-------------
* Thomas Roten
Core Developers:
----------------
* Thomas Roten
* Irina Truong
* Matheus Rosa
* Darik Gamble
@ -35,6 +31,7 @@ Contributors:
* Daniel Black
* Daniel West
* Daniël van Eeden
* Fabrizio Gennari
* François Pietka
* Frederic Aoustin
* Georgy Frolov
@ -94,6 +91,12 @@ Contributors:
* Arvind Mishra
* Kevin Schmeichel
* Mel Dafert
* Thomas Copper
* Will Wang
* Alfred Wingate
* Zhanze Wang
* Houston Wong
Created by:
-----------

View file

@ -1 +1 @@
__version__ = '1.27.0'
__version__ = '1.27.2'

View file

@ -7,8 +7,7 @@ from .packages import special
def create_toolbar_tokens_func(mycli, show_fish_help):
"""Return a function that generates the toolbar tokens."""
def get_toolbar_tokens():
result = []
result.append(('class:bottom-toolbar', ' '))
result = [('class:bottom-toolbar', ' ')]
if mycli.multi_line:
delimiter = special.get_current_delimiter()
@ -26,7 +25,7 @@ def create_toolbar_tokens_func(mycli, show_fish_help):
'[F3] Multiline: OFF '))
if mycli.prompt_app.editing_mode == EditingMode.VI:
result.append((
'class:botton-toolbar.on',
'class:bottom-toolbar.on',
'Vi-mode ({})'.format(_get_vi_mode())
))

View file

@ -93,6 +93,7 @@ SUPPORT_INFO = (
class MyCli(object):
default_prompt = '\\t \\u@\\h:\\d> '
default_prompt_splitln = '\\u@\\h\\n(\\t):\\d>'
max_len_prompt = 45
defaults_suffix = None
@ -427,6 +428,7 @@ class MyCli(object):
port = 3306
if not host or host == 'localhost':
socket = (
socket or
cnf['socket'] or
cnf['default_socket'] or
guess_socket_location()
@ -643,7 +645,7 @@ class MyCli(object):
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> ')
prompt = self.get_prompt(self.default_prompt_splitln)
prompt = prompt.replace("\\x1b", "\x1b")
return ANSI(prompt)
@ -1135,6 +1137,9 @@ class MyCli(object):
@click.option('--ssl-key', help='X509 key in PEM format.',
type=click.Path(exists=True))
@click.option('--ssl-cipher', help='SSL cipher to use.')
@click.option('--tls-version',
type=click.Choice(['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3'], case_sensitive=False),
help='TLS protocol version for secure connection.')
@click.option('--ssl-verify-server-cert', is_flag=True,
help=('Verify server\'s "Common Name" in its cert against '
'hostname used when connecting. This option is disabled '
@ -1186,8 +1191,8 @@ def cli(database, user, host, port, socket, password, dbname,
version, verbose, prompt, logfile, defaults_group_suffix,
defaults_file, login_path, auto_vertical_output, local_infile,
ssl_enable, 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,
tls_version, 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, password_file):
"""A MySQL terminal client with auto-completion and syntax highlighting.
@ -1246,6 +1251,7 @@ def cli(database, user, host, port, socket, password, dbname,
'key': ssl_key and os.path.expanduser(ssl_key),
'capath': ssl_capath,
'cipher': ssl_cipher,
'tls_version': tls_version,
'check_hostname': ssl_verify_server_cert,
}
@ -1278,7 +1284,7 @@ def cli(database, user, host, port, socket, password, dbname,
uri = urlparse(dsn_uri)
if not database:
database = uri.path[1:] # ignore the leading fwd slash
if not user:
if not user and uri.username is not None:
user = unquote(uri.username)
if not password and uri.password is not None:
password = unquote(uri.password)

View file

@ -176,11 +176,15 @@ class SQLExecute(object):
if init_command and len(list(special.split_queries(init_command))) > 1:
client_flag |= pymysql.constants.CLIENT.MULTI_STATEMENTS
ssl_context = None
if ssl:
ssl_context = self._create_ssl_ctx(ssl)
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=client_flag,
local_infile=local_infile, conv=conv, ssl=ssl, program_name="mycli",
local_infile=local_infile, conv=conv, ssl=ssl_context, program_name="mycli",
defer_connect=defer_connect, init_command=init_command
)
@ -354,3 +358,40 @@ class SQLExecute(object):
def change_db(self, db):
self.conn.select_db(db)
self.dbname = db
def _create_ssl_ctx(self, sslp):
import ssl
ca = sslp.get("ca")
capath = sslp.get("capath")
hasnoca = ca is None and capath is None
ctx = ssl.create_default_context(cafile=ca, capath=capath)
ctx.check_hostname = not hasnoca and sslp.get("check_hostname", True)
ctx.verify_mode = ssl.CERT_NONE if hasnoca else ssl.CERT_REQUIRED
if "cert" in sslp:
ctx.load_cert_chain(sslp["cert"], keyfile=sslp.get("key"))
if "cipher" in sslp:
ctx.set_ciphers(sslp["cipher"])
# raise this default to v1.1 or v1.2?
ctx.minimum_version = ssl.TLSVersion.TLSv1
if "tls_version" in sslp:
tls_version = sslp["tls_version"]
if tls_version == "TLSv1":
ctx.minimum_version = ssl.TLSVersion.TLSv1
ctx.maximum_version = ssl.TLSVersion.TLSv1
elif tls_version == "TLSv1.1":
ctx.minimum_version = ssl.TLSVersion.TLSv1_1
ctx.maximum_version = ssl.TLSVersion.TLSv1_1
elif tls_version == "TLSv1.2":
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
ctx.maximum_version = ssl.TLSVersion.TLSv1_2
elif tls_version == "TLSv1.3":
ctx.minimum_version = ssl.TLSVersion.TLSv1_3
ctx.maximum_version = ssl.TLSVersion.TLSv1_3
else:
_logger.error('Invalid tls version: %s', tls_version)
return ctx

View file

@ -14,3 +14,4 @@ pyperclip>=1.8.1
importlib_resources>=5.0.0
pyaes>=1.6.1
sqlglot>=5.1.3
setuptools

View file

@ -18,9 +18,8 @@ description = 'CLI for MySQL Database. With auto-completion and syntax highlight
install_requirements = [
'click >= 7.0',
# Temporary to suppress paramiko Blowfish warning which breaks CI.
# Pinning cryptography should not be needed after paramiko 2.11.0.
'cryptography == 36.0.2',
# Pinning cryptography is not needed after paramiko 2.11.0. Correct it
'cryptography >= 1.0.0',
# 'Pygments>=1.6,<=2.11.1',
'Pygments>=1.6',
'prompt_toolkit>=3.0.6,<4.0.0',
@ -95,7 +94,7 @@ setup(
author_email='mycli-dev@googlegroups.com',
version=version,
url='http://mycli.net',
packages=find_packages(),
packages=find_packages(exclude=['test*']),
package_data={'mycli': ['myclirc', 'AUTHORS', 'SPONSORS']},
description=description,
long_description=description,

View file

@ -254,23 +254,21 @@ def test_conditional_pager(monkeypatch):
SPECIAL_COMMANDS['pager'].handler('')
def test_reserved_space_is_integer():
def test_reserved_space_is_integer(monkeypatch):
"""Make sure that reserved space is returned as an integer."""
def stub_terminal_size():
return (5, 5)
old_func = shutil.get_terminal_size
shutil.get_terminal_size = stub_terminal_size
with monkeypatch.context() as m:
m.setattr(shutil, 'get_terminal_size', stub_terminal_size)
mycli = MyCli()
assert isinstance(mycli.get_reserved_space(), int)
shutil.get_terminal_size = old_func
def test_list_dsn():
runner = CliRunner()
with NamedTemporaryFile(mode="w") as myclirc:
# keep Windows from locking the file with delete=False
with NamedTemporaryFile(mode="w",delete=False) as myclirc:
myclirc.write(dedent("""\
[alias_dsn]
test = mysql://test/test
@ -282,6 +280,15 @@ def test_list_dsn():
result = runner.invoke(cli, args=args + ['--verbose'])
assert result.output == "test : mysql://test/test\n"
# delete=False means we should try to clean up
try:
if os.path.exists(myclirc.name):
os.remove(myclirc.name)
except Exception as e:
print(f"An error occurred while attempting to delete the file: {e}")
def test_prettify_statement():
statement = 'SELECT 1'
@ -299,7 +306,8 @@ def test_unprettify_statement():
def test_list_ssh_config():
runner = CliRunner()
with NamedTemporaryFile(mode="w") as ssh_config:
# keep Windows from locking the file with delete=False
with NamedTemporaryFile(mode="w",delete=False) as ssh_config:
ssh_config.write(dedent("""\
Host test
Hostname test.example.com
@ -314,6 +322,13 @@ def test_list_ssh_config():
result = runner.invoke(cli, args=args + ['--verbose'])
assert "test : test.example.com\n" in result.output
# delete=False means we should try to clean up
try:
if os.path.exists(ssh_config.name):
os.remove(ssh_config.name)
except Exception as e:
print(f"An error occurred while attempting to delete the file: {e}")
def test_dsn(monkeypatch):
# Setup classes to mock mycli.main.MyCli
@ -466,7 +481,8 @@ def test_ssh_config(monkeypatch):
runner = CliRunner()
# Setup temporary configuration
with NamedTemporaryFile(mode="w") as ssh_config:
# keep Windows from locking the file with delete=False
with NamedTemporaryFile(mode="w",delete=False) as ssh_config:
ssh_config.write(dedent("""\
Host test
Hostname test.example.com
@ -489,8 +505,8 @@ def test_ssh_config(monkeypatch):
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.path.expanduser(
"~") + "/.ssh/gateway"
# When a user supplies a ssh config host as argument to mycli,
# and used command line arguments, use the command line
@ -513,6 +529,13 @@ def test_ssh_config(monkeypatch):
MockMyCli.connect_args["ssh_port"] == 3 and \
MockMyCli.connect_args["ssh_key_filename"] == "/path/to/key"
# delete=False means we should try to clean up
try:
if os.path.exists(ssh_config.name):
os.remove(ssh_config.name)
except Exception as e:
print(f"An error occurred while attempting to delete the file: {e}")
@dbtest
def test_init_command_arg(executor):

View file

@ -50,26 +50,50 @@ def test_editor_command():
os.environ['EDITOR'] = 'true'
os.environ['VISUAL'] = 'true'
# Set the editor to Notepad on Windows
if os.name != 'nt':
mycli.packages.special.open_external_editor(sql=r'select 1') == "select 1"
else:
pytest.skip('Skipping on Windows platform.')
def test_tee_command():
mycli.packages.special.write_tee(u"hello world") # write without file set
with tempfile.NamedTemporaryFile() as f:
# keep Windows from locking the file with delete=False
with tempfile.NamedTemporaryFile(delete=False) as f:
mycli.packages.special.execute(None, u"tee " + f.name)
mycli.packages.special.write_tee(u"hello world")
if os.name=='nt':
assert f.read() == b"hello world\r\n"
else:
assert f.read() == b"hello world\n"
mycli.packages.special.execute(None, u"tee -o " + f.name)
mycli.packages.special.write_tee(u"hello world")
f.seek(0)
if os.name=='nt':
assert f.read() == b"hello world\r\n"
else:
assert f.read() == b"hello world\n"
mycli.packages.special.execute(None, u"notee")
mycli.packages.special.write_tee(u"hello world")
f.seek(0)
if os.name=='nt':
assert f.read() == b"hello world\r\n"
else:
assert f.read() == b"hello world\n"
# remove temp file
# delete=False means we should try to clean up
try:
if os.path.exists(f.name):
os.remove(f.name)
except Exception as e:
print(f"An error occurred while attempting to delete the file: {e}")
def test_tee_command_error():
with pytest.raises(TypeError):
@ -82,6 +106,8 @@ def test_tee_command_error():
@dbtest
@pytest.mark.skipif(os.name == "nt", reason="Bug: fails on Windows, needs fixing, singleton of FQ not working right")
def test_favorite_query():
with db_connection().cursor() as cur:
query = u'select ""'
@ -98,16 +124,29 @@ def test_once_command():
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:
# keep Windows from locking the file with delete=False
with tempfile.NamedTemporaryFile(delete=False) as f:
mycli.packages.special.execute(None, u"\\once " + f.name)
mycli.packages.special.write_once(u"hello world")
if os.name=='nt':
assert f.read() == b"hello world\r\n"
else:
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 line 1")
mycli.packages.special.write_once(u"hello world line 2")
f.seek(0)
if os.name=='nt':
assert f.read() == b"hello world line 1\r\nhello world line 2\r\n"
else:
assert f.read() == b"hello world line 1\nhello world line 2\n"
# delete=False means we should try to clean up
try:
if os.path.exists(f.name):
os.remove(f.name)
except Exception as e:
print(f"An error occurred while attempting to delete the file: {e}")
def test_pipe_once_command():
@ -118,6 +157,11 @@ def test_pipe_once_command():
mycli.packages.special.execute(
None, u"\\pipe_once /proc/access-denied")
if os.name == 'nt':
mycli.packages.special.execute(None, u'\\pipe_once python -c "import sys; print(len(sys.stdin.read().strip()))"')
mycli.packages.special.write_once(u"hello world")
mycli.packages.special.unset_pipe_once_if_written()
else:
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()
@ -128,11 +172,20 @@ def test_parseargfile():
"""Test that parseargfile expands the user directory."""
expected = {'file': os.path.join(os.path.expanduser('~'), 'filename'),
'mode': 'a'}
if os.name=='nt':
assert expected == mycli.packages.special.iocommands.parseargfile(
'~\\filename')
else:
assert expected == mycli.packages.special.iocommands.parseargfile(
'~/filename')
expected = {'file': os.path.join(os.path.expanduser('~'), 'filename'),
'mode': 'w'}
if os.name=='nt':
assert expected == mycli.packages.special.iocommands.parseargfile(
'-o ~\\filename')
else:
assert expected == mycli.packages.special.iocommands.parseargfile(
'-o ~/filename')
@ -162,6 +215,7 @@ def test_watch_query_iteration():
@dbtest
@pytest.mark.skipif(os.name == "nt", reason="Bug: Win handles this differently. May need to refactor watch_query to work for Win")
def test_watch_query_full():
"""Test that `watch_query`:

View file

@ -117,6 +117,7 @@ def test_multiple_queries_same_line_syntaxerror(executor):
@dbtest
@pytest.mark.skipif(os.name == "nt", reason="Bug: fails on Windows, needs fixing, singleton of FQ not working right")
def test_favorite_query(executor):
set_expanded_output(False)
run(executor, "create table test(a text)")
@ -136,6 +137,7 @@ def test_favorite_query(executor):
@dbtest
@pytest.mark.skipif(os.name == "nt", reason="Bug: fails on Windows, needs fixing, singleton of FQ not working right")
def test_favorite_query_multiple_statement(executor):
set_expanded_output(False)
run(executor, "create table test(a text)")
@ -159,6 +161,7 @@ def test_favorite_query_multiple_statement(executor):
@dbtest
@pytest.mark.skipif(os.name == "nt", reason="Bug: fails on Windows, needs fixing, singleton of FQ not working right")
def test_favorite_query_expanded_output(executor):
set_expanded_output(False)
run(executor, '''create table test(a text)''')
@ -195,16 +198,21 @@ def test_cd_command_without_a_folder_name(executor):
@dbtest
def test_system_command_not_found(executor):
results = run(executor, 'system xyz')
if os.name=='nt':
assert_result_equal(results, status='OSError: The system cannot find the file specified',
assert_contains=True)
else:
assert_result_equal(results, status='OSError: No such file or directory',
assert_contains=True)
@dbtest
def test_system_command_output(executor):
eol = os.linesep
test_dir = os.path.abspath(os.path.dirname(__file__))
test_file_path = os.path.join(test_dir, 'test.txt')
results = run(executor, 'system cat {0}'.format(test_file_path))
assert_result_equal(results, status='mycli rocks!\n')
assert_result_equal(results, status=f'mycli rocks!{eol}')
@dbtest