Merging upstream version 1.27.2.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
d3f72a1e51
commit
cc1aa7d50e
15 changed files with 248 additions and 118 deletions
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
|
@ -10,28 +10,31 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [
|
python-version: [
|
||||||
'3.7',
|
|
||||||
'3.8',
|
'3.8',
|
||||||
'3.9',
|
'3.9',
|
||||||
'3.10',
|
'3.10',
|
||||||
|
'3.11',
|
||||||
|
'3.12',
|
||||||
]
|
]
|
||||||
include:
|
include:
|
||||||
- python-version: '3.7'
|
|
||||||
os: ubuntu-18.04 # MySQL 5.7.32
|
|
||||||
- python-version: '3.8'
|
- python-version: '3.8'
|
||||||
os: ubuntu-18.04 # MySQL 5.7.32
|
os: ubuntu-20.04 # MySQL 8.0.36
|
||||||
- python-version: '3.9'
|
- python-version: '3.9'
|
||||||
os: ubuntu-20.04 # MySQL 8.0.22
|
os: ubuntu-20.04 # MySQL 8.0.36
|
||||||
- python-version: '3.10'
|
- 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 }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
|
41
.github/workflows/codeql.yml
vendored
41
.github/workflows/codeql.yml
vendored
|
@ -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
3
.gitignore
vendored
|
@ -12,3 +12,6 @@
|
||||||
.cache/
|
.cache/
|
||||||
.coverage
|
.coverage
|
||||||
.coverage.*
|
.coverage.*
|
||||||
|
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
|
24
README.md
24
README.md
|
@ -1,8 +1,6 @@
|
||||||
# mycli
|
# mycli
|
||||||
|
|
||||||
[![Build Status](https://github.com/dbcli/mycli/workflows/mycli/badge.svg)](https://github.com/dbcli/mycli/actions?query=workflow%3Amycli)
|
[![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.
|
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-cert PATH X509 cert in PEM format.
|
||||||
--ssl-key PATH X509 key in PEM format.
|
--ssl-key PATH X509 key in PEM format.
|
||||||
--ssl-cipher TEXT SSL cipher to use.
|
--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
|
--ssl-verify-server-cert Verify server's "Common Name" in its cert
|
||||||
against hostname used when connecting. This
|
against hostname used when connecting. This
|
||||||
option is disabled by default.
|
option is disabled by default.
|
||||||
|
@ -178,29 +179,10 @@ Fedora has a package available for mycli, install it using dnf:
|
||||||
$ sudo dnf install mycli
|
$ 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
|
### Windows
|
||||||
|
|
||||||
Follow the instructions on this blogpost: https://www.codewall.co.uk/installing-using-mycli-on-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:
|
### Thanks:
|
||||||
|
|
||||||
|
|
49
changelog.md
49
changelog.md
|
@ -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)
|
1.27.0 (2023/08/11)
|
||||||
===================
|
===================
|
||||||
|
|
||||||
|
@ -14,6 +57,7 @@ Bug Fixes:
|
||||||
* Remove vi-mode bindings for prettify/unprettify.
|
* Remove vi-mode bindings for prettify/unprettify.
|
||||||
* Honor `\G` when executing from commandline with `-e`.
|
* Honor `\G` when executing from commandline with `-e`.
|
||||||
* Correctly report the version of TiDB.
|
* Correctly report the version of TiDB.
|
||||||
|
* Revised `botton` spelling mistakes with `bottom` in `mycli/clitoolbar.py`
|
||||||
|
|
||||||
|
|
||||||
1.26.1 (2022/09/01)
|
1.26.1 (2022/09/01)
|
||||||
|
@ -35,6 +79,10 @@ Features:
|
||||||
* Add prettify/unprettify keybindings to format the current statement using `sqlglot`.
|
* Add prettify/unprettify keybindings to format the current statement using `sqlglot`.
|
||||||
|
|
||||||
|
|
||||||
|
Features:
|
||||||
|
---------
|
||||||
|
* Add `--tls-version` option to control the tls version used.
|
||||||
|
|
||||||
Internal:
|
Internal:
|
||||||
---------
|
---------
|
||||||
* Pin `cryptography` to suppress `paramiko` warning, helping CI complete and presumably affecting some users.
|
* 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
|
[William GARCIA]: https://github.com/willgarcia
|
||||||
[xeron]: https://github.com/xeron
|
[xeron]: https://github.com/xeron
|
||||||
[Zach DeCook]: https://zachdecook.com
|
[Zach DeCook]: https://zachdecook.com
|
||||||
|
[Will Wang]: https://github.com/willww64
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
Project Lead:
|
|
||||||
-------------
|
|
||||||
* Thomas Roten
|
|
||||||
|
|
||||||
|
|
||||||
Core Developers:
|
Core Developers:
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
* Thomas Roten
|
||||||
* Irina Truong
|
* Irina Truong
|
||||||
* Matheus Rosa
|
* Matheus Rosa
|
||||||
* Darik Gamble
|
* Darik Gamble
|
||||||
|
@ -35,6 +31,7 @@ Contributors:
|
||||||
* Daniel Black
|
* Daniel Black
|
||||||
* Daniel West
|
* Daniel West
|
||||||
* Daniël van Eeden
|
* Daniël van Eeden
|
||||||
|
* Fabrizio Gennari
|
||||||
* François Pietka
|
* François Pietka
|
||||||
* Frederic Aoustin
|
* Frederic Aoustin
|
||||||
* Georgy Frolov
|
* Georgy Frolov
|
||||||
|
@ -94,6 +91,12 @@ Contributors:
|
||||||
* Arvind Mishra
|
* Arvind Mishra
|
||||||
* Kevin Schmeichel
|
* Kevin Schmeichel
|
||||||
* Mel Dafert
|
* Mel Dafert
|
||||||
|
* Thomas Copper
|
||||||
|
* Will Wang
|
||||||
|
* Alfred Wingate
|
||||||
|
* Zhanze Wang
|
||||||
|
* Houston Wong
|
||||||
|
|
||||||
|
|
||||||
Created by:
|
Created by:
|
||||||
-----------
|
-----------
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
__version__ = '1.27.0'
|
__version__ = '1.27.2'
|
||||||
|
|
|
@ -7,8 +7,7 @@ from .packages import special
|
||||||
def create_toolbar_tokens_func(mycli, show_fish_help):
|
def create_toolbar_tokens_func(mycli, show_fish_help):
|
||||||
"""Return a function that generates the toolbar tokens."""
|
"""Return a function that generates the toolbar tokens."""
|
||||||
def get_toolbar_tokens():
|
def get_toolbar_tokens():
|
||||||
result = []
|
result = [('class:bottom-toolbar', ' ')]
|
||||||
result.append(('class:bottom-toolbar', ' '))
|
|
||||||
|
|
||||||
if mycli.multi_line:
|
if mycli.multi_line:
|
||||||
delimiter = special.get_current_delimiter()
|
delimiter = special.get_current_delimiter()
|
||||||
|
@ -26,7 +25,7 @@ def create_toolbar_tokens_func(mycli, show_fish_help):
|
||||||
'[F3] Multiline: OFF '))
|
'[F3] Multiline: OFF '))
|
||||||
if mycli.prompt_app.editing_mode == EditingMode.VI:
|
if mycli.prompt_app.editing_mode == EditingMode.VI:
|
||||||
result.append((
|
result.append((
|
||||||
'class:botton-toolbar.on',
|
'class:bottom-toolbar.on',
|
||||||
'Vi-mode ({})'.format(_get_vi_mode())
|
'Vi-mode ({})'.format(_get_vi_mode())
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
|
@ -93,6 +93,7 @@ SUPPORT_INFO = (
|
||||||
class MyCli(object):
|
class MyCli(object):
|
||||||
|
|
||||||
default_prompt = '\\t \\u@\\h:\\d> '
|
default_prompt = '\\t \\u@\\h:\\d> '
|
||||||
|
default_prompt_splitln = '\\u@\\h\\n(\\t):\\d>'
|
||||||
max_len_prompt = 45
|
max_len_prompt = 45
|
||||||
defaults_suffix = None
|
defaults_suffix = None
|
||||||
|
|
||||||
|
@ -427,6 +428,7 @@ class MyCli(object):
|
||||||
port = 3306
|
port = 3306
|
||||||
if not host or host == 'localhost':
|
if not host or host == 'localhost':
|
||||||
socket = (
|
socket = (
|
||||||
|
socket or
|
||||||
cnf['socket'] or
|
cnf['socket'] or
|
||||||
cnf['default_socket'] or
|
cnf['default_socket'] or
|
||||||
guess_socket_location()
|
guess_socket_location()
|
||||||
|
@ -643,7 +645,7 @@ class MyCli(object):
|
||||||
def get_message():
|
def get_message():
|
||||||
prompt = self.get_prompt(self.prompt_format)
|
prompt = self.get_prompt(self.prompt_format)
|
||||||
if self.prompt_format == self.default_prompt and len(prompt) > self.max_len_prompt:
|
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")
|
prompt = prompt.replace("\\x1b", "\x1b")
|
||||||
return ANSI(prompt)
|
return ANSI(prompt)
|
||||||
|
|
||||||
|
@ -1135,6 +1137,9 @@ class MyCli(object):
|
||||||
@click.option('--ssl-key', help='X509 key in PEM format.',
|
@click.option('--ssl-key', help='X509 key in PEM format.',
|
||||||
type=click.Path(exists=True))
|
type=click.Path(exists=True))
|
||||||
@click.option('--ssl-cipher', help='SSL cipher to use.')
|
@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,
|
@click.option('--ssl-verify-server-cert', is_flag=True,
|
||||||
help=('Verify server\'s "Common Name" in its cert against '
|
help=('Verify server\'s "Common Name" in its cert against '
|
||||||
'hostname used when connecting. This option is disabled '
|
'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,
|
version, verbose, prompt, logfile, defaults_group_suffix,
|
||||||
defaults_file, login_path, auto_vertical_output, local_infile,
|
defaults_file, login_path, auto_vertical_output, local_infile,
|
||||||
ssl_enable, ssl_ca, ssl_capath, ssl_cert, ssl_key, ssl_cipher,
|
ssl_enable, ssl_ca, ssl_capath, ssl_cert, ssl_key, ssl_cipher,
|
||||||
ssl_verify_server_cert, table, csv, warn, execute, myclirc, dsn,
|
tls_version, ssl_verify_server_cert, table, csv, warn, execute,
|
||||||
list_dsn, ssh_user, ssh_host, ssh_port, ssh_password,
|
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, charset, password_file):
|
init_command, charset, password_file):
|
||||||
"""A MySQL terminal client with auto-completion and syntax highlighting.
|
"""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),
|
'key': ssl_key and os.path.expanduser(ssl_key),
|
||||||
'capath': ssl_capath,
|
'capath': ssl_capath,
|
||||||
'cipher': ssl_cipher,
|
'cipher': ssl_cipher,
|
||||||
|
'tls_version': tls_version,
|
||||||
'check_hostname': ssl_verify_server_cert,
|
'check_hostname': ssl_verify_server_cert,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1278,7 +1284,7 @@ def cli(database, user, host, port, socket, password, dbname,
|
||||||
uri = urlparse(dsn_uri)
|
uri = urlparse(dsn_uri)
|
||||||
if not database:
|
if not database:
|
||||||
database = uri.path[1:] # ignore the leading fwd slash
|
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)
|
user = unquote(uri.username)
|
||||||
if not password and uri.password is not None:
|
if not password and uri.password is not None:
|
||||||
password = unquote(uri.password)
|
password = unquote(uri.password)
|
||||||
|
|
|
@ -176,11 +176,15 @@ class SQLExecute(object):
|
||||||
if init_command and len(list(special.split_queries(init_command))) > 1:
|
if init_command and len(list(special.split_queries(init_command))) > 1:
|
||||||
client_flag |= pymysql.constants.CLIENT.MULTI_STATEMENTS
|
client_flag |= pymysql.constants.CLIENT.MULTI_STATEMENTS
|
||||||
|
|
||||||
|
ssl_context = None
|
||||||
|
if ssl:
|
||||||
|
ssl_context = self._create_ssl_ctx(ssl)
|
||||||
|
|
||||||
conn = pymysql.connect(
|
conn = pymysql.connect(
|
||||||
database=db, user=user, password=password, host=host, port=port,
|
database=db, user=user, password=password, host=host, port=port,
|
||||||
unix_socket=socket, use_unicode=True, charset=charset,
|
unix_socket=socket, use_unicode=True, charset=charset,
|
||||||
autocommit=True, client_flag=client_flag,
|
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
|
defer_connect=defer_connect, init_command=init_command
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -354,3 +358,40 @@ class SQLExecute(object):
|
||||||
def change_db(self, db):
|
def change_db(self, db):
|
||||||
self.conn.select_db(db)
|
self.conn.select_db(db)
|
||||||
self.dbname = 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
|
||||||
|
|
|
@ -14,3 +14,4 @@ pyperclip>=1.8.1
|
||||||
importlib_resources>=5.0.0
|
importlib_resources>=5.0.0
|
||||||
pyaes>=1.6.1
|
pyaes>=1.6.1
|
||||||
sqlglot>=5.1.3
|
sqlglot>=5.1.3
|
||||||
|
setuptools
|
||||||
|
|
7
setup.py
7
setup.py
|
@ -18,9 +18,8 @@ description = 'CLI for MySQL Database. With auto-completion and syntax highlight
|
||||||
|
|
||||||
install_requirements = [
|
install_requirements = [
|
||||||
'click >= 7.0',
|
'click >= 7.0',
|
||||||
# Temporary to suppress paramiko Blowfish warning which breaks CI.
|
# Pinning cryptography is not needed after paramiko 2.11.0. Correct it
|
||||||
# Pinning cryptography should not be needed after paramiko 2.11.0.
|
'cryptography >= 1.0.0',
|
||||||
'cryptography == 36.0.2',
|
|
||||||
# 'Pygments>=1.6,<=2.11.1',
|
# 'Pygments>=1.6,<=2.11.1',
|
||||||
'Pygments>=1.6',
|
'Pygments>=1.6',
|
||||||
'prompt_toolkit>=3.0.6,<4.0.0',
|
'prompt_toolkit>=3.0.6,<4.0.0',
|
||||||
|
@ -95,7 +94,7 @@ setup(
|
||||||
author_email='mycli-dev@googlegroups.com',
|
author_email='mycli-dev@googlegroups.com',
|
||||||
version=version,
|
version=version,
|
||||||
url='http://mycli.net',
|
url='http://mycli.net',
|
||||||
packages=find_packages(),
|
packages=find_packages(exclude=['test*']),
|
||||||
package_data={'mycli': ['myclirc', 'AUTHORS', 'SPONSORS']},
|
package_data={'mycli': ['myclirc', 'AUTHORS', 'SPONSORS']},
|
||||||
description=description,
|
description=description,
|
||||||
long_description=description,
|
long_description=description,
|
||||||
|
|
|
@ -254,23 +254,21 @@ def test_conditional_pager(monkeypatch):
|
||||||
SPECIAL_COMMANDS['pager'].handler('')
|
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."""
|
"""Make sure that reserved space is returned as an integer."""
|
||||||
def stub_terminal_size():
|
def stub_terminal_size():
|
||||||
return (5, 5)
|
return (5, 5)
|
||||||
|
|
||||||
old_func = shutil.get_terminal_size
|
with monkeypatch.context() as m:
|
||||||
|
m.setattr(shutil, 'get_terminal_size', stub_terminal_size)
|
||||||
shutil.get_terminal_size = stub_terminal_size
|
|
||||||
mycli = MyCli()
|
mycli = MyCli()
|
||||||
assert isinstance(mycli.get_reserved_space(), int)
|
assert isinstance(mycli.get_reserved_space(), int)
|
||||||
|
|
||||||
shutil.get_terminal_size = old_func
|
|
||||||
|
|
||||||
|
|
||||||
def test_list_dsn():
|
def test_list_dsn():
|
||||||
runner = CliRunner()
|
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("""\
|
myclirc.write(dedent("""\
|
||||||
[alias_dsn]
|
[alias_dsn]
|
||||||
test = mysql://test/test
|
test = mysql://test/test
|
||||||
|
@ -282,6 +280,15 @@ def test_list_dsn():
|
||||||
result = runner.invoke(cli, args=args + ['--verbose'])
|
result = runner.invoke(cli, args=args + ['--verbose'])
|
||||||
assert result.output == "test : mysql://test/test\n"
|
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():
|
def test_prettify_statement():
|
||||||
statement = 'SELECT 1'
|
statement = 'SELECT 1'
|
||||||
|
@ -299,7 +306,8 @@ def test_unprettify_statement():
|
||||||
|
|
||||||
def test_list_ssh_config():
|
def test_list_ssh_config():
|
||||||
runner = CliRunner()
|
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("""\
|
ssh_config.write(dedent("""\
|
||||||
Host test
|
Host test
|
||||||
Hostname test.example.com
|
Hostname test.example.com
|
||||||
|
@ -314,6 +322,13 @@ def test_list_ssh_config():
|
||||||
result = runner.invoke(cli, args=args + ['--verbose'])
|
result = runner.invoke(cli, args=args + ['--verbose'])
|
||||||
assert "test : test.example.com\n" in result.output
|
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):
|
def test_dsn(monkeypatch):
|
||||||
# Setup classes to mock mycli.main.MyCli
|
# Setup classes to mock mycli.main.MyCli
|
||||||
|
@ -466,7 +481,8 @@ def test_ssh_config(monkeypatch):
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|
||||||
# Setup temporary configuration
|
# 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("""\
|
ssh_config.write(dedent("""\
|
||||||
Host test
|
Host test
|
||||||
Hostname test.example.com
|
Hostname test.example.com
|
||||||
|
@ -489,8 +505,8 @@ def test_ssh_config(monkeypatch):
|
||||||
MockMyCli.connect_args["ssh_user"] == "joe" and \
|
MockMyCli.connect_args["ssh_user"] == "joe" and \
|
||||||
MockMyCli.connect_args["ssh_host"] == "test.example.com" and \
|
MockMyCli.connect_args["ssh_host"] == "test.example.com" and \
|
||||||
MockMyCli.connect_args["ssh_port"] == 22222 and \
|
MockMyCli.connect_args["ssh_port"] == 22222 and \
|
||||||
MockMyCli.connect_args["ssh_key_filename"] == os.getenv(
|
MockMyCli.connect_args["ssh_key_filename"] == os.path.expanduser(
|
||||||
"HOME") + "/.ssh/gateway"
|
"~") + "/.ssh/gateway"
|
||||||
|
|
||||||
# When a user supplies a ssh config host as argument to mycli,
|
# When a user supplies a ssh config host as argument to mycli,
|
||||||
# and used command line arguments, use the command line
|
# 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_port"] == 3 and \
|
||||||
MockMyCli.connect_args["ssh_key_filename"] == "/path/to/key"
|
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
|
@dbtest
|
||||||
def test_init_command_arg(executor):
|
def test_init_command_arg(executor):
|
||||||
|
|
|
@ -50,26 +50,50 @@ def test_editor_command():
|
||||||
|
|
||||||
os.environ['EDITOR'] = 'true'
|
os.environ['EDITOR'] = 'true'
|
||||||
os.environ['VISUAL'] = '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"
|
mycli.packages.special.open_external_editor(sql=r'select 1') == "select 1"
|
||||||
|
else:
|
||||||
|
pytest.skip('Skipping on Windows platform.')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_tee_command():
|
def test_tee_command():
|
||||||
mycli.packages.special.write_tee(u"hello world") # write without file set
|
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.execute(None, u"tee " + f.name)
|
||||||
mycli.packages.special.write_tee(u"hello world")
|
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"
|
assert f.read() == b"hello world\n"
|
||||||
|
|
||||||
mycli.packages.special.execute(None, u"tee -o " + f.name)
|
mycli.packages.special.execute(None, u"tee -o " + f.name)
|
||||||
mycli.packages.special.write_tee(u"hello world")
|
mycli.packages.special.write_tee(u"hello world")
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
|
if os.name=='nt':
|
||||||
|
assert f.read() == b"hello world\r\n"
|
||||||
|
else:
|
||||||
assert f.read() == b"hello world\n"
|
assert f.read() == b"hello world\n"
|
||||||
|
|
||||||
mycli.packages.special.execute(None, u"notee")
|
mycli.packages.special.execute(None, u"notee")
|
||||||
mycli.packages.special.write_tee(u"hello world")
|
mycli.packages.special.write_tee(u"hello world")
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
|
if os.name=='nt':
|
||||||
|
assert f.read() == b"hello world\r\n"
|
||||||
|
else:
|
||||||
assert f.read() == b"hello world\n"
|
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():
|
def test_tee_command_error():
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
|
@ -82,6 +106,8 @@ def test_tee_command_error():
|
||||||
|
|
||||||
|
|
||||||
@dbtest
|
@dbtest
|
||||||
|
|
||||||
|
@pytest.mark.skipif(os.name == "nt", reason="Bug: fails on Windows, needs fixing, singleton of FQ not working right")
|
||||||
def test_favorite_query():
|
def test_favorite_query():
|
||||||
with db_connection().cursor() as cur:
|
with db_connection().cursor() as cur:
|
||||||
query = u'select "✔"'
|
query = u'select "✔"'
|
||||||
|
@ -98,16 +124,29 @@ def test_once_command():
|
||||||
mycli.packages.special.execute(None, u"\\once /proc/access-denied")
|
mycli.packages.special.execute(None, u"\\once /proc/access-denied")
|
||||||
|
|
||||||
mycli.packages.special.write_once(u"hello world") # write without file set
|
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.execute(None, u"\\once " + f.name)
|
||||||
mycli.packages.special.write_once(u"hello world")
|
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"
|
assert f.read() == b"hello world\n"
|
||||||
|
|
||||||
mycli.packages.special.execute(None, u"\\once -o " + f.name)
|
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 1")
|
||||||
mycli.packages.special.write_once(u"hello world line 2")
|
mycli.packages.special.write_once(u"hello world line 2")
|
||||||
f.seek(0)
|
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"
|
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():
|
def test_pipe_once_command():
|
||||||
|
@ -118,6 +157,11 @@ def test_pipe_once_command():
|
||||||
mycli.packages.special.execute(
|
mycli.packages.special.execute(
|
||||||
None, u"\\pipe_once /proc/access-denied")
|
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.execute(None, u"\\pipe_once wc")
|
||||||
mycli.packages.special.write_once(u"hello world")
|
mycli.packages.special.write_once(u"hello world")
|
||||||
mycli.packages.special.unset_pipe_once_if_written()
|
mycli.packages.special.unset_pipe_once_if_written()
|
||||||
|
@ -128,11 +172,20 @@ def test_parseargfile():
|
||||||
"""Test that parseargfile expands the user directory."""
|
"""Test that parseargfile expands the user directory."""
|
||||||
expected = {'file': os.path.join(os.path.expanduser('~'), 'filename'),
|
expected = {'file': os.path.join(os.path.expanduser('~'), 'filename'),
|
||||||
'mode': 'a'}
|
'mode': 'a'}
|
||||||
|
|
||||||
|
if os.name=='nt':
|
||||||
|
assert expected == mycli.packages.special.iocommands.parseargfile(
|
||||||
|
'~\\filename')
|
||||||
|
else:
|
||||||
assert expected == mycli.packages.special.iocommands.parseargfile(
|
assert expected == mycli.packages.special.iocommands.parseargfile(
|
||||||
'~/filename')
|
'~/filename')
|
||||||
|
|
||||||
expected = {'file': os.path.join(os.path.expanduser('~'), 'filename'),
|
expected = {'file': os.path.join(os.path.expanduser('~'), 'filename'),
|
||||||
'mode': 'w'}
|
'mode': 'w'}
|
||||||
|
if os.name=='nt':
|
||||||
|
assert expected == mycli.packages.special.iocommands.parseargfile(
|
||||||
|
'-o ~\\filename')
|
||||||
|
else:
|
||||||
assert expected == mycli.packages.special.iocommands.parseargfile(
|
assert expected == mycli.packages.special.iocommands.parseargfile(
|
||||||
'-o ~/filename')
|
'-o ~/filename')
|
||||||
|
|
||||||
|
@ -162,6 +215,7 @@ def test_watch_query_iteration():
|
||||||
|
|
||||||
|
|
||||||
@dbtest
|
@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():
|
def test_watch_query_full():
|
||||||
"""Test that `watch_query`:
|
"""Test that `watch_query`:
|
||||||
|
|
||||||
|
|
|
@ -117,6 +117,7 @@ def test_multiple_queries_same_line_syntaxerror(executor):
|
||||||
|
|
||||||
|
|
||||||
@dbtest
|
@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):
|
def test_favorite_query(executor):
|
||||||
set_expanded_output(False)
|
set_expanded_output(False)
|
||||||
run(executor, "create table test(a text)")
|
run(executor, "create table test(a text)")
|
||||||
|
@ -136,6 +137,7 @@ def test_favorite_query(executor):
|
||||||
|
|
||||||
|
|
||||||
@dbtest
|
@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):
|
def test_favorite_query_multiple_statement(executor):
|
||||||
set_expanded_output(False)
|
set_expanded_output(False)
|
||||||
run(executor, "create table test(a text)")
|
run(executor, "create table test(a text)")
|
||||||
|
@ -159,6 +161,7 @@ def test_favorite_query_multiple_statement(executor):
|
||||||
|
|
||||||
|
|
||||||
@dbtest
|
@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):
|
def test_favorite_query_expanded_output(executor):
|
||||||
set_expanded_output(False)
|
set_expanded_output(False)
|
||||||
run(executor, '''create table test(a text)''')
|
run(executor, '''create table test(a text)''')
|
||||||
|
@ -195,16 +198,21 @@ def test_cd_command_without_a_folder_name(executor):
|
||||||
@dbtest
|
@dbtest
|
||||||
def test_system_command_not_found(executor):
|
def test_system_command_not_found(executor):
|
||||||
results = run(executor, 'system xyz')
|
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_result_equal(results, status='OSError: No such file or directory',
|
||||||
assert_contains=True)
|
assert_contains=True)
|
||||||
|
|
||||||
|
|
||||||
@dbtest
|
@dbtest
|
||||||
def test_system_command_output(executor):
|
def test_system_command_output(executor):
|
||||||
|
eol = os.linesep
|
||||||
test_dir = os.path.abspath(os.path.dirname(__file__))
|
test_dir = os.path.abspath(os.path.dirname(__file__))
|
||||||
test_file_path = os.path.join(test_dir, 'test.txt')
|
test_file_path = os.path.join(test_dir, 'test.txt')
|
||||||
results = run(executor, 'system cat {0}'.format(test_file_path))
|
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
|
@dbtest
|
||||||
|
|
Loading…
Add table
Reference in a new issue