1
0
Fork 0

Merge branch 'master' into feature/get-last-sql-query

This commit is contained in:
Amjith Ramanujam 2018-05-18 21:48:21 -07:00 committed by GitHub
commit 9ae9df624f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 119 additions and 9 deletions

0
.git-blame-ignore-revs Normal file
View File

View File

@ -8,6 +8,7 @@ python:
install:
- pip install .
- pip install -r requirements-dev.txt
- pip install keyrings.alt>=3.1
script:
- set -e

View File

@ -75,6 +75,7 @@ Contributors:
* Frederic Aoustin
* Pierre Giraud
* Andrew Kuchling
* Dan Clark
* Catherine Devlin
* Jason Ribeiro
* Rishi Ramraj

View File

@ -148,8 +148,8 @@ To see stdout/stderr, use the following command:
$ behave --no-capture
PEP8 checks
-----------
PEP8 checks (lint)
-----------------_
When you submit a PR, the changeset is checked for pep8 compliance using
`pep8radius <https://github.com/hayd/pep8radius>`_. If you see a build failing because
@ -158,7 +158,7 @@ of these checks, install pep8radius and apply style fixes:
::
$ pip install pep8radius
$ pep8radius --docformatter --diff # view a diff of proposed fixes
$ pep8radius --docformatter --in-place # apply the fixes
$ pep8radius master --docformatter --diff # view a diff of proposed fixes
$ pep8radius master --docformatter --in-place # apply the fixes
Then commit and push the fixes.

View File

@ -12,10 +12,12 @@ Internal changes:
* Mark tests requiring a running database server as dbtest (Thanks: `Dick Marinus`_)
* Add ``application_name`` to help identify pgcli connection to database (issue #868) (Thanks: `François Pietka`_)
* Add an is_special command flag to MetaQuery (Thanks: `Rishi Ramraj`_)
* Ported Destructive Warning from mycli.
Bug Fixes:
----------
* Disable pager when using \watch (#837). (Thanks: `Jason Ribeiro`_)
* Don't offer to reconnect when we can't change a param in realtime (#807). (Thanks: `Amjith Ramanujam`_)
1.9.1:
======

View File

@ -40,6 +40,7 @@ from pygments.token import Token
from pgspecial.main import (PGSpecial, NO_QUERY, PAGER_OFF)
import pgspecial as special
import keyring
from .pgcompleter import PGCompleter
from .pgtoolbar import create_toolbar_tokens_func
from .pgstyle import style_factory, style_factory_output
@ -51,6 +52,7 @@ from .config import (get_casing_file,
from .key_bindings import pgcli_bindings
from .encodingutils import utf8tounicode
from .encodingutils import text_type
from .packages.prompt_utils import confirm_destructive_query
from .__init__ import __version__
click.disable_unicode_literals_warning = True
@ -124,7 +126,7 @@ class PGCli(object):
def __init__(self, force_passwd_prompt=False, never_passwd_prompt=False,
pgexecute=None, pgclirc_file=None, row_limit=None,
single_connection=False, less_chatty=None, prompt=None, prompt_dsn=None,
auto_vertical_output=False):
auto_vertical_output=False, warn=None):
self.force_passwd_prompt = force_passwd_prompt
self.never_passwd_prompt = never_passwd_prompt
@ -159,6 +161,8 @@ class PGCli(object):
self.syntax_style = c['main']['syntax_style']
self.cli_style = c['colors']
self.wider_completion_menu = c['main'].as_bool('wider_completion_menu')
c_dest_warning = c['main'].as_bool('destructive_warning')
self.destructive_warning = c_dest_warning if warn is None else warn
self.less_chatty = bool(less_chatty) or c['main'].as_bool('less_chatty')
self.null_string = c['main'].get('null_string', '<null>')
self.prompt_format = prompt if prompt is not None else c['main'].get('prompt', self.default_prompt)
@ -294,6 +298,11 @@ class PGCli(object):
except IOError as e:
return [(None, None, None, str(e), '', False, True)]
if (self.destructive_warning and
confirm_destructive_query(query) is False):
message = 'Wise choice. Command execution stopped.'
return [(None, None, None, message)]
on_error_resume = (self.on_error == 'RESUME')
return self.pgexecute.run(
query, self.pgspecial, on_error_resume=on_error_resume
@ -397,6 +406,11 @@ class PGCli(object):
if not self.force_passwd_prompt and not passwd:
passwd = os.environ.get('PGPASSWORD', '')
# Find password from store
key = '%s@%s' % (user, host)
if not passwd:
passwd = keyring.get_password('pgcli', key)
# Prompt for a password immediately if requested via the -W flag. This
# avoids wasting time trying to connect to the database and catching a
# no-password exception.
@ -418,6 +432,8 @@ class PGCli(object):
try:
pgexecute = PGExecute(database, user, passwd, host, port, dsn,
application_name='pgcli', **kwargs)
if passwd:
keyring.set_password('pgcli', key, passwd)
except (OperationalError, InterfaceError) as e:
if ('no password supplied' in utf8tounicode(e.args[0]) and
auto_passwd_prompt):
@ -427,6 +443,8 @@ class PGCli(object):
pgexecute = PGExecute(database, user, passwd, host, port,
dsn, application_name='pgcli',
**kwargs)
if passwd:
keyring.set_password('pgcli', key, passwd)
else:
raise e
@ -479,7 +497,16 @@ class PGCli(object):
query = MetaQuery(query=text, successful=False)
try:
output, query = self._evaluate_command(text)
if (self.destructive_warning):
destroy = confirm = confirm_destructive_query(text)
if destroy is None:
output, query = self._evaluate_command(text)
elif destroy is True:
click.secho('Your call!')
output, query = self._evaluate_command(text)
else:
click.secho('Wise choice!')
raise KeyboardInterrupt
except KeyboardInterrupt:
# Restart connection to the database
self.pgexecute.connect()
@ -886,12 +913,14 @@ class PGCli(object):
'available databases, then exit.')
@click.option('--auto-vertical-output', is_flag=True,
help='Automatically switch to vertical output mode if the result is wider than the terminal width.')
@click.option('--warn/--no-warn', default=None,
help='Warn before running a destructive query.')
@click.argument('database', default=lambda: None, envvar='PGDATABASE', nargs=1)
@click.argument('username', default=lambda: None, envvar='PGUSER', nargs=1)
def cli(database, username_opt, host, port, prompt_passwd, never_prompt,
single_connection, dbname, username, version, pgclirc, dsn, row_limit,
less_chatty, prompt, prompt_dsn, list_databases, auto_vertical_output,
list_dsn):
list_dsn, warn):
if version:
print('Version:', __version__)
@ -927,7 +956,7 @@ def cli(database, username_opt, host, port, prompt_passwd, never_prompt,
pgcli = PGCli(prompt_passwd, never_prompt, pgclirc_file=pgclirc,
row_limit=row_limit, single_connection=single_connection,
less_chatty=less_chatty, prompt=prompt, prompt_dsn=prompt_dsn,
auto_vertical_output=auto_vertical_output)
auto_vertical_output=auto_vertical_output, warn=warn)
# Choose which ever one has a valid value.
database = database or dbname

View File

@ -0,0 +1,19 @@
import sqlparse
def query_starts_with(query, prefixes):
"""Check if the query starts with any item from *prefixes*."""
prefixes = [prefix.lower() for prefix in prefixes]
formatted_sql = sqlparse.format(query.lower(), strip_comments=True)
return bool(formatted_sql) and formatted_sql.split()[0] in prefixes
def queries_start_with(queries, prefixes):
"""Check if any queries start with any item from *prefixes*."""
for query in sqlparse.split(queries):
if query and query_starts_with(query, prefixes) is True:
return True
return False
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)

View File

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
import sys
import click
from .parseutils import is_destructive
def confirm_destructive_query(queries):
"""Check if the query is destructive and prompts the user to confirm.
Returns:
* None if the query is non-destructive or we can't prompt the user.
* True if the query is destructive and the user wants to proceed.
* False if the query is destructive and the user doesn't want to proceed.
"""
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)
def confirm(*args, **kwargs):
"""Prompt for confirmation (yes/no) and handle any abort exceptions."""
try:
return click.confirm(*args, **kwargs)
except click.Abort:
return False
def prompt(*args, **kwargs):
"""Prompt the user for input and handle any abort exceptions."""
try:
return click.prompt(*args, **kwargs)
except click.Abort:
return False

View File

@ -22,6 +22,11 @@ multi_line = False
# a command.
multi_line_mode = psql
# Destructive warning mode will alert you before executing a sql statement
# that may cause harm to the database such as "drop table", "drop database"
# or "shutdown".
destructive_warning = True
# Enables expand mode, which is similar to `\x` in psql.
expand = False

View File

@ -346,7 +346,8 @@ class PGExecute(object):
"""
return (isinstance(e, psycopg2.OperationalError) and
(not e.pgcode or
psycopg2.errorcodes.lookup(e.pgcode) != 'LOCK_NOT_AVAILABLE'))
psycopg2.errorcodes.lookup(e.pgcode) not in
('LOCK_NOT_AVAILABLE', 'CANT_CHANGE_RUNTIME_PARAM')))
def execute_normal_sql(self, split_sql):
"""Returns tuple (title, rows, headers, status)"""

View File

@ -21,6 +21,7 @@ install_requirements = [
'configobj >= 5.0.6',
'humanize >= 0.5.1',
'cli_helpers[styles] >= 1.0.1',
'keyring >= 12.2.0'
]

View File

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
import click
from pgcli.packages.prompt_utils import confirm_destructive_query
def test_confirm_destructive_query_notty():
stdin = click.get_text_stream('stdin')
assert stdin.isatty() is False
sql = 'drop database foo;'
assert confirm_destructive_query(sql) is None