diff --git a/.travis.yml b/.travis.yml index ccb0641..a5cd9b3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ python: - "2.7" - "3.4" - "3.5" + - "3.6" install: - sudo apt-get update - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then diff --git a/Makefile b/Makefile index 6846c6a..8fa9d46 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,6 @@ test: nosetests -v --with-coverage --cover-package=tableprint --logging-level=INFO clean: - rm -rf tableprint.egg-info - rm -f *.pyc - rm -rf __pycache__ + rm -R tableprint.egg-info + rm -f tableprint/*.pyc + rm -R tableprint/__pycache__/ diff --git a/README.md b/README.md index a3fea49..749b16d 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ Hosted at Read The Docs: [tableprint.readthedocs.org](http://tableprint.readthed - `six` ## Version +- 0.6.9 (May 25 2017) Splitting the tableprint.py module into a pacakge with multiple files +- 0.6.7 (May 25 2017) Fixes some bugs with ANSI escape sequences - 0.5.0 (Sept 29 2016) Better handling of ANSI escape sequences in table rows - 0.4.0 (May 3 2016) Adds a 'block' style - 0.3.2 (May 3 2016) Adds a test suite diff --git a/setup.py b/setup.py index bab43c2..73fe1a5 100644 --- a/setup.py +++ b/setup.py @@ -1,29 +1,29 @@ +import re +import os from setuptools import setup + +__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) +with open(os.path.join(__location__, 'tableprint/metadata.py'), 'r') as f: + metadata = dict(re.findall("__([a-z_]+)__\s*=\s*'([^']+)'", f.read())) + + setup( name='tableprint', + url=metadata['url'], + version=metadata['version'], - # Versions should comply with PEP440. For a discussion on single-sourcing - # the version across setup.py and the project code, see - # https://packaging.python.org/en/latest/single_source_version.html - version='0.6.1', + author=metadata['author'], + author_email=metadata['author_email'], - description='Pretty console printing of tabular data', + license=metadata['license'], + + description=metadata['description'], long_description='''Formatted console printing of tabular data. tableprint lets you easily print formatted tables of data. Unlike other modules, you can print single rows of data at a time (useful for printing ongoing computation results).''', - # The project's main homepage. - url='https://github.com/nirum/tableprint', - - # Author details - author='Niru Maheswaranathan', - author_email='niru@fastmail.com', - - # Choose your license - license='MIT', - # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ # How mature is this project? Common values are @@ -44,6 +44,7 @@ setup( 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', ], # What does your project relate to? @@ -51,8 +52,7 @@ setup( # You can just specify the packages manually here if your project is # simple. Or you can use find_packages(). - packages=[], - py_modules=['tableprint'], + packages=['tableprint'], # List run-time dependencies here. These will be installed by pip when your # project is installed. For an analysis of "install_requires" vs pip's @@ -67,5 +67,4 @@ setup( 'dev': [], 'test': ['pytest', 'coverage'], }, - ) diff --git a/tableprint/__init__.py b/tableprint/__init__.py new file mode 100644 index 0000000..9ad1910 --- /dev/null +++ b/tableprint/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +""" +Tableprint +""" +from .metadata import __author__, __version__ +from .printer import * +from .style import * +from .utils import * diff --git a/tableprint/metadata.py b/tableprint/metadata.py new file mode 100644 index 0000000..a0a705b --- /dev/null +++ b/tableprint/metadata.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + +# Version info +__version__ = '0.6.9' +__license__ = 'MIT' + +# Project description(s) +__description__ = 'Pretty console printing of tabular data' + +# The project's main homepage. +__url__ = 'https://github.com/nirum/tableprint' + +# Author details +__author__ = 'Niru Maheswaranathan' +__author_email__ = 'niru@fastmail.com' diff --git a/tableprint.py b/tableprint/printer.py similarity index 58% rename from tableprint.py rename to tableprint/printer.py index 7735006..1ff16c0 100644 --- a/tableprint.py +++ b/tableprint/printer.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -Tableprint +Table printing A module to print and display formatted tables of data @@ -11,58 +11,18 @@ Usage >>> tableprint.table(data, headers) """ from __future__ import print_function, unicode_literals -from six import string_types -from collections import namedtuple -from numbers import Number + import sys -import re +from numbers import Number + import numpy as np +from six import string_types -__all__ = ('table', 'header', 'row', 'hr', 'top', 'bottom', - 'banner', 'dataframe', 'humantime', 'styles') -__version__ = '0.5.4' +from .style import LineStyle, STYLES +from .utils import ansi_len, format_line + +__all__ = ('table', 'header', 'row', 'hrule', 'top', 'bottom', 'banner', 'dataframe') -# set up table styles -LineStyle = namedtuple('LineStyle', ('begin', 'hline', 'sep', 'end')) -TableStyle = namedtuple('TableStyle', ('top', 'below_header', 'bottom', 'row')) -styles = { - 'grid': TableStyle( - top=LineStyle('+', '-', '+', '+'), - below_header=LineStyle('+', '-', '+', '+'), - bottom=LineStyle('+', '-', '+', '+'), - row=LineStyle('|', '', '|', '|'), - ), - 'fancy_grid': TableStyle( - top=LineStyle('╒', '═', '╤', '╕'), - below_header=LineStyle('╞', '═', '╪', '╡'), - bottom=LineStyle("╘", "═", "╧", "╛"), - row=LineStyle('│', '', '│', '│'), - ), - 'clean': TableStyle( - top=LineStyle(' ', '─', ' ', ' '), - below_header=LineStyle(' ', '─', ' ', ' '), - bottom=LineStyle(" ", "─", " ", " "), - row=LineStyle(' ', '', ' ', ' '), - ), - 'round': TableStyle( - top=LineStyle('╭', '─', '┬', '╮'), - below_header=LineStyle('├', '─', '┼', '┤'), - bottom=LineStyle('╰', '─', '┴', '╯'), - row=LineStyle('│', '', '│', '│'), - ), - 'banner': TableStyle( - top=LineStyle('╒', '═', '╤', '╕'), - below_header=LineStyle("╘", "═", "╧", "╛"), - bottom=LineStyle("╘", "═", "╧", "╛"), - row=LineStyle('│', '', '│', '│'), - ), - 'block': TableStyle( - top=LineStyle('◢', '■', '■', '◣'), - below_header=LineStyle(' ', '━', '━', ' '), - bottom=LineStyle('◥', '■', '■', '◤'), - row=LineStyle(' ', '', ' ', ' '), - ), -} STYLE = 'round' WIDTH = 11 FMT = '5g' @@ -130,10 +90,10 @@ def table(data, headers=None, format_spec=FMT, width=WIDTH, style=STYLE, out=sys A file handle or object that has write() and flush() methods (Default: sys.stdout) """ ncols = len(data[0]) if headers is None else len(headers) - tablestyle = styles[style] + tablestyle = STYLES[style] # Initialize with a hr or the header - tablestr = [hr(ncols, width, tablestyle.top)] \ + tablestr = [hrule(ncols, width, tablestyle.top)] \ if headers is None else [header(headers, width, style)] # parse each row @@ -141,7 +101,7 @@ def table(data, headers=None, format_spec=FMT, width=WIDTH, style=STYLE, out=sys # only add the final border if there was data in the table if len(data) > 0: - tablestr += [hr(ncols, width, tablestyle.bottom)] + tablestr += [hrule(ncols, width, tablestyle.bottom)] # print the table out.write('\n'.join(tablestr) + '\n') @@ -160,24 +120,24 @@ def header(headers, width=WIDTH, style=STYLE, add_hr=True): The width of each column (Default: 11) style : string or tuple, optional - A formatting style (see styles) + A formatting style (see STYLES) Returns ------- headerstr : string A string consisting of the full header row to print """ - tablestyle = styles[style] + tablestyle = STYLES[style] # string formatter - data = map(lambda x: ('{:^%d}' % (width + _ansi_len(x))).format(x), headers) + data = map(lambda x: ('{:^%d}' % (width + ansi_len(x))).format(x), headers) # build the formatted str - headerstr = _format_line(data, tablestyle.row) + headerstr = format_line(data, tablestyle.row) if add_hr: - upper = hr(len(headers), width, tablestyle.top) - lower = hr(len(headers), width, tablestyle.below_header) + upper = hrule(len(headers), width, tablestyle.top) + lower = hrule(len(headers), width, tablestyle.below_header) headerstr = '\n'.join([upper, headerstr, lower]) return headerstr @@ -205,9 +165,9 @@ def row(values, width=WIDTH, format_spec=FMT, style=STYLE): rowstr : string A string consisting of the full row of data to print """ - tablestyle = styles[style] + tablestyle = STYLES[style] - assert isinstance(format_spec, string_types) | (type(format_spec) is list), \ + assert isinstance(format_spec, string_types) | isinstance(format_spec, list), \ "format_spec must be a string or list of strings" if isinstance(format_spec, string_types): @@ -220,7 +180,7 @@ def row(values, width=WIDTH, format_spec=FMT, style=STYLE): datum, prec = val if isinstance(datum, string_types): - return ('{:>%i}' % (width + _ansi_len(datum))).format(datum) + return ('{:>%i}' % (width + ansi_len(datum))).format(datum) elif isinstance(datum, Number): return ('{:>%i.%s}' % (width, prec)).format(datum) @@ -232,10 +192,10 @@ def row(values, width=WIDTH, format_spec=FMT, style=STYLE): data = map(mapdata, zip(values, format_spec)) # build the row string - return _format_line(data, tablestyle.row) + return format_line(data, tablestyle.row) -def hr(n, width=WIDTH, linestyle=LineStyle('', '─', '─', '')): +def hrule(n=1, width=WIDTH, linestyle=LineStyle('', '─', '─', '')): """Returns a formatted string used as a border between table rows Parameters @@ -261,12 +221,12 @@ def hr(n, width=WIDTH, linestyle=LineStyle('', '─', '─', '')): def top(n, width=WIDTH, style=STYLE): """Prints the top row of a table""" - return hr(n, width, linestyle=styles[style].top) + return hrule(n, width, linestyle=STYLES[style].top) def bottom(n, width=WIDTH, style=STYLE): """Prints the top row of a table""" - return hr(n, width, linestyle=styles[style].bottom) + return hrule(n, width, linestyle=STYLES[style].bottom) def banner(message, width=30, style='banner', out=sys.stdout): @@ -299,70 +259,3 @@ def dataframe(df, **kwargs): A pandas DataFrame with the table to print """ table(np.array(df), list(df.columns), **kwargs) - - -def humantime(t): - """Converts a time in seconds to a reasonable human readable time - - Parameters - ---------- - t : float - The number of seconds - - Returns - ------- - time : string - The human readable formatted value of the given time - """ - try: - t = float(t) - except (ValueError, TypeError): - raise ValueError("Input must be numeric") - - # weeks - if t >= 7*60*60*24: - weeks = np.floor(t / (7.*60.*60.*24.)) - timestr = "{:g} weeks, ".format(weeks) + humantime(t % (7*60*60*24)) - - # days - elif t >= 60*60*24: - days = np.floor(t / (60.*60.*24.)) - timestr = "{:g} days, ".format(days) + humantime(t % (60*60*24)) - - # hours - elif t >= 60*60: - hours = np.floor(t / (60.*60.)) - timestr = "{:g} hours, ".format(hours) + humantime(t % (60*60)) - - # minutes - elif t >= 60: - minutes = np.floor(t / 60.) - timestr = "{:g} min., ".format(minutes) + humantime(t % 60) - - # seconds - elif (t >= 1) | (t == 0): - timestr = "{:g} s".format(t) - - # milliseconds - elif t >= 1e-3: - timestr = "{:g} ms".format(t*1e3) - - # microseconds - elif t >= 1e-6: - timestr = "{:g} \u03BCs".format(t*1e6) - - # nanoseconds or smaller - else: - timestr = "{:g} ns".format(t*1e9) - - return timestr - - -def _ansi_len(string): - """Extra length due to any ANSI sequences in the string.""" - return len(string) - len(re.compile(r'\x1b[^m]*m').sub('', string)) - - -def _format_line(data, linestyle): - """Formats a list of elements using the given line style""" - return linestyle.begin + linestyle.sep.join(data) + linestyle.end diff --git a/tableprint/style.py b/tableprint/style.py new file mode 100644 index 0000000..35dc92d --- /dev/null +++ b/tableprint/style.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +""" +Table styles +""" +from __future__ import print_function, unicode_literals +from collections import namedtuple + +__all__ = ('STYLES', 'LineStyle', 'TableStyle') + +LineStyle = namedtuple('LineStyle', ('begin', 'hline', 'sep', 'end')) +TableStyle = namedtuple('TableStyle', ('top', 'below_header', 'bottom', 'row')) +STYLES = { + 'grid': TableStyle( + top=LineStyle('+', '-', '+', '+'), + below_header=LineStyle('+', '-', '+', '+'), + bottom=LineStyle('+', '-', '+', '+'), + row=LineStyle('|', '', '|', '|'), + ), + 'fancy_grid': TableStyle( + top=LineStyle('╒', '═', '╤', '╕'), + below_header=LineStyle('╞', '═', '╪', '╡'), + bottom=LineStyle("╘", "═", "╧", "╛"), + row=LineStyle('│', '', '│', '│'), + ), + 'clean': TableStyle( + top=LineStyle(' ', '─', ' ', ' '), + below_header=LineStyle(' ', '─', ' ', ' '), + bottom=LineStyle(" ", "─", " ", " "), + row=LineStyle(' ', '', ' ', ' '), + ), + 'round': TableStyle( + top=LineStyle('╭', '─', '┬', '╮'), + below_header=LineStyle('├', '─', '┼', '┤'), + bottom=LineStyle('╰', '─', '┴', '╯'), + row=LineStyle('│', '', '│', '│'), + ), + 'banner': TableStyle( + top=LineStyle('╒', '═', '╤', '╕'), + below_header=LineStyle("╘", "═", "╧", "╛"), + bottom=LineStyle("╘", "═", "╧", "╛"), + row=LineStyle('│', '', '│', '│'), + ), + 'block': TableStyle( + top=LineStyle('◢', '■', '■', '◣'), + below_header=LineStyle(' ', '━', '━', ' '), + bottom=LineStyle('◥', '■', '■', '◤'), + row=LineStyle(' ', '', ' ', ' '), + ), +} diff --git a/tableprint/utils.py b/tableprint/utils.py new file mode 100644 index 0000000..9dd4e1d --- /dev/null +++ b/tableprint/utils.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +""" +Tableprint utilities +""" +from __future__ import print_function, unicode_literals +import re +import numpy as np + +__all__ = ('humantime',) + + +def humantime(time): + """Converts a time in seconds to a reasonable human readable time + + Parameters + ---------- + t : float + The number of seconds + + Returns + ------- + time : string + The human readable formatted value of the given time + """ + try: + time = float(time) + except (ValueError, TypeError): + raise ValueError("Input must be numeric") + + # weeks + if time >= 7 * 60 * 60 * 24: + weeks = np.floor(time / (7 * 60 * 60 * 24)) + timestr = "{:g} weeks, ".format(weeks) + humantime(time % (7 * 60 * 60 * 24)) + + # days + elif time >= 60 * 60 * 24: + days = np.floor(time / (60 * 60 * 24)) + timestr = "{:g} days, ".format(days) + humantime(time % (60 * 60 * 24)) + + # hours + elif time >= 60 * 60: + hours = np.floor(time / (60 * 60)) + timestr = "{:g} hours, ".format(hours) + humantime(time % (60 * 60)) + + # minutes + elif time >= 60: + minutes = np.floor(time / 60.) + timestr = "{:g} min., ".format(minutes) + humantime(time % 60) + + # seconds + elif (time >= 1) | (time == 0): + timestr = "{:g} s".format(time) + + # milliseconds + elif time >= 1e-3: + timestr = "{:g} ms".format(time * 1e3) + + # microseconds + elif time >= 1e-6: + timestr = "{:g} \u03BCs".format(time * 1e6) + + # nanoseconds or smaller + else: + timestr = "{:g} ns".format(time * 1e9) + + return timestr + + +def ansi_len(string): + """Extra length due to any ANSI sequences in the string.""" + return len(string) - len(re.compile(r'\x1b[^m]*m').sub('', string)) + + +def format_line(data, linestyle): + """Formats a list of elements using the given line style""" + return linestyle.begin + linestyle.sep.join(data) + linestyle.end diff --git a/tests/test_functions.py b/tests/test_functions.py index f14755b..35bbaa2 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -5,6 +5,7 @@ import pytest def test_borders(): + """Tests printing of the top and bottom borders""" # top assert top(5, width=2, style='round') == '╭──┬──┬──┬──┬──╮' @@ -16,11 +17,12 @@ def test_borders(): def test_row(): + """Tests printing of a single row of data""" # valid assert row("abc", width=3, style='round') == '│ a│ b│ c│' assert row([1, 2, 3], width=3, style='clean') == ' 1 2 3 ' # invalid - with pytest.raises(ValueError) as context: + with pytest.raises(ValueError): row([{}]) diff --git a/tests/test_io.py b/tests/test_io.py index b443eda..ac7b5fd 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from tableprint import table, banner, dataframe, hr +from tableprint import table, banner, dataframe, hrule from io import StringIO import numpy as np @@ -23,6 +23,7 @@ def test_frame(): def __init__(self, data, headers): self.data = data self.columns = headers + def __array__(self): return self.data @@ -44,8 +45,8 @@ def test_banner(): assert output.getvalue() == '╒═╕\n│!│\n╘═╛\n' -def test_hr(): +def test_hrule(): - output = hr(1, width=11) + output = hrule(1, width=11) assert len(output) == 11 assert '───────────' diff --git a/tests/test_utils.py b/tests/test_utils.py index 3a83ae2..92e3cc9 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,20 +1,21 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from tableprint import humantime, _format_line, LineStyle +from tableprint import humantime, LineStyle +from tableprint.utils import format_line import pytest def test_format_line(): # using ASCII - assert _format_line(['foo', 'bar'], LineStyle('(', '_', '+', ')')) == '(foo+bar)' - assert _format_line("abc", LineStyle('[', '*', '.', ']')) == '[a.b.c]' - assert _format_line(["_"], LineStyle('o', '', '!', 'o')) == 'o_o' - assert _format_line([], LineStyle(':', '', '', ')')) == ':)' + assert format_line(['foo', 'bar'], LineStyle('(', '_', '+', ')')) == '(foo+bar)' + assert format_line("abc", LineStyle('[', '*', '.', ']')) == '[a.b.c]' + assert format_line(["_"], LineStyle('o', '', '!', 'o')) == 'o_o' + assert format_line([], LineStyle(':', '', '', ')')) == ':)' # using unicode - assert _format_line(['.', '.', '.'], LineStyle('★', '_', '╳', '☆')) == '★.╳.╳.☆' - assert _format_line("☚☛", LineStyle('♪', '*', '♩', '♫')) == '♪☚♩☛♫' + assert format_line(['.', '.', '.'], LineStyle('★', '_', '╳', '☆')) == '★.╳.╳.☆' + assert format_line("☚☛", LineStyle('♪', '*', '♩', '♫')) == '♪☚♩☛♫' def test_humantime():