diff options
author | Amjith Ramanujam <amjith.r@gmail.com> | 2018-05-18 21:48:21 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-05-18 21:48:21 -0700 |
commit | 9ae9df624fbef44003cb5e28f963074fee1ea1ee (patch) | |
tree | 07d89be0c963e469a09aa7c9710b5cef503a176e | |
parent | 024effd17ed96835a61474f0740e71dacec4f2af (diff) | |
parent | 644ad1a963ee3de3d5aba02b4afb0e9effd4b42d (diff) |
Merge branch 'master' into feature/get-last-sql-query
-rw-r--r-- | .git-blame-ignore-revs | 0 | ||||
-rw-r--r-- | .travis.yml | 1 | ||||
-rw-r--r-- | AUTHORS | 1 | ||||
-rw-r--r-- | DEVELOP.rst | 8 | ||||
-rw-r--r-- | changelog.rst | 2 | ||||
-rw-r--r-- | pgcli/main.py | 37 | ||||
-rw-r--r-- | pgcli/packages/parseutils/__init__.py | 19 | ||||
-rw-r--r-- | pgcli/packages/prompt_utils.py | 37 | ||||
-rw-r--r-- | pgcli/pgclirc | 5 | ||||
-rw-r--r-- | pgcli/pgexecute.py | 3 | ||||
-rw-r--r-- | setup.py | 1 | ||||
-rw-r--r-- | tests/test_prompt_utils.py | 14 |
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 @@ -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)""" @@ -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 |