From 2dec287d4e08a14b435ece8777088c3a59489db3 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Thu, 28 Aug 2014 14:35:02 -0700 Subject: [PATCH] Remove the dependency on prettytable Instead of pulling in yet another dependency for a mostly debugging feature lets just have a tiny table object/class that can work fine for our usage instead. Part of blueprint top-level-types Fixes bug 1368975 Change-Id: I21b1bd8152e5bf64b9060d3aabf4384350d05c0f --- requirements-py2.txt | 2 - requirements-py3.txt | 2 - taskflow/engines/action_engine/runner.py | 32 +++--- taskflow/tests/unit/test_types.py | 21 ++++ taskflow/types/fsm.py | 8 +- taskflow/types/table.py | 128 +++++++++++++++++++++++ 6 files changed, 169 insertions(+), 24 deletions(-) create mode 100644 taskflow/types/table.py diff --git a/requirements-py2.txt b/requirements-py2.txt index 9b204ea6e..bfb837e42 100644 --- a/requirements-py2.txt +++ b/requirements-py2.txt @@ -18,5 +18,3 @@ stevedore>=0.14 futures>=2.1.6 # Used for structured input validation jsonschema>=2.0.0,<3.0.0 -# For pretty printing state-machine tables -PrettyTable>=0.7,<0.8 diff --git a/requirements-py3.txt b/requirements-py3.txt index 63880b314..1e1052e58 100644 --- a/requirements-py3.txt +++ b/requirements-py3.txt @@ -14,5 +14,3 @@ Babel>=1.3 stevedore>=0.14 # Used for structured input validation jsonschema>=2.0.0,<3.0.0 -# For pretty printing state-machine tables -PrettyTable>=0.7,<0.8 diff --git a/taskflow/engines/action_engine/runner.py b/taskflow/engines/action_engine/runner.py index 7a0b9c87e..d2a176979 100644 --- a/taskflow/engines/action_engine/runner.py +++ b/taskflow/engines/action_engine/runner.py @@ -47,23 +47,23 @@ class _MachineBuilder(object): NOTE(harlowja): the machine states that this build will for are:: +--------------+-----------+------------+----------+---------+ - | Start | Event | End | On Enter | On Exit | + Start | Event | End | On Enter | On Exit +--------------+-----------+------------+----------+---------+ - | ANALYZING | finished | GAME_OVER | on_enter | on_exit | - | ANALYZING | schedule | SCHEDULING | on_enter | on_exit | - | ANALYZING | wait | WAITING | on_enter | on_exit | - | FAILURE[$] | | | | | - | GAME_OVER | failed | FAILURE | on_enter | on_exit | - | GAME_OVER | reverted | REVERTED | on_enter | on_exit | - | GAME_OVER | success | SUCCESS | on_enter | on_exit | - | GAME_OVER | suspended | SUSPENDED | on_enter | on_exit | - | RESUMING | schedule | SCHEDULING | on_enter | on_exit | - | REVERTED[$] | | | | | - | SCHEDULING | wait | WAITING | on_enter | on_exit | - | SUCCESS[$] | | | | | - | SUSPENDED[$] | | | | | - | UNDEFINED[^] | start | RESUMING | on_enter | on_exit | - | WAITING | analyze | ANALYZING | on_enter | on_exit | + ANALYZING | finished | GAME_OVER | | + ANALYZING | schedule | SCHEDULING | | + ANALYZING | wait | WAITING | | + FAILURE[$] | | | | + GAME_OVER | failed | FAILURE | | + GAME_OVER | reverted | REVERTED | | + GAME_OVER | success | SUCCESS | | + GAME_OVER | suspended | SUSPENDED | | + RESUMING | schedule | SCHEDULING | | + REVERTED[$] | | | | + SCHEDULING | wait | WAITING | | + SUCCESS[$] | | | | + SUSPENDED[$] | | | | + UNDEFINED[^] | start | RESUMING | | + WAITING | analyze | ANALYZING | | +--------------+-----------+------------+----------+---------+ Between any of these yielded states (minus ``GAME_OVER`` and ``UNDEFINED``) diff --git a/taskflow/tests/unit/test_types.py b/taskflow/tests/unit/test_types.py index 141cdfc8e..0c13ee0f3 100644 --- a/taskflow/tests/unit/test_types.py +++ b/taskflow/tests/unit/test_types.py @@ -23,6 +23,7 @@ from taskflow import exceptions as excp from taskflow import test from taskflow.types import fsm from taskflow.types import graph +from taskflow.types import table from taskflow.types import timing as tt from taskflow.types import tree @@ -161,6 +162,26 @@ class StopWatchTest(test.TestCase): self.assertGreater(0.01, watch.elapsed()) +class TableTest(test.TestCase): + def test_create_valid_no_rows(self): + tbl = table.PleasantTable(['Name', 'City', 'State', 'Country']) + self.assertGreater(0, len(tbl.pformat())) + + def test_create_valid_rows(self): + tbl = table.PleasantTable(['Name', 'City', 'State', 'Country']) + before_rows = tbl.pformat() + tbl.add_row(["Josh", "San Jose", "CA", "USA"]) + after_rows = tbl.pformat() + self.assertGreater(len(before_rows), len(after_rows)) + + def test_create_invalid_columns(self): + self.assertRaises(ValueError, table.PleasantTable, []) + + def test_create_invalid_rows(self): + tbl = table.PleasantTable(['Name', 'City', 'State', 'Country']) + self.assertRaises(ValueError, tbl.add_row, ['a', 'b']) + + class FSMTest(test.TestCase): def setUp(self): super(FSMTest, self).setUp() diff --git a/taskflow/types/fsm.py b/taskflow/types/fsm.py index cbe85b789..8df6461bb 100644 --- a/taskflow/types/fsm.py +++ b/taskflow/types/fsm.py @@ -19,10 +19,10 @@ try: except ImportError: from ordereddict import OrderedDict # noqa -import prettytable import six from taskflow import exceptions as excp +from taskflow.types import table class _Jump(object): @@ -252,8 +252,8 @@ class FSM(object): if sort: return sorted(six.iterkeys(data)) return list(six.iterkeys(data)) - tbl = prettytable.PrettyTable( - ["Start", "Event", "End", "On Enter", "On Exit"]) + tbl = table.PleasantTable(["Start", "Event", "End", + "On Enter", "On Exit"]) for state in orderedkeys(self._states): prefix_markings = [] if self.current_state == state: @@ -287,4 +287,4 @@ class FSM(object): tbl.add_row(row) else: tbl.add_row([pretty_state, "", "", "", ""]) - return tbl.get_string(print_empty=True) + return tbl.pformat() diff --git a/taskflow/types/table.py b/taskflow/types/table.py new file mode 100644 index 000000000..d8d7f3b0e --- /dev/null +++ b/taskflow/types/table.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2014 Yahoo! Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import itertools + +import six + + +class PleasantTable(object): + """A tiny pretty printing table (like prettytable/tabulate but smaller). + + Creates simply formatted tables (with no special sauce):: + + >>> from taskflow.types import table + >>> tbl = table.PleasantTable(['Name', 'City', 'State', 'Country']) + >>> tbl.add_row(["Josh", "San Jose", "CA", "USA"]) + >>> print(tbl.pformat()) + +------+----------+-------+---------+ + Name | City | State | Country + +------+----------+-------+---------+ + Josh | San Jose | CA | USA + +------+----------+-------+---------+ + """ + COLUMN_STARTING_CHAR = ' ' + COLUMN_ENDING_CHAR = '' + COLUMN_SEPARATOR_CHAR = '|' + HEADER_FOOTER_JOINING_CHAR = '+' + HEADER_FOOTER_CHAR = '-' + + @staticmethod + def _center_text(text, max_len, fill=' '): + return '{0:{fill}{align}{size}}'.format(text, fill=fill, + align="^", size=max_len) + + @classmethod + def _size_selector(cls, possible_sizes): + # The number two is used so that the edges of a column have spaces + # around them (instead of being right next to a column separator). + try: + return max(x + 2 for x in possible_sizes) + except ValueError: + return 0 + + def __init__(self, columns): + if len(columns) == 0: + raise ValueError("Column count must be greater than zero") + self._columns = [column.strip() for column in columns] + self._rows = [] + + def add_row(self, row): + if len(row) != len(self._columns): + raise ValueError("Row must have %s columns instead of" + " %s columns" % (len(self._columns), len(row))) + self._rows.append([six.text_type(column) for column in row]) + + def pformat(self): + # Figure out the maximum column sizes... + column_count = len(self._columns) + column_sizes = [0] * column_count + headers = [] + for i, column in enumerate(self._columns): + possible_sizes_iter = itertools.chain( + [len(column)], (len(row[i]) for row in self._rows)) + column_sizes[i] = self._size_selector(possible_sizes_iter) + headers.append(self._center_text(column, column_sizes[i])) + # Build the header and footer prefix/postfix. + header_footer_buf = six.StringIO() + header_footer_buf.write(self.HEADER_FOOTER_JOINING_CHAR) + for i, header in enumerate(headers): + header_footer_buf.write(self.HEADER_FOOTER_CHAR * len(header)) + if i + 1 != column_count: + header_footer_buf.write(self.HEADER_FOOTER_JOINING_CHAR) + header_footer_buf.write(self.HEADER_FOOTER_JOINING_CHAR) + # Build the main header. + content_buf = six.StringIO() + content_buf.write(header_footer_buf.getvalue()) + content_buf.write("\n") + content_buf.write(self.COLUMN_STARTING_CHAR) + for i, header in enumerate(headers): + if i + 1 == column_count: + if self.COLUMN_ENDING_CHAR: + content_buf.write(headers[i]) + content_buf.write(self.COLUMN_ENDING_CHAR) + else: + content_buf.write(headers[i].rstrip()) + else: + content_buf.write(headers[i]) + content_buf.write(self.COLUMN_SEPARATOR_CHAR) + content_buf.write("\n") + content_buf.write(header_footer_buf.getvalue()) + # Build the main content. + row_count = len(self._rows) + if row_count: + content_buf.write("\n") + for i, row in enumerate(self._rows): + pieces = [] + for j, column in enumerate(row): + pieces.append(self._center_text(column, column_sizes[j])) + if j + 1 != column_count: + pieces.append(self.COLUMN_SEPARATOR_CHAR) + blob = ''.join(pieces) + if self.COLUMN_ENDING_CHAR: + content_buf.write(self.COLUMN_STARTING_CHAR) + content_buf.write(blob) + content_buf.write(self.COLUMN_ENDING_CHAR) + else: + blob = blob.rstrip() + if blob: + content_buf.write(self.COLUMN_STARTING_CHAR) + content_buf.write(blob) + if i + 1 != row_count: + content_buf.write("\n") + content_buf.write("\n") + content_buf.write(header_footer_buf.getvalue()) + return content_buf.getvalue()