Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add generic output formatter interface. #388

Merged
merged 80 commits into from
Apr 21, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
6ada4d4
Add basic output formatter class functionality.
tsroten Mar 29, 2017
2bc4fac
Add missing class.
tsroten Mar 29, 2017
3c78321
Make logic more readable.
tsroten Mar 29, 2017
26ff1f3
Move CSV to output formatter.
tsroten Mar 29, 2017
5da4175
Simply format output.
tsroten Mar 29, 2017
8918a51
Fix tsv/table logic.
tsroten Mar 29, 2017
7b6398a
Use missing_value keyword for csv wrapper.
tsroten Mar 29, 2017
f5a305d
Do not use tabulate for TSV formatting.
tsroten Mar 29, 2017
93c6306
Add space before expanded.
tsroten Mar 29, 2017
3a88128
Add preprocessor.
tsroten Mar 29, 2017
cff2ddd
Simplify missing value.
tsroten Mar 29, 2017
5cbdf48
Disable number parsing.
tsroten Mar 29, 2017
cced490
Remove unused imports.
tsroten Mar 29, 2017
d0f1dbf
Remove tabulate license note.
tsroten Mar 29, 2017
369b5b0
Fix tests/test_sqlexecute unit tests
meeuw Apr 1, 2017
ae3712f
fix test_main unit tests
meeuw Apr 1, 2017
3dbe5c0
use str for python2 delimiter
meeuw Apr 1, 2017
434aea0
add docstring to bytes_to_unicode function
meeuw Apr 1, 2017
be0e2ed
Add headers to preprocessor.
tsroten Apr 1, 2017
f4d0527
Move mycli table format to output formatter.
tsroten Apr 1, 2017
90fd5fe
Use context manager for StringIO.
tsroten Apr 1, 2017
9f0ace2
Remove register output format method.
tsroten Apr 1, 2017
e7b93b8
Remove print statement.
tsroten Apr 1, 2017
b58cad0
Update list of table formats.
tsroten Apr 1, 2017
cbd5ba0
Move bytes_to_hex to encoding utils package.
tsroten Apr 1, 2017
ae648ab
Fix encoding issue.
tsroten Apr 1, 2017
6ce7e15
Do not rely on tabulate for text type.
tsroten Apr 1, 2017
4a65f7b
Fix import.
tsroten Apr 1, 2017
02f5c52
Move pre-processing to output_formatter.
tsroten Apr 1, 2017
5fe9d68
Refactor expanded table.
tsroten Apr 1, 2017
2052ae4
Remove extra variable.
tsroten Apr 1, 2017
328a767
Remove extra argument to get_separator.
tsroten Apr 1, 2017
041eafb
Use named format strings for clarity.
tsroten Apr 1, 2017
3a10f5b
Move divider to format_row function.
tsroten Apr 1, 2017
61bdbd9
Make the expanded format test more readable.
tsroten Apr 2, 2017
0f01a9d
Update docstring to be clearer.
tsroten Apr 2, 2017
4b47e43
Use pymysql default conversions (instead of only our conversions)
meeuw Mar 23, 2017
5839c11
align floats on decimal point, quote columns with trailing/starting w…
meeuw Apr 2, 2017
1db7f86
Add basic output_formatter tests.
tsroten Apr 2, 2017
d330bf8
Merge branch 'feature/output_formatter' of github.com:dbcli/mycli int…
tsroten Apr 2, 2017
8684616
Add terminaltables.
tsroten Apr 2, 2017
4c13163
Default myclirc to ascii table.
tsroten Apr 2, 2017
dd41cf3
Fix table format tests.
tsroten Apr 2, 2017
145d5d2
Add a multi-column test for align_decimals.
tsroten Apr 2, 2017
6ea35ba
Format/idiomatic changes.
tsroten Apr 3, 2017
c51cfac
Move format_output tests to tests module.
tsroten Apr 3, 2017
a36cf85
Refactor OutputFormatter to remove tight coupling.
amjith Apr 4, 2017
7a81b9e
Update the tests.
amjith Apr 4, 2017
a96867c
PEP8 edits.
tsroten Apr 4, 2017
3d9feb9
Move expanded.py into output_formatter package.
amjith Apr 4, 2017
b5bda3f
Refactor the delimiter adapter to include tsv.
amjith Apr 4, 2017
2120b36
Fix failing tests.
amjith Apr 4, 2017
55aeab6
Merge branch 'master' of github.com:dbcli/mycli into feature/output_f…
tsroten Apr 4, 2017
549d9ad
Fix behave tests table formatting.
tsroten Apr 4, 2017
d9096ac
Merge branch 'master' of github.com:dbcli/mycli into feature/output_f…
tsroten Apr 6, 2017
079d9f8
PEP8 changes per pep8radius request.
tsroten Apr 6, 2017
8bc0fc4
Add period for pep8radius.
tsroten Apr 6, 2017
935fc56
Do not reference results rows by index.
tsroten Apr 10, 2017
5849ac9
Add empty result tests.
tsroten Apr 10, 2017
385f478
Revert "Remove tabulate license note."
tsroten Apr 12, 2017
16738b1
Add vendored tabulate version 0.8.0.
tsroten Apr 12, 2017
4c90605
Add preserve whitespace option to tabulate.
tsroten Apr 12, 2017
694919e
Remove extra html format.
tsroten Apr 12, 2017
85d0e02
Do not align columns for markup tables.
tsroten Apr 12, 2017
ae8311c
Do not quote whitespaces for tabulate formats.
tsroten Apr 12, 2017
359044c
Pep8radius changes.
tsroten Apr 12, 2017
13991b1
Add output format changes to changelog.
tsroten Apr 12, 2017
33e5c26
Add additional table formats to myclirc.
tsroten Apr 12, 2017
f06451c
Don't import quote_whitespaces since it's not used.
tsroten Apr 12, 2017
8f70b12
Fix outdated docstring.
tsroten Apr 13, 2017
fd861d4
Make SQLCompleter options pass through refresher.
tsroten Apr 13, 2017
81c5496
Pep8radius fixes.
tsroten Apr 13, 2017
9a23584
Fix fancy_grid table format.
tsroten Apr 13, 2017
4b0de47
Remove single terminaltable format since it's causing problems.
tsroten Apr 13, 2017
cc76dee
Use constant for missing value default.
tsroten Apr 13, 2017
39c4aec
Make registering a format not require kwargs.
tsroten Apr 13, 2017
9c70ce1
don't import as
meeuw Apr 15, 2017
cdbc7cd
Merge remote-tracking branch 'upstream/master' into feature/output_fo…
meeuw Apr 15, 2017
a5f11e6
rename all adapter functions to adapter
meeuw Apr 15, 2017
8fa96a2
don't import as (also for tests/test_output_formatter.py)
meeuw Apr 15, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Features:
---------

* Add ability to specify alternative myclirc file. (Thanks: [Dick Marinus]).
* Add new display formats for pretty printing query results. (Thanks: [Amjith
Ramanujam], [Dick Marinus], [Thomas Roten]).
* Add logic to shorten the default prompt if it becomes too long once generated. (Thanks: [John Sterling]).

Bug Fixes:
Expand All @@ -16,6 +18,7 @@ Bug Fixes:
* Fix requirements and remove old compatibility code (Thanks: [Dick Marinus])
* Fix bug where mycli would not start due to the thanks/credit intro text.
(Thanks: [Thomas Roten]).
* Use pymysql default conversions (issue #375). (Thanks: [Dick Marinus]).

Internal Changes:
-----------------
Expand Down
18 changes: 10 additions & 8 deletions mycli/completion_refresher.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,26 @@ def __init__(self):
self._completer_thread = None
self._restart_refresh = threading.Event()

def refresh(self, executor, callbacks):
"""
Creates a SQLCompleter object and populates it with the relevant
def refresh(self, executor, callbacks, completer_options={}):
"""Creates a SQLCompleter object and populates it with the relevant
completion suggestions in a background thread.

executor - SQLExecute object, used to extract the credentials to connect
to the database.
callbacks - A function or a list of functions to call after the thread
has completed the refresh. The newly created completion
object will be passed in as an argument to each callback.
completer_options - dict of options to pass to SQLCompleter.

"""
if self.is_refreshing():
self._restart_refresh.set()
return [(None, None, None, 'Auto-completion refresh restarted.')]
else:
self._completer_thread = threading.Thread(target=self._bg_refresh,
args=(executor, callbacks),
name='completion_refresh')
self._completer_thread = threading.Thread(
target=self._bg_refresh,
args=(executor, callbacks, completer_options),
name='completion_refresh')
self._completer_thread.setDaemon(True)
self._completer_thread.start()
return [(None, None, None,
Expand All @@ -39,8 +41,8 @@ def refresh(self, executor, callbacks):
def is_refreshing(self):
return self._completer_thread and self._completer_thread.is_alive()

def _bg_refresh(self, sqlexecute, callbacks):
completer = SQLCompleter(smart_completion=True)
def _bg_refresh(self, sqlexecute, callbacks, completer_options):
completer = SQLCompleter(**completer_options)

# Create a new pgexecute method to popoulate the completions.
e = sqlexecute
Expand Down
50 changes: 42 additions & 8 deletions mycli/encodingutils.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,58 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import binascii
import sys

PY2 = sys.version_info[0] == 2
PY3 = sys.version_info[0] == 3

if PY2:
text_type = unicode
binary_type = str
else:
text_type = str
binary_type = bytes


def unicode2utf8(arg):
"""
Only in Python 2. Psycopg2 expects the args as bytes not unicode.
In Python 3 the args are expected as unicode.
"""Convert strings to UTF8-encoded bytes.

Only in Python 2. In Python 3 the args are expected as unicode.

"""

if PY2 and isinstance(arg, unicode):
if PY2 and isinstance(arg, text_type):
return arg.encode('utf-8')
return arg


def utf8tounicode(arg):
"""
Only in Python 2. Psycopg2 returns the error message as utf-8.
In Python 3 the errors are returned as unicode.
"""Convert UTF8-encoded bytes to strings.

Only in Python 2. In Python 3 the errors are returned as strings.

"""

if PY2 and isinstance(arg, str):
if PY2 and isinstance(arg, binary_type):
return arg.decode('utf-8')
return arg


def bytes_to_string(b):
"""Convert bytes to a string. Hexlify bytes that can't be decoded.

>>> print(bytes_to_string(b"\\xff"))
0xff
>>> print(bytes_to_string('abc'))
abc
>>> print(bytes_to_string('✌'))

"""
if isinstance(b, binary_type):
try:
return b.decode('utf8')
except UnicodeDecodeError:
return '0x' + binascii.hexlify(b).decode('ascii')
return b
141 changes: 63 additions & 78 deletions mycli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,14 @@
import os
import os.path
import sys
import csv
import traceback
import socket
import logging
import threading
from time import time
from datetime import datetime
from random import choice
from io import open

# support StringIO for Python 2 and 3
try:
from cStringIO import StringIO
except ImportError:
from io import StringIO

import click
import sqlparse
from prompt_toolkit import CommandLineInterface, Application, AbortAction
Expand All @@ -33,11 +25,8 @@
ConditionalProcessor)
from prompt_toolkit.history import FileHistory
from pygments.token import Token
from configobj import ConfigObj, ConfigObjError

from .packages.tabulate import tabulate, table_formats
from .packages.expanded import expanded_table
from .packages.special.main import (COMMANDS, NO_QUERY)
from .packages.special.main import NO_QUERY
import mycli.packages.special as special
from .sqlcompleter import SQLCompleter
from .clitoolbar import create_toolbar_tokens_func
Expand All @@ -46,9 +35,9 @@
from .clibuffer import CLIBuffer
from .completion_refresher import CompletionRefresher
from .config import (write_default_config, get_mylogin_cnf_path,
open_mylogin_cnf, read_config_file,
read_config_files, str_to_bool)
open_mylogin_cnf, read_config_files, str_to_bool)
from .key_bindings import mycli_bindings
from .output_formatter import output_formatter
from .encodingutils import utf8tounicode
from .lexer import MyCliLexer
from .__init__ import __version__
Expand Down Expand Up @@ -118,7 +107,8 @@ def __init__(self, sqlexecute=None, prompt=None,
self.multi_line = c['main'].as_bool('multi_line')
self.key_bindings = c['main']['key_bindings']
special.set_timing_enabled(c['main'].as_bool('timing'))
self.table_format = c['main']['table_format']
self.formatter = output_formatter.OutputFormatter(
format_name=c['main']['table_format'])
self.syntax_style = c['main']['syntax_style']
self.less_chatty = c['main'].as_bool('less_chatty')
self.cli_style = c['colors']
Expand Down Expand Up @@ -157,7 +147,9 @@ def __init__(self, sqlexecute=None, prompt=None,

# Initialize completer.
self.smart_completion = c['main'].as_bool('smart_completion')
self.completer = SQLCompleter(self.smart_completion)
self.completer = SQLCompleter(
self.smart_completion,
supported_formats=self.formatter.supported_formats())
self._completer_lock = threading.Lock()

# Register custom special commands.
Expand Down Expand Up @@ -192,14 +184,16 @@ def register_special_commands(self):
'\\R', 'Change prompt format.', aliases=('\\R',), case_sensitive=True)

def change_table_format(self, arg, **_):
if not arg in table_formats():
msg = "Table type %s not yet implemented. Allowed types:" % arg
for table_type in table_formats():
msg += "\n\t%s" % table_type
try:
self.formatter.set_format_name(arg)
yield (None, None, None,
'Changed table type to {}'.format(arg))
except ValueError:
msg = 'Table type {} not yet implemented. Allowed types:'.format(
arg)
for table_type in self.formatter.supported_formats():
msg += "\n\t{}".format(table_type)
yield (None, None, None, msg)
else:
self.table_format = arg
yield (None, None, None, "Changed table Type to %s" % self.table_format)

def change_db(self, arg, **_):
if arg is None:
Expand Down Expand Up @@ -537,9 +531,9 @@ def one_iteration(document=None):
else:
max_width = None

formatted = format_output(title, cur, headers,
status, self.table_format,
special.is_expanded_output(), max_width)
formatted = self.format_output(title, cur, headers, status,
special.is_expanded_output(),
max_width)

output.extend(formatted)
end = time()
Expand Down Expand Up @@ -676,8 +670,10 @@ def refresh_completions(self, reset=False):
if reset:
with self._completer_lock:
self.completer.reset_completions()
self.completion_refresher.refresh(self.sqlexecute,
self._on_completions_refreshed)
self.completion_refresher.refresh(
self.sqlexecute, self._on_completions_refreshed,
{'smart_completion': self.smart_completion,
'supported_formats': self.formatter.supported_formats()})

return [(None, None, None,
'Auto-completion refresh started in the background.')]
Expand Down Expand Up @@ -719,15 +715,41 @@ def get_prompt(self, string):
string = string.replace('\\_', ' ')
return string

def run_query(self, query, table_format=None, new_line=True):
"""Runs query"""
def run_query(self, query, new_line=True):
"""Runs *query*."""
results = self.sqlexecute.run(query)
for result in results:
title, cur, headers, status = result
output = format_output(title, cur, headers, None, table_format)
output = self.format_output(title, cur, headers, None)
for line in output:
click.echo(line, nl=new_line)

def format_output(self, title, cur, headers, status, expanded=False,
max_width=None):
expanded = expanded or self.formatter.get_format_name() == 'expanded'
output = []

if title: # Only print the title if it's not None.
output.append(title)

if cur:
rows = list(cur)
formatted = self.formatter.format_output(
rows, headers, format_name='expanded' if expanded else None)

if (not expanded and max_width and rows and
content_exceeds_width(rows[0], max_width) and headers):
formatted = self.formatter.format_output(
rows, headers, format_name='expanded')

output.append(formatted)

if status: # Only print the status if it's not None.
output.append(status)

return output


@click.command()
@click.option('-h', '--host', envvar='MYSQL_HOST', help='Host address of the database.')
@click.option('-P', '--port', envvar='MYSQL_TCP_PORT', type=int, help='Port number to use for connection. Honors '
Expand Down Expand Up @@ -825,12 +847,12 @@ def cli(database, user, host, port, socket, password, dbname,
# --execute argument
if execute:
try:
table_format = None
if table:
table_format = mycli.table_format
elif csv:
table_format = 'csv'
mycli.run_query(execute, table_format=table_format)
if csv:
mycli.formatter.set_format_name('csv')
elif not table:
mycli.formatter.set_format_name('tsv')

mycli.run_query(execute)
exit(0)
except Exception as e:
click.secho(str(e), err=True, fg='red')
Expand All @@ -851,58 +873,21 @@ def cli(database, user, host, port, socket, password, dbname,
confirm_destructive_query(stdin_text) is False):
exit(0)
try:
table_format = None
new_line = True

if csv:
table_format = 'csv'
mycli.formatter.set_format_name('csv')
new_line = False
elif table:
table_format = mycli.table_format
elif not table:
mycli.formatter.set_format_name('tsv')

mycli.run_query(stdin_text, table_format=table_format, new_line=new_line)
mycli.run_query(stdin_text, new_line=new_line)
exit(0)
except Exception as e:
click.secho(str(e), err=True, fg='red')
exit(1)


def format_output(title, cur, headers, status, table_format, expanded=False, max_width=None):
output = []
if title: # Only print the title if it's not None.
output.append(title)
if cur:
headers = [utf8tounicode(x) for x in headers]
table_format = 'tsv' if table_format is None else table_format

if expanded:
output.append(expanded_table(cur, headers))
elif table_format == 'csv':
content = StringIO()
writer = csv.writer(content)
writer.writerow(headers)

for row in cur:
row = ['null' if val is None else str(val) for val in row]
writer.writerow(row)

output.append(content.getvalue())
content.close()
else:
rows = list(cur)
tabulated, frows = tabulate(rows, headers, tablefmt=table_format,
missingval='<null>')
if (max_width and rows and
content_exceeds_width(frows[0], max_width) and
headers):
output.append(expanded_table(rows, headers))
else:
output.append(tabulated)
if status: # Only print the status if it's not None.
output.append(status)

return output

def content_exceeds_width(row, width):
# Account for 3 characters between each column
separator_space = (len(row)*3)
Expand Down
11 changes: 6 additions & 5 deletions mycli/myclirc
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ log_level = INFO
# Timing of sql statments and table rendering.
timing = True

# Table format. Possible values: psql, plain, simple, grid, fancy_grid, pipe,
# orgtbl, rst, mediawiki, html, latex, latex_booktabs, tsv.
# Recommended: psql, fancy_grid and grid.
table_format = psql
# Table format. Possible values: ascii, double, github,
# psql, plain, simple, grid, fancy_grid, pipe, orgtbl, rst, mediawiki, html,
# latex, latex_booktabs, textile, moinmoin, jira, expanded, tsv, csv.
# Recommended: ascii
table_format = ascii

# Syntax coloring style. Possible values (many support the "-dark" suffix):
# manni, igor, xcode, vim, autumn, vs, rrt, native, perldoc, borland, tango, emacs,
Expand Down Expand Up @@ -66,7 +67,7 @@ less_chatty = False
login_path_as_host = False

# Cause result sets to be displayed vertically if they are too wide for the current window,
# and using normal tabular format otherwise. (This applies to statements terminated by ; or \G.)
# and using normal tabular format otherwise. (This applies to statements terminated by ; or \G.)
auto_vertical_output = False

# Custom colors for the completion menu, toolbar, etc.
Expand Down
Empty file.
Loading