From c9027b446caac8c790be1e1ca257654240296d21 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 19 Oct 2019 18:52:39 +0300 Subject: [PATCH 1/7] Drop support fo Python 2.7 --- docs/intro.rst | 2 +- src/tablib/packages/dbfpy/fields.py | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/intro.rst b/docs/intro.rst index 00562c1..064e5ad 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -74,7 +74,7 @@ THE SOFTWARE. Pythons Supported ----------------- -Python 3.5+ are officially supported. +Python 3.5+ is officially supported. Now, go :ref:`install Tablib `. diff --git a/src/tablib/packages/dbfpy/fields.py b/src/tablib/packages/dbfpy/fields.py index c763e1e..c46037b 100644 --- a/src/tablib/packages/dbfpy/fields.py +++ b/src/tablib/packages/dbfpy/fields.py @@ -134,11 +134,7 @@ class DbfFieldDef: definition of this field. """ - if sys.version_info < (2, 4): - # earlier versions did not support padding character - _name = self.name[:11] + "\0" * (11 - len(self.name)) - else: - _name = self.name.ljust(11, '\0') + _name = self.name.ljust(11, '\0') return ( _name + self.typeCode + From 7347d07624664bb3fd525c2fe68d4aa86cab32fe Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 19 Oct 2019 19:25:34 +0300 Subject: [PATCH 2/7] Upgrade Python syntax with pyupgrade --py3-plus --- docs/conf.py | 13 ++++++------- src/tablib/core.py | 10 +++++----- src/tablib/formats/_html.py | 2 +- src/tablib/formats/_jira.py | 8 ++++---- src/tablib/formats/_rst.py | 2 +- src/tablib/formats/_xlsx.py | 2 +- src/tablib/packages/dbfpy/dbfnew.py | 2 +- src/tablib/packages/dbfpy/fields.py | 2 +- tests/test_tablib.py | 4 ++-- 9 files changed, 22 insertions(+), 23 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 5c56c66..8df446e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Tablib documentation build configuration file, created by # sphinx-quickstart on Tue Oct 5 15:25:21 2010. @@ -38,8 +37,8 @@ source_suffix = '.rst' master_doc = 'index' # General information about the project. -project = u'Tablib' -copyright = u'2019 Jazzband' +project = 'Tablib' +copyright = '2019 Jazzband' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -181,8 +180,8 @@ htmlhelp_basename = 'Tablibdoc' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'Tablib.tex', u'Tablib Documentation', - u'Jazzband', 'manual'), + ('index', 'Tablib.tex', 'Tablib Documentation', + 'Jazzband', 'manual'), ] latex_use_modindex = False @@ -222,6 +221,6 @@ latex_use_parts = True # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'tablib', u'Tablib Documentation', - [u'Jazzband'], 1) + ('index', 'tablib', 'Tablib Documentation', + ['Jazzband'], 1) ] diff --git a/src/tablib/core.py b/src/tablib/core.py index 23903eb..335927e 100644 --- a/src/tablib/core.py +++ b/src/tablib/core.py @@ -264,7 +264,7 @@ class Dataset: else: is_valid = (len(col) == self.height) if self.height else True else: - is_valid = all((len(x) == self.width for x in self._data)) + is_valid = all(len(x) == self.width for x in self._data) if is_valid: return True @@ -423,7 +423,7 @@ class Dataset: export_set, import_set = self._formats.get(format, (None, None)) if not import_set: - raise UnsupportedFormat('Format {0} cannot be imported.'.format(format)) + raise UnsupportedFormat('Format {} cannot be imported.'.format(format)) import_set(self, in_stream, **kwargs) return self @@ -436,7 +436,7 @@ class Dataset: """ export_set, import_set = self._formats.get(format, (None, None)) if not export_set: - raise UnsupportedFormat('Format {0} cannot be exported.'.format(format)) + raise UnsupportedFormat('Format {} cannot be exported.'.format(format)) return export_set(self, **kwargs) @@ -1095,7 +1095,7 @@ class Databook: export_book, import_book = self._formats.get(format, (None, None)) if not import_book: - raise UnsupportedFormat('Format {0} cannot be loaded.'.format(format)) + raise UnsupportedFormat('Format {} cannot be loaded.'.format(format)) import_book(self, in_stream, **kwargs) return self @@ -1108,7 +1108,7 @@ class Databook: """ export_book, import_book = self._formats.get(format, (None, None)) if not export_book: - raise UnsupportedFormat('Format {0} cannot be exported.'.format(format)) + raise UnsupportedFormat('Format {} cannot be exported.'.format(format)) return export_book(self, **kwargs) diff --git a/src/tablib/formats/_html.py b/src/tablib/formats/_html.py index bb31128..edf0c0a 100644 --- a/src/tablib/formats/_html.py +++ b/src/tablib/formats/_html.py @@ -53,7 +53,7 @@ def export_book(databook): for i, dset in enumerate(databook._datasets): title = (dset.title if dset.title else 'Set %s' % (i)) - wrapper.write('<%s>%s\n' % (BOOK_ENDINGS, title, BOOK_ENDINGS)) + wrapper.write('<{}>{}\n'.format(BOOK_ENDINGS, title, BOOK_ENDINGS)) wrapper.write(dset.html) wrapper.write('\n') diff --git a/src/tablib/formats/_jira.py b/src/tablib/formats/_jira.py index 99dbf3e..96efcf7 100644 --- a/src/tablib/formats/_jira.py +++ b/src/tablib/formats/_jira.py @@ -19,7 +19,7 @@ def export_set(dataset): header = _get_header(dataset.headers) if dataset.headers else '' body = _get_body(dataset) - return '%s\n%s' % (header, body) if header else body + return '{}\n{}'.format(header, body) if header else body def _get_body(dataset): @@ -31,6 +31,6 @@ def _get_header(headers): def _serialize_row(row, delimiter='|'): - return '%s%s%s' % (delimiter, - delimiter.join([str(item) if item else ' ' for item in row]), - delimiter) + return '{}{}{}'.format(delimiter, + delimiter.join([str(item) if item else ' ' for item in row]), + delimiter) diff --git a/src/tablib/formats/_rst.py b/src/tablib/formats/_rst.py index 8b5cfa0..8067f73 100644 --- a/src/tablib/formats/_rst.py +++ b/src/tablib/formats/_rst.py @@ -34,7 +34,7 @@ def _max_word_len(text): 8 """ - return max((len(word) for word in text.split())) if text else 0 + return max(len(word) for word in text.split()) if text else 0 def _get_column_string_lengths(dataset): diff --git a/src/tablib/formats/_xlsx.py b/src/tablib/formats/_xlsx.py index 0a947b8..6ac46b9 100644 --- a/src/tablib/formats/_xlsx.py +++ b/src/tablib/formats/_xlsx.py @@ -112,7 +112,7 @@ def dset_sheet(dataset, ws, freeze_panes=True): row_number = i + 1 for j, col in enumerate(row): col_idx = get_column_letter(j + 1) - cell = ws['%s%s' % (col_idx, row_number)] + cell = ws['{}{}'.format(col_idx, row_number)] # bold headers if (row_number == 1) and dataset.headers: diff --git a/src/tablib/packages/dbfpy/dbfnew.py b/src/tablib/packages/dbfpy/dbfnew.py index 29c09a1..1562021 100644 --- a/src/tablib/packages/dbfpy/dbfnew.py +++ b/src/tablib/packages/dbfpy/dbfnew.py @@ -176,7 +176,7 @@ if __name__ == '__main__': for i1 in range(len(dbft)): rec = dbft[i1] for fldName in dbft.fieldNames: - print('%s:\t %s' % (fldName, rec[fldName])) + print('{}:\t {}'.format(fldName, rec[fldName])) print() dbft.close() diff --git a/src/tablib/packages/dbfpy/fields.py b/src/tablib/packages/dbfpy/fields.py index c46037b..d6ee84e 100644 --- a/src/tablib/packages/dbfpy/fields.py +++ b/src/tablib/packages/dbfpy/fields.py @@ -307,7 +307,7 @@ class DbfLogicalFieldDef(DbfFieldDef): return False if value in "YyTt": return True - raise ValueError("[%s] Invalid logical value %r" % (self.name, value)) + raise ValueError("[{}] Invalid logical value {!r}".format(self.name, value)) def encodeValue(self, value): """Return a character from the "TF?" set. diff --git a/tests/test_tablib.py b/tests/test_tablib.py index 77f2ef6..4318d7d 100755 --- a/tests/test_tablib.py +++ b/tests/test_tablib.py @@ -1012,7 +1012,7 @@ class DBFTests(BaseTestCase): for reg_char, data_char in zip(_dbf, data.dbf): so_far += chr(data_char) if reg_char != data_char and index not in [1, 2, 3]: - raise AssertionError('Failing at char %s: %s vs %s %s' % ( + raise AssertionError('Failing at char {}: {} vs {} {}'.format( index, reg_char, data_char, so_far)) index += 1 @@ -1055,7 +1055,7 @@ class DBFTests(BaseTestCase): # found_so_far += chr(data_char) if reg_char != data_char and index not in [1, 2, 3]: raise AssertionError( - 'Failing at char %s: %s vs %s (found %s)' % ( + 'Failing at char {}: {} vs {} (found {})'.format( index, reg_char, data_char, found_so_far)) index += 1 From e4ac50260e7d3dd3c175741624721210a6c1d7f9 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 19 Oct 2019 19:58:20 +0300 Subject: [PATCH 3/7] __getslice__ is deprecated since Python 2.0 and it is not available in Python 3 https://docs.python.org/2/reference/datamodel.html#object.__getslice__ --- src/tablib/core.py | 3 --- tests/test_tablib.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/tablib/core.py b/src/tablib/core.py index 335927e..61c7a50 100644 --- a/src/tablib/core.py +++ b/src/tablib/core.py @@ -40,9 +40,6 @@ class Row: def __repr__(self): return repr(self._row) - def __getslice__(self, i, j): - return self._row[i:j] - def __getitem__(self, i): return self._row[i] diff --git a/tests/test_tablib.py b/tests/test_tablib.py index 4318d7d..b8049e8 100755 --- a/tests/test_tablib.py +++ b/tests/test_tablib.py @@ -202,7 +202,7 @@ class TablibTestCase(BaseTestCase): self.assertEqual(self.founders[2:], [self.tom]) def test_row_slicing(self): - """Verify Row's __getslice__ method. Issue #184.""" + """Verify Row slicing. Issue #184.""" john = Row(self.john) From 088b916bab9f34221a4a831df9683525538de728 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 20 Oct 2019 12:04:33 +0300 Subject: [PATCH 4/7] __unicode__ not used in Python 3 --- src/tablib/packages/dbfpy/utils.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/tablib/packages/dbfpy/utils.py b/src/tablib/packages/dbfpy/utils.py index b3388a7..284d3ee 100644 --- a/src/tablib/packages/dbfpy/utils.py +++ b/src/tablib/packages/dbfpy/utils.py @@ -158,9 +158,6 @@ class _InvalidValue: def __str__(self): return "" - def __unicode__(self): - return "" - def __repr__(self): return "" From bf6e5c2e7845f13888abbf0d6362e4c2374471fb Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 19 Oct 2019 21:55:01 +0300 Subject: [PATCH 5/7] 100% Row test coverage --- tests/test_tablib.py | 82 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/tests/test_tablib.py b/tests/test_tablib.py index b8049e8..5464a8d 100755 --- a/tests/test_tablib.py +++ b/tests/test_tablib.py @@ -4,6 +4,7 @@ import datetime import doctest import json +import pickle import unittest from uuid import uuid4 @@ -486,6 +487,87 @@ class TablibTestCase(BaseTestCase): self.founders.append(('First\nSecond', 'Name', 42)) self.founders.export('xlsx') + def test_row_repr(self): + """Row repr.""" + # Arrange + john = Row(self.john) + + # Act + output = str(john) + + # Assert + self.assertEqual(output, "['John', 'Adams', 90]") + + def test_row_pickle_unpickle(self): + """Row __setstate__ and __getstate__.""" + # Arrange + before_pickle = Row(self.john) + + # Act + output = pickle.loads(pickle.dumps(before_pickle)) + + # Assert + self.assertEqual(output[0], before_pickle[0]) + self.assertEqual(output[1], before_pickle[1]) + self.assertEqual(output[2], before_pickle[2]) + + def test_row_lpush(self): + """Row lpush.""" + # Arrange + john = Row(self.john) + george = Row(self.george) + + # Act + john.lpush(george) + + # Assert + self.assertEqual(john[-1], george) + + def test_row_append(self): + """Row append.""" + # Arrange + john = Row(self.john) + george = Row(self.george) + + # Act + john.append(george) + + # Assert + self.assertEqual(john[0], george) + + def test_row_contains(self): + """Row __contains__.""" + # Arrange + john = Row(self.john) + + # Act / Assert + self.assertIn("John", john) + + def test_row_no_tag(self): + """Row has_tag.""" + # Arrange + john = Row(self.john) + + # Act / Assert + self.assertFalse(john.has_tag("not found")) + self.assertFalse(john.has_tag(None)) + + def test_row_has_tag(self): + """Row has_tag.""" + # Arrange + john = Row(self.john, tags=["tag1"]) + + # Act / Assert + self.assertTrue(john.has_tag("tag1")) + + def test_row_has_tags(self): + """Row has_tag.""" + # Arrange + john = Row(self.john, tags=["tag1", "tag2"]) + + # Act / Assert + self.assertTrue(john.has_tag(["tag2", "tag1"])) + class HTMLTests(BaseTestCase): def test_html_export(self): From d77aba6210f44f9cb5828670aef93305594474ca Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 20 Oct 2019 12:29:59 +0300 Subject: [PATCH 6/7] Add more tests --- tests/test_tablib_dbfpy_packages_utils.py | 170 ++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 tests/test_tablib_dbfpy_packages_utils.py diff --git a/tests/test_tablib_dbfpy_packages_utils.py b/tests/test_tablib_dbfpy_packages_utils.py new file mode 100644 index 0000000..9288916 --- /dev/null +++ b/tests/test_tablib_dbfpy_packages_utils.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python +"""Tests for tablib.packages.dbfpy.""" + +import datetime +import unittest + +from tablib.packages.dbfpy import utils + + +class UtilsUnzfillTestCase(unittest.TestCase): + """dbfpy.utils.unzfill test cases.""" + + def test_unzfill_with_nul(self): + # Arrange + text = b"abc\0xyz" + + # Act + output = utils.unzfill(text) + + # Assert + self.assertEqual(output, b"abc") + + def test_unzfill_without_nul(self): + # Arrange + text = b"abcxyz" + + # Act + output = utils.unzfill(text) + + # Assert + self.assertEqual(output, b"abcxyz") + + +class UtilsGetDateTestCase(unittest.TestCase): + """dbfpy.utils.getDate test cases.""" + + def test_getDate_none(self): + # Arrange + value = None + + # Act + output = utils.getDate(value) + + # Assert + self.assertIsInstance(output, datetime.date) + + def test_getDate_datetime_date(self): + # Arrange + value = datetime.date(2019, 10, 19) + + # Act + output = utils.getDate(value) + + # Assert + self.assertIsInstance(output, datetime.date) + self.assertEqual(output, value) + + def test_getDate_datetime_datetime(self): + # Arrange + value = datetime.datetime(2019, 10, 19, 12, 00, 00) + + # Act + output = utils.getDate(value) + + # Assert + self.assertIsInstance(output, datetime.date) + self.assertEqual(output, value) + + def test_getDate_datetime_timestamp(self): + # Arrange + value = 1571515306 + + # Act + output = utils.getDate(value) + + # Assert + self.assertIsInstance(output, datetime.date) + self.assertEqual(output, datetime.date(2019, 10, 19)) + + def test_getDate_datetime_string_yyyy_mm_dd(self): + # Arrange + value = "20191019" + + # Act + output = utils.getDate(value) + + # Assert + self.assertIsInstance(output, datetime.date) + self.assertEqual(output, datetime.date(2019, 10, 19)) + + def test_getDate_datetime_string_yymmdd(self): + # Arrange + value = "191019" + + # Act + output = utils.getDate(value) + + # Assert + self.assertIsInstance(output, datetime.date) + self.assertEqual(output, datetime.date(2019, 10, 19)) + + +class UtilsGetDateTimeTestCase(unittest.TestCase): + """dbfpy.utils.getDateTime test cases.""" + + def test_getDateTime_none(self): + # Arrange + value = None + + # Act + output = utils.getDateTime(value) + + # Assert + self.assertIsInstance(output, datetime.datetime) + + def test_getDateTime_datetime_datetime(self): + # Arrange + value = datetime.datetime(2019, 10, 19, 12, 00, 00) + + # Act + output = utils.getDateTime(value) + + # Assert + self.assertIsInstance(output, datetime.date) + self.assertEqual(output, value) + + def test_getDateTime_datetime_date(self): + # Arrange + value = datetime.date(2019, 10, 19) + + # Act + output = utils.getDateTime(value) + + # Assert + self.assertIsInstance(output, datetime.date) + self.assertEqual(output, datetime.datetime(2019, 10, 19, 00, 00)) + + def test_getDateTime_datetime_timestamp(self): + # Arrange + value = 1571515306 + + # Act + output = utils.getDateTime(value) + + # Assert + self.assertIsInstance(output, datetime.datetime) + + def test_getDateTime_datetime_string(self): + # Arrange + value = "20191019" + + # Act / Assert + with self.assertRaises(NotImplementedError): + output = utils.getDateTime(value) + + +class InvalidValueTestCase(unittest.TestCase): + """dbfpy.utils._InvalidValue test cases.""" + + def test_sanity(self): + # Arrange + INVALID_VALUE = utils.INVALID_VALUE + + # Act / Assert + self.assertEqual(INVALID_VALUE, INVALID_VALUE) + self.assertNotEqual(INVALID_VALUE, 123) + self.assertEqual(int(INVALID_VALUE), 0) + self.assertEqual(float(INVALID_VALUE), 0.0) + self.assertEqual(str(INVALID_VALUE), "") + self.assertEqual(repr(INVALID_VALUE), "") From b8bff1190e97184f475f7c9834123274599bcca4 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 20 Oct 2019 12:34:34 +0300 Subject: [PATCH 7/7] Don't omit tests from coverage https://nedbatchelder.com/blog/201908/dont_omit_tests_from_coverage.html --- pytest.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index f69d336..2288651 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,4 @@ [pytest] norecursedirs = .git .* -addopts = -rsxX --showlocals --tb=native --cov=tablib --cov-report xml --cov-report term --cov-report html +addopts = -rsxX --showlocals --tb=native --cov=tablib --cov=tests --cov-report xml --cov-report term --cov-report html python_paths = .