diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8968d1e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +lazy_table.egg-info diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f3c4cc5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2021 Parsiad Azimzadeh + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cfa4a66 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +

+ lazy_table +

+ +A [python-tabulate](https://github.com/astanin/python-tabulate) wrapper for producing tables from generators. + +## Motivation + +lazy_table is useful when (*i*) each row of your table is generated by a possibly expensive computation and (*ii*) you want to print rows to the screen as soon as they are available. + +For example, the rows in the table below correspond to [numerically solving an ordinary differential equation](https://en.wikipedia.org/wiki/Numerical_methods_for_ordinary_differential_equations) with progressively more steps. See [examples/ode.py](https://raw.githubusercontent.com/parsiad/lazy-table/master/examples/ode.py) for the code to generate this table. + +![](https://raw.githubusercontent.com/parsiad/lazy-table/master/examples/ode.gif) + +## Installation and usage + +Install lazy_table via pip: + +```console +$ pip install lazy_table +``` + +In your Python code, add + +```python +import lazy_table as lt +``` + +Now, generating your own lazy table is as simple as calling `lt.stream` on a generator that yields one or more lists, where each list is a row of your table (see the example below). + +## Example + +As a toy example, consider printing the first 10 [Fibonacci numbers](https://en.wikipedia.org/wiki/Fibonacci_number) in a table. +Since this is a relatively cheap operation, we will pretend it is expensive by adding an artificial `sleep(1)` call beteween Fibonacci numbers. + +```python +import time +import lazy_table as lt + +def fib_table(n): + x0, x1 = 0, 1 + yield [0, x0] + yield [1, x1] + for i in range(2, n + 1): + x0, x1 = x1, x0 + x1 + yield [i, x1] + time.sleep(1) # Simulate work + +lt.stream(fib_table(10), headers=['N', 'F_N']) +``` + +Your final table should look like this: + +``` + N F_N +--- ----- + 0 0 + 1 1 + 2 1 + 3 2 + 4 3 + 5 5 + 6 8 + 7 13 + 8 21 + 9 34 + 10 55 +``` diff --git a/examples/ode.gif b/examples/ode.gif new file mode 100755 index 0000000..754bda2 Binary files /dev/null and b/examples/ode.gif differ diff --git a/examples/ode.py b/examples/ode.py new file mode 100755 index 0000000..5c8775f --- /dev/null +++ b/examples/ode.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +import lazy_table as lt +import numpy as np + + +def f(y): + return np.array([y[1], (1. - y[0]**2) * y[1] - y[0]]) + + +def solve(n_steps, t1=16., y0=(2., 0.)): + h = t1 / n_steps + y = np.array(y0) + for _ in range(n_steps): + y = y + h * f(y) + return y + + +def convergence_table(init_n_steps=200): + prev_value = np.nan + prev_delta = np.nan + for n_steps in init_n_steps * 2**np.arange(0, 12): + y = solve(n_steps) + value = y[0] + delta = value - prev_value + ratio = prev_delta / delta + order = np.log2(ratio) + prev_value = value + prev_delta = delta + yield [n_steps, value, delta, order] + + +lt.stream( + convergence_table(), + tablefmt='simple', + headers=['Steps', 'Value', 'Delta', 'Order'], +) diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..885d22b Binary files /dev/null and b/logo.png differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..374b58c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..ce94745 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,26 @@ +[metadata] +name = lazy-table +version = 0.1.0 +author = Parsiad Azimzadeh +author_email = parsiad.azimzadeh@gmail.com +description = A python-tabulate wrapper for producing tables from generators +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/parsiad/lazy-table +project_urls = + Bug Tracker = https://github.com/parsiad/lazy-table/issues +classifiers = + Programming Language :: Python :: 3 + License :: OSI Approved :: MIT License + Operating System :: OS Independent + +[options] +install_requires = + tabulate +package_dir = + = src +packages = find: +python_requires = >= 3.6 + +[options.packages.find] +where = src diff --git a/src/lazy_table/__init__.py b/src/lazy_table/__init__.py new file mode 100644 index 0000000..dbe5b05 --- /dev/null +++ b/src/lazy_table/__init__.py @@ -0,0 +1,5 @@ +"""A python-tabulate wrapper for producing tables from generators.""" + +__all__ = ['ConsoleArtist', 'stream'] + +from ._lazy_table import ConsoleArtist, stream diff --git a/src/lazy_table/_lazy_table.py b/src/lazy_table/_lazy_table.py new file mode 100644 index 0000000..19fc3d5 --- /dev/null +++ b/src/lazy_table/_lazy_table.py @@ -0,0 +1,67 @@ +import sys + +from tabulate import tabulate + + +class ConsoleArtist: + """Renders a table to the console. + + Parameters + ---------- + clear : bool + Clears the screen every time the table is updated. + out : TextIOBase + Text stream to write to. If unspecified, ``sys.stdout`` is used. + """ + def __init__(self, clear=False, out=sys.stdout): + self._clear = clear + self._n_lines = 0 + self._out = out + + def __call__(self, result): + if self._clear: + self._out.write('\x1b[2J') + else: + self._out.write('\033[F\033[K' * self._n_lines) + self._n_lines = result.count('\n') + 1 + self._out.write(result) + self._out.write('\n') + self._out.flush() + + +def stream(table, artist=None, **kwargs): + """Streams a table. + + kwargs are forwarded to tabulate. + + Examples + -------- + >>> import time + >>> import lazy_table as lt + >>> + >>> def fib_table(n): + ... x0, x1 = 0, 1 + ... yield [0, x0] + ... yield [1, x1] + ... for i in range(2, n + 1): + ... x0, x1 = x1, x0 + x1 + ... yield [i, x1] + ... time.sleep(1) # Simulate work + >>> + >>> lt.stream(fib_table(10), headers=['N', 'F_N']) + + Parameters + ---------- + table : Generator[List[T], None, None] + A generator which yields rows of the table. + artist : callable, optional + A callable of the form ``draw(string)`` which determines how to render the table. If unspecified, + ``lazy_table.Console`` is used. + """ + if artist is None: + artist = ConsoleArtist() + rows = [] + for row in table: + rows.append(row) + t = tabulate(rows, **kwargs) + artist(t)