1
0
Fork 0

Merge branch 'master' into master

This commit is contained in:
Amjith Ramanujam 2018-05-17 05:57:46 -07:00 committed by GitHub
commit 0643fd6534
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 94 additions and 48 deletions

View File

@ -5,5 +5,5 @@
## Checklist
<!--- We appreciate your help and want to give you credit. Please take a moment to put an `x` in the boxes below as you complete them. -->
- [ ] I've added this contribution to the `changelog.md`.
- [ ] I've added this contribution to the `changelog.rst`.
- [ ] I've added my name to the `AUTHORS` file (or it's already there).

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

@ -76,6 +76,8 @@ Contributors:
* Pierre Giraud
* Andrew Kuchling
* Dan Clark
* Catherine Devlin
* Jason Ribeiro
Creator:

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

@ -48,7 +48,7 @@ Usage
or
$ pgcli postgresql://[user[:password]@][netloc][:port][/dbname]
$ pgcli postgresql://[user[:password]@][netloc][:port][/dbname][?extra=value[&other=other-value]]
Examples:
@ -56,7 +56,7 @@ Examples:
$ pgcli local_database
$ pgcli postgres://amjith:pa$$w0rd@example.com:5432/app_db
$ pgcli postgres://amjith:pa$$w0rd@example.com:5432/app_db?sslmode=verify-ca&sslrootcert=/myrootcert
Features
--------

View File

@ -1,6 +1,11 @@
Upcoming:
=========
Features:
---------
* Add quit commands to the completion menu. (Thanks: `Jason Ribeiro`_)
* Add table formats to ``\T`` completion. (Thanks: `Jason Ribeiro`_)
Internal changes:
-----------------
@ -8,6 +13,11 @@ Internal changes:
* Add ``application_name`` to help identify pgcli connection to database (issue #868) (Thanks: `François Pietka`_)
* 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:
======
@ -746,6 +756,7 @@ Bug Fixes:
----------
* Fix the broken behavior of \d+. (Thanks: https://github.com/macobo)
* Fix a crash during auto-completion. (Thanks: https://github.com/Erethon)
* Avoid losing pre_run_callables on error in editing. (Thanks: https://github.com/catherinedevlin)
Improvements:
-------------
@ -814,3 +825,4 @@ Improvements:
.. _`Isank`: https://github.com/isank
.. _`Bojan Delić`: https://github.com/delicb
.. _`Frederic Aoustin`: https://github.com/fraoustin
.. _`Jason Ribeiro`: https://github.com/jrib

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
@ -91,6 +92,10 @@ OutputSettings.__new__.__defaults__ = (
)
class PgCliQuitError(Exception):
pass
class PGCli(object):
default_prompt = '\\u@\\h:\\d> '
@ -202,6 +207,9 @@ class PGCli(object):
self.eventloop = create_eventloop()
self.cli = None
def quit(self):
raise PgCliQuitError
def register_special_commands(self):
self.pgspecial.register(
@ -211,6 +219,12 @@ class PGCli(object):
refresh_callback = lambda: self.refresh_completions(
persist_priorities='all')
self.pgspecial.register(self.quit, '\\q', '\\q',
'Quit pgcli.', arg_type=NO_QUERY, case_sensitive=True,
aliases=(':q',))
self.pgspecial.register(self.quit, 'quit', 'quit',
'Quit pgcli.', arg_type=NO_QUERY, case_sensitive=False,
aliases=('exit',))
self.pgspecial.register(refresh_callback, '\\#', '\\#',
'Refresh auto-completions.', arg_type=NO_QUERY)
self.pgspecial.register(refresh_callback, '\\refresh', '\\refresh',
@ -367,7 +381,7 @@ class PGCli(object):
user=fixup_possible_percent_encoding(uri.username),
port=fixup_possible_percent_encoding(uri.port),
passwd=fixup_possible_percent_encoding(uri.password))
# Deal with extra params e.g. ?sslmode=verify-ca&ssl-cert=/mycert
# Deal with extra params e.g. ?sslmode=verify-ca&sslrootcert=/myrootcert
if uri.query:
arguments = dict(
{k: v for k, (v,) in parse_qs(uri.query).items()},
@ -391,6 +405,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.
@ -412,6 +431,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):
@ -421,6 +442,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
@ -449,19 +472,22 @@ class PGCli(object):
# It's internal api of prompt_toolkit that may change. This was added to fix #668.
# We may find a better way to do it in the future.
saved_callables = cli.application.pre_run_callables
while special.editor_command(document.text):
filename = special.get_filename(document.text)
query = (special.get_editor_query(document.text) or
self.get_last_query())
sql, message = special.open_external_editor(filename, sql=query)
if message:
# Something went wrong. Raise an exception and bail.
raise RuntimeError(message)
cli.current_buffer.document = Document(sql, cursor_position=len(sql))
cli.application.pre_run_callables = []
document = cli.run()
continue
cli.application.pre_run_callables = saved_callables
try:
while special.editor_command(document.text):
filename = special.get_filename(document.text)
query = (special.get_editor_query(document.text) or
self.get_last_query())
sql, message = special.open_external_editor(
filename, sql=query)
if message:
# Something went wrong. Raise an exception and bail.
raise RuntimeError(message)
cli.current_buffer.document = Document(sql,
cursor_position=len(sql))
cli.application.pre_run_callables = []
document = cli.run()
finally:
cli.application.pre_run_callables = saved_callables
return document
def execute_command(self, text, query):
@ -489,6 +515,8 @@ class PGCli(object):
logger.error("sql: %r, error: %r", text, e)
logger.error("traceback: %r", traceback.format_exc())
self._handle_server_closed_connection()
except PgCliQuitError as e:
raise
except Exception as e:
logger.error("sql: %r, error: %r", text, e)
logger.error("traceback: %r", traceback.format_exc())
@ -555,13 +583,6 @@ class PGCli(object):
while True:
document = self.cli.run()
# The reason we check here instead of inside the pgexecute is
# because we want to raise the Exit exception which will be
# caught by the try/except block that wraps the pgexecute.run()
# statement.
if quit_command(document.text):
raise EOFError
try:
document = self.handle_editor_command(self.cli, document)
except RuntimeError as e:
@ -573,15 +594,19 @@ class PGCli(object):
# Initialize default metaquery in case execution fails
query = MetaQuery(query=document.text, successful=False)
watch_command, timing = special.get_watch_command(document.text)
if watch_command:
while watch_command:
self.watch_command, timing = special.get_watch_command(
document.text)
if self.watch_command:
while self.watch_command:
try:
query = self.execute_command(watch_command, query)
click.echo('Waiting for {0} seconds before repeating'.format(timing))
query = self.execute_command(
self.watch_command, query)
click.echo(
'Waiting for {0} seconds before repeating'
.format(timing))
sleep(timing)
except KeyboardInterrupt:
watch_command = None
self.watch_command = None
else:
query = self.execute_command(document.text, query)
@ -593,7 +618,7 @@ class PGCli(object):
self.query_history.append(query)
except EOFError:
except PgCliQuitError:
if not self.less_chatty:
print ('Goodbye!')
@ -849,7 +874,7 @@ class PGCli(object):
return self.query_history[-1][0] if self.query_history else None
def echo_via_pager(self, text, color=None):
if self.pgspecial.pager_config == PAGER_OFF:
if self.pgspecial.pager_config == PAGER_OFF or self.watch_command:
click.echo(text, color=color)
else:
click.echo_via_pager(text, color)
@ -1044,13 +1069,6 @@ def is_select(status):
return status.split(None, 1)[0].lower() == 'select'
def quit_command(sql):
return (sql.strip().lower() == 'exit'
or sql.strip().lower() == 'quit'
or sql.strip() == r'\q'
or sql.strip() == ':q')
def exception_formatter(e):
return click.style(utf8tounicode(str(e)), fg='red')

View File

@ -28,6 +28,7 @@ Schema.__new__.__defaults__ = (False,)
# used to ensure that the alias we suggest is unique
FromClauseItem = namedtuple('FromClauseItem', 'schema table_refs local_tables')
Table = namedtuple('Table', ['schema', 'table_refs', 'local_tables'])
TableFormat = namedtuple('TableFormat', [])
View = namedtuple('View', ['schema', 'table_refs'])
# JoinConditions are suggested after ON, e.g. 'foo.barid = bar.barid'
JoinCondition = namedtuple('JoinCondition', ['table_refs', 'parent'])
@ -252,6 +253,9 @@ def suggest_special(text):
if cmd in ('\\c', '\\connect'):
return (Database(),)
if cmd == '\\T':
return (TableFormat(),)
if cmd == '\\dn':
return (Schema(),)

View File

@ -4,13 +4,15 @@ import re
from itertools import count, repeat, chain
import operator
from collections import namedtuple, defaultdict, OrderedDict
from cli_helpers.tabular_output import TabularOutputFormatter
from pgspecial.namedqueries import NamedQueries
from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.contrib.completers import PathCompleter
from prompt_toolkit.document import Document
from .packages.sqlcompletion import (FromClauseItem,
suggest_type, Special, Database, Schema, Table, Function, Column, View,
Keyword, NamedQuery, Datatype, Alias, Path, JoinCondition, Join)
from .packages.sqlcompletion import (
FromClauseItem, suggest_type, Special, Database, Schema, Table,
TableFormat, Function, Column, View, Keyword, NamedQuery,
Datatype, Alias, Path, JoinCondition, Join)
from .packages.parseutils.meta import ColumnMetadata, ForeignKey
from .packages.parseutils.utils import last_word
from .packages.parseutils.tables import TableReference
@ -321,7 +323,8 @@ class PGCompleter(Completer):
return []
prio_order = [
'keyword', 'function', 'view', 'table', 'datatype', 'database',
'schema', 'column', 'table alias', 'join', 'name join', 'fk join'
'schema', 'column', 'table alias', 'join', 'name join', 'fk join',
'table format'
]
type_priority = prio_order.index(meta) if meta in prio_order else -1
text = last_word(text, include='most_punctuations').lower()
@ -778,6 +781,9 @@ class PGCompleter(Completer):
tables = [self._make_cand(t, alias, suggestion) for t in tables]
return self.find_matches(word_before_cursor, tables, meta='table')
def get_table_formats(self, _, word_before_cursor):
formats = TabularOutputFormatter().supported_formats
return self.find_matches(word_before_cursor, formats, meta='table format')
def get_view_matches(self, suggestion, word_before_cursor, alias=False):
views = self.populate_schema_objects(suggestion.schema, 'views')
@ -861,6 +867,7 @@ class PGCompleter(Completer):
Function: get_function_matches,
Schema: get_schema_matches,
Table: get_table_matches,
TableFormat: get_table_formats,
View: get_view_matches,
Alias: get_alias_matches,
Database: get_database_matches,

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'
]