summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAmjith Ramanujam <amjith.r@gmail.com>2018-05-18 21:48:21 -0700
committerGitHub <noreply@github.com>2018-05-18 21:48:21 -0700
commit9ae9df624fbef44003cb5e28f963074fee1ea1ee (patch)
tree07d89be0c963e469a09aa7c9710b5cef503a176e
parent024effd17ed96835a61474f0740e71dacec4f2af (diff)
parent644ad1a963ee3de3d5aba02b4afb0e9effd4b42d (diff)
Merge branch 'master' into feature/get-last-sql-query
-rw-r--r--.git-blame-ignore-revs0
-rw-r--r--.travis.yml1
-rw-r--r--AUTHORS1
-rw-r--r--DEVELOP.rst8
-rw-r--r--changelog.rst2
-rw-r--r--pgcli/main.py37
-rw-r--r--pgcli/packages/parseutils/__init__.py19
-rw-r--r--pgcli/packages/prompt_utils.py37
-rw-r--r--pgcli/pgclirc5
-rw-r--r--pgcli/pgexecute.py3
-rw-r--r--setup.py1
-rw-r--r--tests/test_prompt_utils.py14
12 files changed, 119 insertions, 9 deletions
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.git-blame-ignore-revs
diff --git a/.travis.yml b/.travis.yml
index 6093e745..f54e1a8e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,6 +8,7 @@ python:
install:
- pip install .
- pip install -r requirements-dev.txt
+ - pip install keyrings.alt>=3.1
script:
- set -e
diff --git a/AUTHORS b/AUTHORS
index f467e11c..df481086 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -75,6 +75,7 @@ Contributors:
* Frederic Aoustin
* Pierre Giraud
* Andrew Kuchling
+ * Dan Clark
* Catherine Devlin
* Jason Ribeiro
* Rishi Ramraj
diff --git a/DEVELOP.rst b/DEVELOP.rst
index 504ad3af..601c3f30 100644
--- a/DEVELOP.rst
+++ b/DEVELOP.rst
@@ -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.
diff --git a/changelog.rst b/changelog.rst
index 34d7c102..d8c21080 100644
--- a/changelog.rst
+++ b/changelog.rst
@@ -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:
======
diff --git a/pgcli/main.py b/pgcli/main.py
index b05cf88d..8ae5927d 100644
--- a/pgcli/main.py
+++ b/pgcli/main.py
@@ -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
diff --git a/pgcli/packages/parseutils/__init__.py b/pgcli/packages/parseutils/__init__.py
index e69de29b..818af9a7 100644
--- a/pgcli/packages/parseutils/__init__.py
+++ b/pgcli/packages/parseutils/__init__.py
@@ -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)
diff --git a/pgcli/packages/prompt_utils.py b/pgcli/packages/prompt_utils.py
new file mode 100644
index 00000000..420ea2a6
--- /dev/null
+++ b/pgcli/packages/prompt_utils.py
@@ -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
diff --git a/pgcli/pgclirc b/pgcli/pgclirc
index 70f10ee7..f2061c71 100644
--- a/pgcli/pgclirc
+++ b/pgcli/pgclirc
@@ -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
diff --git a/pgcli/pgexecute.py b/pgcli/pgexecute.py
index 14eb1ed2..0d6195e0 100644
--- a/pgcli/pgexecute.py
+++ b/pgcli/pgexecute.py
@@ -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)"""
diff --git a/setup.py b/setup.py
index 84489924..25849aea 100644
--- a/setup.py
+++ b/setup.py
@@ -21,6 +21,7 @@ install_requirements = [
'configobj >= 5.0.6',
'humanize >= 0.5.1',
'cli_helpers[styles] >= 1.0.1',
+ 'keyring >= 12.2.0'
]
diff --git a/tests/test_prompt_utils.py b/tests/test_prompt_utils.py
new file mode 100644
index 00000000..de676366
--- /dev/null
+++ b/tests/test_prompt_utils.py
@@ -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