diff --git a/.gitignore b/.gitignore index ab599fc..0d6be57 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ dist .coverage .cover/* + +.pytest_cache/ +venv/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index c704523..0a664d2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,26 @@ language: python - matrix: include: - - python: "2.7" - - python: "3.4" - - python: "3.5" - - python: "3.6" - - python: "3.7" + - python: '2.7' + - python: '3.4' + - python: '3.5' + - python: '3.6' + - python: '3.7' dist: xenial sudo: required - install: - - "pip install -r req-dev.txt" - - "pip install flake8" - +- pip install -r req-dev.txt +- pip install flake8 before_script: - # stop the build if there are Python syntax errors or undefined names - - time flake8 . --count --exclude=_compact.py --select=E901,E999,F821,F822,F823 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - - time flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - +- time flake8 . --count --exclude=_compact.py --select=E901,E999,F821,F822,F823 --show-source + --statistics +- time flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics script: make test after_success: coveralls +deploy: + provider: pypi + user: thibdct + password: + secure: SAVNEMbrG4sIW4l3JcRv+nsiDygazwAw7oaU+fsSUxODvu+ekS+6HUGykQnHZabBVN9qy0y47tXjubkmpnSWP/jbFaz1uwo07mE5mlcGUOxIpkoYsbFoig7hYOjGCv02Eo/FJ7CXWXLgj9FR9gdoSPwxUdIf0TmXQfG5klQH/4piUNx/i05aqFT212Lp19byGUDuLaEsTHwbzuov5XJ6bSyN8P0ZIEU7edTUIlZGP89PJonC7Uzo+hCyfqf5YgFAC182GgnI5sR4jQ/divygaX2fGyfTLTYY8BbFCwfuEKTjE1gGdoKeAFNl21MgZxPT5dWIkZNePOIZY37TctDvM85tAIW68kF9wFxR6GfdUUlGc3HLdUrAkzdl2Sq2xy5xJ9qnb3DalqGLex+qnqEE9gQLqyfLXPcO0aJ58ZpPgzshKLjo+aQzMzRmYXND5SZ9DEDBC0dtMv1XNEnCrTF9tZMvCGdy73ZEeoT8UiZWnOSCmLXa4FMN9Q1Hh2hzAvmi/qydHXI1t8h/y3VJBkWtOM8bIa5FnQ0iOGyzMgpUWSZtlQ7uoHp8x7xwKOGwj9EjrMY4avRdtKIvMHuqiyuxQIPogTQeVG1Hz9gt2z3yoxsRYB8YD8OXC6z5Go0m40pd4oqPvfFKkFts96OPJShoqw3rOgMmKR3E7PSBxdPkpnU= + on: + tags: true diff --git a/prettytable/__init__.py b/prettytable/__init__.py index bef5807..4869060 100644 --- a/prettytable/__init__.py +++ b/prettytable/__init__.py @@ -30,8 +30,9 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -__version__ = "0.9.2" +__version__ = "0.9.3" from .prettytable import PrettyTable from .prettytable import ALL, HEADER, MSWORD_FRIENDLY, NONE, PLAIN_COLUMNS +from .prettytable import FRAME, UNICODE from .factory import from_csv, from_db_cursor, from_html, from_html_one diff --git a/prettytable/prettytable.py b/prettytable/prettytable.py index 02dab66..b9f9530 100644 --- a/prettytable/prettytable.py +++ b/prettytable/prettytable.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- import copy import math import random @@ -21,6 +22,7 @@ MSWORD_FRIENDLY = 11 PLAIN_COLUMNS = 12 RANDOM = 20 +UNICODE = 30 _re = re.compile("\033\[[0-9;]*m") @@ -60,6 +62,14 @@ def __init__(self, field_names=None, **kwargs): vertical_char - single character string used to draw vertical lines horizontal_char - single character string used to draw horizontal lines junction_char - single character string used to draw line junctions + left_junction_char - single character string used to draw line junctions (left part) + right_junction_char - single character string used to draw line junctions (right part) + upper_junction_char - single character string used to draw line junctions (upper part) + bottom_junction_char - single character string used to draw line junctions (bottom part) + upper_left_corner_char - single character string used to draw the upper left corner + bottom_left_corner_char - single character string used to draw the bottom left corner + upper_right_corner_char - single character string used to draw the upper right corner + bottom_right_corner_char - single character string used to draw the bottom right corner sortby - name of field to sort rows by sort_key - sorting key function, applied to data points before sorting valign - default valign for each row (None, "t", "m" or "b") @@ -83,11 +93,20 @@ def __init__(self, field_names=None, **kwargs): self._widths = [] # Options - self._options = "title start end fields header border sortby reversesort sort_key attributes format hrules vrules".split() + self._options = "title start end fields header border sortby \ + reversesort sort_key attributes format hrules vrules".split() self._options.extend( - "int_format float_format min_table_width max_table_width padding_width left_padding_width right_padding_width".split()) + "int_format float_format min_table_width max_table_width \ + padding_width left_padding_width right_padding_width".split()) self._options.extend( - "vertical_char horizontal_char junction_char header_style valign xhtml print_empty oldsortslice".split()) + "vertical_char horizontal_char junction_char header_style valign \ + xhtml print_empty oldsortslice".split()) + self._options.extend( + "left_junction_char right_junction_char upper_junction_char \ + bottom_junction_char".split()) + self._options.extend( + "upper_left_corner_char upper_right_corner_char \ + bottom_left_corner_char bottom_right_corner_char".split()) self._options.extend("align valign max_width min_width".split()) for option in self._options: if option in kwargs: @@ -139,6 +158,17 @@ def __init__(self, field_names=None, **kwargs): self._vertical_char = kwargs["vertical_char"] or self._unicode("|") self._horizontal_char = kwargs["horizontal_char"] or self._unicode("-") self._junction_char = kwargs["junction_char"] or self._unicode("+") + self._left_junction_char = kwargs["left_junction_char"] or None + self._right_junction_char = kwargs["right_junction_char"] or None + self._upper_junction_char = kwargs["upper_junction_char"] or None + self._bottom_junction_char = kwargs["bottom_junction_char"] or None + self._upper_left_corner_char = kwargs["upper_left_corner_char"] or None + self._upper_right_corner_char = kwargs["upper_right_corner_char"] \ + or None + self._bottom_left_corner_char = kwargs["bottom_left_corner_char"] \ + or None + self._bottom_right_corner_char = kwargs["bottom_right_corner_char"] \ + or None if kwargs["print_empty"] in (True, False): self._print_empty = kwargs["print_empty"] @@ -255,7 +285,11 @@ def _validate_option(self, option, val): self._validate_int_format(option, val) elif option in ("float_format"): self._validate_float_format(option, val) - elif option in ("vertical_char", "horizontal_char", "junction_char"): + elif option in ("vertical_char", "horizontal_char", "junction_char", + "left_junction_char", "right_junction_char", + "upper_junction_char", "bottom_junction_char", + "upper_left_corner_char", "upper_right_corner_char", + "bottom_left_corner_char", "bottom_right_corner_char"): self._validate_single_char(option, val) elif option in ("attributes"): self._validate_attributes(option, val) @@ -755,7 +789,7 @@ def right_padding_width(self, val): @property def vertical_char(self): - """The charcter used when printing table borders to draw vertical lines + """The character used when printing table borders to draw vertical lines Arguments: @@ -770,11 +804,12 @@ def vertical_char(self, val): @property def horizontal_char(self): - """The charcter used when printing table borders to draw horizontal lines + """The character used when printing table borders to draw horizontal lines Arguments: - horizontal_char - single character string used to draw horizontal lines""" + horizontal_char - single character string used to draw horizontal lines + """ return self._horizontal_char @horizontal_char.setter @@ -785,7 +820,7 @@ def horizontal_char(self, val): @property def junction_char(self): - """The charcter used when printing table borders to draw line junctions + """The character used when printing table borders to draw line junctions Arguments: @@ -795,9 +830,137 @@ def junction_char(self): @junction_char.setter def junction_char(self, val): val = self._unicode(val) - self._validate_option("vertical_char", val) + self._validate_option("junction_char", val) self._junction_char = val + @property + def left_junction_char(self): + """The character used when printing table borders to draw line junctions + + Arguments: + + left_junction_char - single character string used to draw line \ +junctions (left part) """ + return self._left_junction_char + + @left_junction_char.setter + def left_junction_char(self, val): + val = self._unicode(val) + self._validate_option("left_junction_char", val) + self._left_junction_char = val + + @property + def right_junction_char(self): + """The character used when printing table borders to draw line junctions + + Arguments: + + right_junction_char - single character string used to draw line \ +junctions (right part)""" + return self._right_junction_char + + @right_junction_char.setter + def right_junction_char(self, val): + val = self._unicode(val) + self._validate_option("right_junction_char", val) + self._right_junction_char = val + + @property + def bottom_junction_char(self): + """The character used when printing table borders to draw line junctions + + Arguments: + + bottom_junction_char - single character string used to draw line \ +junctions (bottom part)""" + return self._bottom_junction_char + + @bottom_junction_char.setter + def bottom_junction_char(self, val): + val = self._unicode(val) + self._validate_option("bottom_junction_char", val) + self._bottom_junction_char = val + + @property + def upper_junction_char(self): + """The character used when printing table borders to draw line junctions + + Arguments: + + upper_junction_char - single character string used to draw line \ +junctions (upper part) """ + return self._upper_junction_char + + @upper_junction_char.setter + def upper_junction_char(self, val): + val = self._unicode(val) + self._validate_option("upper_junction_char", val) + self._upper_junction_char = val + + @property + def upper_left_corner_char(self): + """The character used when printing table borders to draw corner + + Arguments: + + upper_left_corner_char - single character string used to draw corner \ +(upper left)""" + return self._upper_left_corner_char + + @upper_left_corner_char.setter + def upper_left_corner_char(self, val): + val = self._unicode(val) + self._validate_option("upper_left_corner_char", val) + self._upper_left_corner_char = val + + @property + def upper_right_corner_char(self): + """The character used when printing table borders to draw corner + + Arguments: + + upper_right_corner_char - single character string used to draw corner \ +(upper right)""" + return self._upper_right_corner_char + + @upper_right_corner_char.setter + def upper_right_corner_char(self, val): + val = self._unicode(val) + self._validate_option("upper_right_corner_char", val) + self._upper_right_corner_char = val + + @property + def bottom_left_corner_char(self): + """The character used when printing table borders to draw corner + + Arguments: + + bottom_left_corner_char - single character string used to draw corner \ +(upper left)""" + return self._bottom_left_corner_char + + @bottom_left_corner_char.setter + def bottom_left_corner_char(self, val): + val = self._unicode(val) + self._validate_option("bottom_left_corner_char", val) + self._bottom_left_corner_char = val + + @property + def bottom_right_corner_char(self): + """The character used when printing table borders to draw corner + + Arguments: + + bottom_right_corner_char - single character string used to draw \ +corner (upper right)""" + return self._bottom_right_corner_char + + @bottom_right_corner_char.setter + def bottom_right_corner_char(self, val): + val = self._unicode(val) + self._validate_option("bottom_right_corner_char", val) + self._bottom_right_corner_char = val + @property def format(self): """Controls whether or not HTML tables are formatted to match styling options @@ -879,6 +1042,8 @@ def set_style(self, style): self._set_columns_style() elif style == RANDOM: self._set_random_style() + elif style == UNICODE: + self._set_unicode_style() else: raise Exception("Invalid pre-set style!") @@ -926,6 +1091,28 @@ def _set_random_style(self): self.horizontal_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?") self.junction_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?") + def _set_unicode_style(self): + + # Unicode style + self.header = True + self.border = True + self._hrules = ALL + self._vrules = ALL + self.padding_width = 1 + self.left_padding_width = 1 + self.right_padding_width = 1 + self.vertical_char = "│" + self.horizontal_char = "─" + self.junction_char = "┼" + self.left_junction_char = "├" + self.right_junction_char = "┤" + self.upper_junction_char = "┬" + self.bottom_junction_char = "┴" + self.upper_left_corner_char = "┌" + self.upper_right_corner_char = "┐" + self.bottom_left_corner_char = "└" + self.bottom_right_corner_char = "┘" + ############################## # DATA INPUT METHODS # ############################## @@ -1140,6 +1327,21 @@ def _format_row(self, row, options): def _format_rows(self, rows, options): return [self._format_row(row, options) for row in rows] + def _modify_line(self, line, left_corner, right_corner, + old_junction_char, new_junction_char): + """ Change specific characters in a line (corners, and junction_char) + """ + line_list = list(line) + # We cannot set line[0] if line is from `str` type + if left_corner: + line_list[0] = left_corner + if right_corner: + line_list[-1] = right_corner + line = ''.join(line_list) + if new_junction_char: + line = line.replace(old_junction_char, new_junction_char) + return line + ############################## # PLAIN TEXT STRING METHODS # ############################## @@ -1178,7 +1380,13 @@ def _prepare_lines(self, **kwargs): if options["header"]: lines.extend(self._stringify_header(options).split('\n')) elif options["border"] and options["hrules"] in (ALL, FRAME): - lines.append(self._hrule) + first_line = self._modify_line( + line=self._hrule, + left_corner=options["upper_left_corner_char"], + right_corner=options["upper_right_corner_char"], + old_junction_char=options["junction_char"], + new_junction_char=options["upper_junction_char"]) + lines.append(first_line) # Add rows for row in formatted_rows: @@ -1186,7 +1394,32 @@ def _prepare_lines(self, **kwargs): # Add bottom of border if options["border"] and options["hrules"] == FRAME: - lines.append(self._hrule) + last_line = self._modify_line( + line=self._hrule, + left_corner=options["bottom_left_corner_char"], + right_corner=options["bottom_right_corner_char"], + old_junction_char=options["junction_char"], + new_junction_char=options["bottom_junction_char"]) + lines.append(last_line) + + if options["border"] and options["vrules"] in (ALL, FRAME) \ + and options["hrules"] == ALL: + # Extract + last_row = lines[-1] + # last row contains both values and bottom border + # we only need to modify the bottom border + last_row_list = last_row.split('\n') + bottom_border = last_row_list[-1] + lines = lines[:-1] # Remove last row (to rebuild it properly) + bottom_border = self._modify_line( + line=bottom_border, + left_corner=options["bottom_left_corner_char"], + right_corner=options["bottom_right_corner_char"], + old_junction_char=options["junction_char"], + new_junction_char=options["bottom_junction_char"]) + last_row_list[-1] = bottom_border + last_row = "\n".join(last_row_list) + lines.append(last_row) return lines def get_string(self, **kwargs): @@ -1224,24 +1457,35 @@ def _stringify_hrule(self, options): return "" lpad, rpad = self._get_padding_widths(options) if options['vrules'] in (ALL, FRAME): - bits = [options["junction_char"]] + if options["left_junction_char"] != None: + bits = [options["left_junction_char"]] + else: + bits = [options["junction_char"]] else: bits = [options["horizontal_char"]] # For tables with no data or fieldnames if not self._field_names: bits.append(options["junction_char"]) return "".join(bits) + for field, width in zip(self._field_names, self._widths): if options["fields"] and field not in options["fields"]: continue bits.append((width + lpad + rpad) * options["horizontal_char"]) if options['vrules'] == ALL: - bits.append(options["junction_char"]) + bits.append(options["junction_char"]) else: bits.append(options["horizontal_char"]) + if options["vrules"] == ALL: + if options["right_junction_char"] != None: + bits.pop() + bits.append(options["right_junction_char"]) if options["vrules"] == FRAME: bits.pop() - bits.append(options["junction_char"]) + if options["right_junction_char"] != None: + bits.append(options["right_junction_char"]) + else: + bits.append(options["junction_char"]) return "".join(bits) def _stringify_title(self, title, options): @@ -1270,7 +1514,16 @@ def _stringify_header(self, options): lpad, rpad = self._get_padding_widths(options) if options["border"]: if options["hrules"] in (ALL, FRAME): - bits.append(self._hrule) + if options["vrules"] != NONE: + first_line = self._modify_line( + line=self._hrule, + left_corner=options["upper_left_corner_char"], + right_corner=options["upper_right_corner_char"], + old_junction_char=options["junction_char"], + new_junction_char=options["upper_junction_char"]) + bits.append(first_line) + else: + bits.append(self._hrule) bits.append("\n") if options["vrules"] in (ALL, FRAME): bits.append(options["vertical_char"]) diff --git a/setup.py b/setup.py index 1f49401..28f70f4 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ def fread(filepath): setup( - name='PTable', + name='PTableUnicode', version=version, include_package_data=True, zip_safe=False, @@ -38,7 +38,7 @@ def fread(filepath): author_email='luke@maurits.id.au', maintainer='Kane Blueriver', maintainer_email='kxxoling@gmail.com', - url='https://github.com/kxxoling/PTable', + url='https://github.com/tducret/PTable', py_modules=['prettytable', 'prettytable.cli', 'prettytable.prettytable', 'prettytable.factory', 'prettytable._compact'], test_suite="test_prettytable", diff --git a/tests/test_prettytable.py b/tests/test_prettytable.py index 064e2b0..89ef675 100644 --- a/tests/test_prettytable.py +++ b/tests/test_prettytable.py @@ -1,8 +1,8 @@ #!/usr/bin/env python -# coding=UTF-8 +# -*- coding: utf-8 -*- from prettytable import PrettyTable -from prettytable import ALL, HEADER, MSWORD_FRIENDLY, NONE +from prettytable import ALL, HEADER, FRAME, MSWORD_FRIENDLY, NONE, UNICODE from prettytable import from_csv, from_db_cursor, from_html, from_html_one from prettytable._compact import StringIO @@ -759,5 +759,119 @@ def testRstOutput(self): """.strip()) +class UnicodeStyleTest(unittest.TestCase): + def setUp(self): + self.x = PrettyTable(["A", "B", "C"]) + self.x.add_row(["a", "b", "c"]) + self.x.add_row(["aa", "bb", "cc"]) + + def testUnicodeStyle(self): + self.x.set_style(UNICODE) + print() + print(self.x) + self.assertEqual(self.x.get_string().strip(), u""" +┌────┬────┬────┐ +│ A │ B │ C │ +├────┼────┼────┤ +│ a │ b │ c │ +├────┼────┼────┤ +│ aa │ bb │ cc │ +└────┴────┴────┘ +""".strip()) + + def testUnicodeStyleNoBorder(self): + self.x.set_style(UNICODE) + self.x.border = False + print() + print(self.x) + self.assertEqual(self.x.get_string().strip(), u""" + A B C \n\ + a b c \n\ + aa bb cc \n\ +""".strip()) # I have to put '\n\' because my trailing spaces are deleted + # by my editor otherwise, and the test would fail. + + def testUnicodeStyleHruleFrame(self): + self.x.set_style(UNICODE) + self.x.hrules = FRAME + print() + print(self.x) + self.assertEqual(self.x.get_string().strip(), u""" +┌────┬────┬────┐ +│ A │ B │ C │ +├────┼────┼────┤ +│ a │ b │ c │ +│ aa │ bb │ cc │ +└────┴────┴────┘ +""".strip()) + + def testUnicodeStyleHruleHeader(self): + self.x.set_style(UNICODE) + self.x.hrules = HEADER + print() + print(self.x) + self.assertEqual(self.x.get_string().strip(), u""" +│ A │ B │ C │ +├────┼────┼────┤ +│ a │ b │ c │ +│ aa │ bb │ cc │ +""".strip()) + + def testUnicodeStyleHruleNone(self): + self.x.set_style(UNICODE) + self.x.hrules = NONE + print() + print(self.x) + self.assertEqual(self.x.get_string().strip(), u""" +│ A │ B │ C │ +│ a │ b │ c │ +│ aa │ bb │ cc │ +""".strip()) + + def testUnicodeStyleVruleNone(self): + self.x.set_style(UNICODE) + self.x.vrules = NONE + print() + print(self.x) + self.assertEqual(self.x.get_string().strip(), u""" +──────────────── + A B C \n\ +──────────────── + a b c \n\ +──────────────── + aa bb cc \n\ +──────────────── +""".strip()) # I have to put '\n\' because my trailing spaces are deleted + # by my editor otherwise, and the test would fail. + + def testUnicodeStyleVruleFrame(self): + self.x.set_style(UNICODE) + self.x.vrules = FRAME + print() + print(self.x) + self.assertEqual(self.x.get_string().strip(), u""" +┌──────────────┐ +│ A B C │ +├──────────────┤ +│ a b c │ +├──────────────┤ +│ aa bb cc │ +└──────────────┘ +""".strip()) + + def testUnicodeStyleWithoutHeader(self): + self.x.set_style(UNICODE) + self.x.header = False + print() + print(self.x) + self.assertEqual(self.x.get_string().strip(), u""" +┌────┬────┬────┐ +│ a │ b │ c │ +├────┼────┼────┤ +│ aa │ bb │ cc │ +└────┴────┴────┘ +""".strip()) + + if __name__ == "__main__": unittest.main()