From 61b209f39ea003b03ecff31636953cfb78d204e1 Mon Sep 17 00:00:00 2001 From: Tammo Ippen Date: Thu, 28 Mar 2019 23:09:27 +0100 Subject: [PATCH] Remove hard dependency on pendulum (#20) --- .circleci/config.yml | 15 ++++++-- .gitignore | 3 +- README.md | 4 +-- plotille/_figure.py | 39 ++++++++++---------- plotille/_input_formatter.py | 36 ++++++++++++++----- plotille/_util.py | 62 ++++++++++++++++++++++---------- pyproject.toml | 4 +-- setup.cfg | 7 ++++ tests/test_canvas.py | 3 +- tests/test_colors.py | 3 +- tests/test_datetime_formatter.py | 3 +- tests/test_dots.py | 3 +- tests/test_examples.py | 3 +- tests/test_figure.py | 20 +++++++++-- tests/test_hist.py | 16 ++++++++- tests/test_input_formatter.py | 1 + tests/test_numberformats.py | 3 +- 17 files changed, 162 insertions(+), 63 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b42d8c3..aaee5c8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -87,7 +87,7 @@ jobs: test_2_7_pypy: executor: name: python - image: "pypy:2-6.0.0" + image: "pypy:2-7.1.0" steps: - tester test_3_5: @@ -99,7 +99,7 @@ jobs: test_3_5_pypy: executor: name: python - image: "pypy:3-6.0.0" + image: "pypy:3.5-7.0.0" steps: - tester test_3_6: @@ -108,6 +108,12 @@ jobs: image: "circleci/python:3.6.7" steps: - tester + test_3_6_pypy: + executor: + name: python + image: "pypy:3.6-7.1.0" + steps: + - tester test_3_7: executor: name: python @@ -159,6 +165,10 @@ workflows: filters: tags: only: /.*/ + - test_3_6_pypy: + filters: + tags: + only: /.*/ - test_3_7: filters: tags: @@ -170,6 +180,7 @@ workflows: - test_3_5 - test_3_5_pypy - test_3_6 + - test_3_6_pypy - test_3_7 filters: branches: diff --git a/.gitignore b/.gitignore index 70d5590..d8bbb40 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ venv/ -.venv/ +.venv*/ .vscode/ data/ build/ @@ -18,3 +18,4 @@ README.rst poetry.lock Pipfile.lock cov_html/ +pip-wheel-metadata/ diff --git a/README.md b/README.md index 3ad0bb6..60524fc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Coverage Status](https://coveralls.io/repos/github/tammoippen/plotille/badge.svg?branch=master)](https://coveralls.io/github/tammoippen/plotille?branch=master) [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/tammoippen/plotille.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/tammoippen/plotille/context:python) [![Tested CPython Versions](https://img.shields.io/badge/cpython-2.7%2C%203.5%2C%203.6%2C%203.7-brightgreen.svg)](https://img.shields.io/badge/cpython-2.7%2C%203.5%2C%203.6%2C%203.7-brightgreen.svg) -[![Tested PyPy Versions](https://img.shields.io/badge/pypy-2.7--6.0.0%2C%203.5--6.0.0-brightgreen.svg)](https://img.shields.io/badge/pypy-2.7--6.0.0%2C%203.5--6.0.0-brightgreen.svg) +[![Tested PyPy Versions](https://img.shields.io/badge/pypy-2.7--7.1.0%2C%203.5--7.0.0%2C%203.6--7.1.0-brightgreen.svg)](https://img.shields.io/badge/pypy-2.7--6.0.0%2C%203.5--6.0.0-brightgreen.svg) [![PyPi version](https://img.shields.io/pypi/v/plotille.svg)](https://pypi.python.org/pypi/plotille) [![PyPi license](https://img.shields.io/pypi/l/plotille.svg)](https://pypi.python.org/pypi/plotille) @@ -23,7 +23,7 @@ Similar to other libraries: * like [termgraph](https://github.com/sgeisler/termgraph) (not on pypi), but very different style. * like [terminalplot](https://github.com/kressi/terminalplot), but with braille, X/Y-axis, histogram, linear interpolation. -Basic support for timeseries plotting is provided with release 3.2: for any `X` or `Y` values you can also add `datetime.datetime` or `pendulum.datetime` values (internally I work with `pendulum.datetime`, as the python 2/3 consistency is much better). Labels are generated respecting the difference of of `x_limits` and `y_limits`. +Basic support for timeseries plotting is provided with release 3.2: for any `X` or `Y` values you can also add `datetime.datetime`, `pendulum.datetime` or `numpy.datetime64` values. Labels are generated respecting the difference of `x_limits` and `y_limits`. ## Documentation diff --git a/plotille/_figure.py b/plotille/_figure.py index 6cb5d33..6320f90 100644 --- a/plotille/_figure.py +++ b/plotille/_figure.py @@ -24,7 +24,7 @@ # THE SOFTWARE. from collections import namedtuple -from datetime import datetime +from datetime import timedelta from itertools import cycle import os @@ -33,7 +33,7 @@ from ._canvas import Canvas from ._colors import color from ._input_formatter import InputFormatter -from ._util import dt2pendulum_dt, hist, is_datetimes, make_datetimes +from ._util import hist, mk_timedelta, timestamp # TODO documentation!!! # TODO tests @@ -151,12 +151,6 @@ def set_y_limits(self, min_=None, max_=None): self._y_min, self._y_max = self._set_limits(self._y_min, self._y_max, min_, max_) def _set_limits(self, init_min, init_max, min_=None, max_=None): - if isinstance(min_, datetime): - min_ = dt2pendulum_dt(min_) - - if isinstance(max_, datetime): - max_ = dt2pendulum_dt(max_) - if min_ is not None and max_ is not None: if min_ >= max_: raise ValueError('min_ is larger or equal than max_.') @@ -196,7 +190,11 @@ def _limits(self, low_set, high_set, is_height): return _choose(low, high, low_set, high_set) def _y_axis(self, ymin, ymax, label='Y'): - y_delta = abs((ymax - ymin) / self.height) + delta = abs(ymax - ymin) + if isinstance(delta, timedelta): + y_delta = mk_timedelta(timestamp(delta) / self.height) + else: + y_delta = delta / self.height res = [self._in_fmt.fmt(i * y_delta + ymin, abs(ymax - ymin), chars=10) + ' | ' for i in range(self.height)] @@ -211,14 +209,18 @@ def _y_axis(self, ymin, ymax, label='Y'): return list(reversed(res)) def _x_axis(self, xmin, xmax, label='X', with_y_axis=False): - x_delta = abs((xmax - xmin) / self.width) + delta = abs(xmax - xmin) + if isinstance(delta, timedelta): + x_delta = mk_timedelta(timestamp(delta) / self.width) + else: + x_delta = delta / self.width starts = ['', ''] if with_y_axis: starts = ['-' * 11 + '|-', ' ' * 11 + '| '] res = [] res += [starts[0] + '|---------' * (self.width // 10) + '|-> (' + label + ')'] - res += [starts[1] + ' '.join(self._in_fmt.fmt(i * 10 * x_delta + xmin, abs(xmax - xmin), left=True, chars=9) + res += [starts[1] + ' '.join(self._in_fmt.fmt(i * 10 * x_delta + xmin, delta, left=True, chars=9) for i in range(self.width // 10 + 1))] return res @@ -302,12 +304,6 @@ def create(cls, X, Y, lc, interp, label): # noqa: N803 if interp not in ('linear', None): raise ValueError('Only "linear" and None are allowed values for `interp`.') - if is_datetimes(X): - X = make_datetimes(X) # noqa: N806 - - if is_datetimes(Y): - Y = make_datetimes(Y) # noqa: N806 - return cls(X, Y, lc, interp, label) def width_vals(self): @@ -337,9 +333,6 @@ def write(self, canvas, with_colors, in_fmt): class Histogram(namedtuple('Histogram', ['X', 'bins', 'frequencies', 'buckets', 'lc'])): @classmethod def create(cls, X, bins, lc): # noqa: N803 - if is_datetimes(X): - X = make_datetimes(X) # noqa: N806 - frequencies, buckets = hist(X, bins) return cls(X, bins, frequencies, buckets, lc) @@ -387,7 +380,11 @@ def _diff(low, high): else: return abs(low * 0.1) else: - return abs(high - low) * 0.1 + delta = abs(high - low) + if isinstance(delta, timedelta): + return mk_timedelta(timestamp(delta) * 0.1) + else: + return delta * 0.1 def _default(low_set, high_set): diff --git a/plotille/_input_formatter.py b/plotille/_input_formatter.py index fe9b3b2..6ec61bc 100644 --- a/plotille/_input_formatter.py +++ b/plotille/_input_formatter.py @@ -24,12 +24,12 @@ # THE SOFTWARE. from collections import OrderedDict +from datetime import datetime, timedelta import math -from pendulum import DateTime, Duration, Period import six -from ._util import roundeven +from ._util import roundeven, timestamp class InputFormatter(object): @@ -40,14 +40,22 @@ def __init__(self): for int_type in six.integer_types: self.formatters[int_type] = _num_formatter - self.formatters[DateTime] = _datetime_formatter + self.formatters[datetime] = _datetime_formatter self.converters = OrderedDict() self.converters[float] = _convert_numbers for int_type in six.integer_types: self.converters[int_type] = _convert_numbers - self.converters[DateTime] = _convert_datetime + self.converters[datetime] = _convert_datetime + + try: + import numpy as np + + self.converters[np.datetime64] = _convert_np_datetime + self.formatters[np.datetime64] = _np_datetime_formatter + except ImportError: # pragma: nocover + pass def register_formatter(self, t, f): self.formatters[t] = f @@ -70,9 +78,16 @@ def convert(self, val): return val +def _np_datetime_formatter(val, chars, delta, left=False): + # assert isinstance(val, np.datetime64) + # assert isinstance(delta, np.timedelta64) + + return _datetime_formatter(val.item(), chars, delta.item(), left) + + def _datetime_formatter(val, chars, delta, left=False): - assert isinstance(val, DateTime) - assert isinstance(delta, (Duration, Period)) + assert isinstance(val, datetime) + assert isinstance(delta, timedelta) if chars < 8: raise ValueError('Not possible to display value "{}" with {} characters!'.format(val, chars)) @@ -174,6 +189,11 @@ def _convert_numbers(v): return float(v) +def _convert_np_datetime(v): + # assert isinstance(v, np.datetime64) + return timestamp(v.item()) + + def _convert_datetime(v): - assert isinstance(v, DateTime) - return v.timestamp() + assert isinstance(v, datetime) + return timestamp(v) diff --git a/plotille/_util.py b/plotille/_util.py index 0b87513..d611aed 100644 --- a/plotille/_util.py +++ b/plotille/_util.py @@ -23,10 +23,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -import datetime +from datetime import datetime, timedelta, tzinfo import math - -import pendulum +import time def roundeven(x): @@ -65,32 +64,59 @@ def hist(X, bins): # noqa: N803 """ assert bins > 0 - if is_datetimes(X): - X = make_datetimes(X) # noqa: N806 - xmin = min(X) if len(X) > 0 else 0.0 xmax = max(X) if len(X) > 0 else 1.0 - xwidth = (xmax - xmin) / bins + delta = xmax - xmin + is_datetime = False + if isinstance(delta, timedelta): + is_datetime = True + delta = timestamp(delta) + + xwidth = delta / bins y = [0] * bins for x in X: - x_idx = min(bins - 1, int((x - xmin) // xwidth)) + delta = (x - xmin) + if isinstance(delta, timedelta): + delta = timestamp(delta) + x_idx = min(bins - 1, int(delta // xwidth)) y[x_idx] += 1 + if is_datetime: + xwidth = mk_timedelta(xwidth) + return y, [i * xwidth + xmin for i in range(bins + 1)] -def make_datetimes(l): - return [dt2pendulum_dt(dt) for dt in l] +class _UTC(tzinfo): + """UTC""" + _ZERO = timedelta(0) + + def utcoffset(self, dt): + return self._ZERO + + def tzname(self, dt): + return 'UTC' + + def dst(self, dt): + return self._ZERO + + +_EPOCH = datetime(1970, 1, 1, tzinfo=_UTC()) -def is_datetimes(l): - return ( - all(isinstance(x, datetime.datetime) for x in l) and # all are datetimes, - any(not isinstance(x, pendulum.DateTime) for x in l) # but at least one is not pendulum datetime - ) +def timestamp(v): + """Get timestamp of `v` datetime in py2/3.""" + if isinstance(v, datetime): + if v.tzinfo is None: + return time.mktime(v.timetuple()) + v.microsecond / 1e6 + else: + return (v - _EPOCH).total_seconds() + elif isinstance(v, timedelta): + return v.total_seconds() -def dt2pendulum_dt(dt): - assert isinstance(dt, datetime.datetime) # also works on pendulum datetimes - return pendulum.datetime(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond, dt.tzinfo) +def mk_timedelta(v): + seconds = int(v) + microseconds = int((v - seconds) * 1e6) + return timedelta(seconds=seconds, microseconds=microseconds) diff --git a/pyproject.toml b/pyproject.toml index 6ee74d5..75bc15a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "plotille" -version = "3.5" +version = "3.6" description = "Plot in the terminal using braille dots." authors = ["Tammo Ippen "] license = "MIT" @@ -35,7 +35,6 @@ classifiers = [ python = "~2.7 || ^3.5" six = "^1.12" -pendulum = "^2.0" [tool.poetry.dev-dependencies] @@ -51,6 +50,7 @@ flake8-polyfill = "^1.0.2" flake8-quotes = "^1.0.0" funcsigs = { version = "^1.0", python = "~2.7" } mock = "^2.0" +pendulum = "^2.0" pep8-naming = "^0.7" pytest = "^3.7.3" pytest-cov = "^2.5.1" diff --git a/setup.cfg b/setup.cfg index 34fb8c4..47dc7c6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,11 @@ +[coverage:report] +show_missing = True + +[coverage:run] +branch = True + [flake8] +application_import_names = plotille max-line-length = 120 import-order-style = google diff --git a/tests/test_canvas.py b/tests/test_canvas.py index e5576cf..92acb4d 100644 --- a/tests/test_canvas.py +++ b/tests/test_canvas.py @@ -2,10 +2,11 @@ from __future__ import absolute_import, division, print_function, unicode_literals import numpy as np -from plotille import Canvas import pytest import six +from plotille import Canvas + def test_invalids(): with pytest.raises(AssertionError): diff --git a/tests/test_colors.py b/tests/test_colors.py index 4a2f684..7c8f6aa 100644 --- a/tests/test_colors.py +++ b/tests/test_colors.py @@ -3,9 +3,10 @@ from random import choice -import plotille._colors as clr import pytest +import plotille._colors as clr + def test_color_edges(mocker, tty): assert '' == clr.color('') diff --git a/tests/test_datetime_formatter.py b/tests/test_datetime_formatter.py index bad095c..1ac49a5 100644 --- a/tests/test_datetime_formatter.py +++ b/tests/test_datetime_formatter.py @@ -2,9 +2,10 @@ from __future__ import absolute_import, division, print_function, unicode_literals from pendulum import datetime, duration -from plotille._input_formatter import _convert_datetime, _datetime_formatter import pytest +from plotille._input_formatter import _convert_datetime, _datetime_formatter + @pytest.fixture() def date(): diff --git a/tests/test_dots.py b/tests/test_dots.py index 0addf54..a67cdc7 100644 --- a/tests/test_dots.py +++ b/tests/test_dots.py @@ -3,9 +3,10 @@ from itertools import combinations -from plotille._dots import braille_from, Dots, dots_from import six +from plotille._dots import braille_from, Dots, dots_from + def test_update(): d = Dots() diff --git a/tests/test_examples.py b/tests/test_examples.py index ed8e4c0..6e67c98 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -2,9 +2,10 @@ from __future__ import absolute_import, division, print_function, unicode_literals import numpy as np -import plotille import pytest +import plotille + @pytest.fixture() def seed(): diff --git a/tests/test_figure.py b/tests/test_figure.py index 8bc0c3c..0682f1b 100644 --- a/tests/test_figure.py +++ b/tests/test_figure.py @@ -4,10 +4,12 @@ import datetime as orig_datetime from mock import call +import numpy as np from pendulum import datetime, duration +import pytest + from plotille import Figure from plotille._figure import Histogram, Plot -import pytest def test_width(): @@ -544,7 +546,21 @@ def test_timeseries_orig_dt(): | 06T13:33 07T21:57 09T06:21 10T14:45 11T23:09 13T07:33 14T15:57 16T00:21 17T08:45 """ -def test_timehistogram(): +def test_timehistogram_numpy(): + fig = Figure() + fig.with_colors = False + + day = np.timedelta64(1, 'D') + now = np.datetime64('2018-01-16T11:09:42.000100') + x = [now - i * day for i in range(10)] + + fig.histogram(x, bins=8) + + print(fig.show()) + assert _histogram == fig.show() + + +def test_timehistogram_pendulum(): fig = Figure() fig.with_colors = False diff --git a/tests/test_hist.py b/tests/test_hist.py index 480608d..a6a3154 100644 --- a/tests/test_hist.py +++ b/tests/test_hist.py @@ -3,7 +3,9 @@ import datetime as orig_datetime +import numpy as np from pendulum import datetime, duration + from plotille import hist @@ -20,7 +22,7 @@ ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾""" -def test_timehist(): +def test_timehist_pendulum(): day = duration(days=1) now = datetime(2018, 1, 16, 11, 9, 42, 100) x = [now - i * day for i in range(10)] @@ -32,6 +34,18 @@ def test_timehist(): assert _hist == res +def test_timehist_numpy(): + day = np.timedelta64(1, 'D') + now = np.datetime64('2018-01-16T11:09:42.0001') + x = [now - i * day for i in range(10)] + + res = hist(x, bins=8) + + print() + print(res) + assert _hist == res + + def test_timehist_orig_dt(): day = orig_datetime.timedelta(days=1) now = orig_datetime.datetime(2018, 1, 16, 11, 9, 42, 100) diff --git a/tests/test_input_formatter.py b/tests/test_input_formatter.py index c0fa2c2..ff79091 100644 --- a/tests/test_input_formatter.py +++ b/tests/test_input_formatter.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, division, print_function, unicode_literals from pendulum import datetime, duration + from plotille._input_formatter import InputFormatter diff --git a/tests/test_numberformats.py b/tests/test_numberformats.py index 89c220f..81f036c 100644 --- a/tests/test_numberformats.py +++ b/tests/test_numberformats.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function, unicode_literals -from plotille._input_formatter import _num_formatter import pytest +from plotille._input_formatter import _num_formatter + def test_small_int(): assert ' 13' == _num_formatter(13, chars=10, delta=0)