From 691f97ef25f4e742968df6d933c86348e97c555b Mon Sep 17 00:00:00 2001 From: Conrad Schloer Date: Thu, 18 Jun 2020 14:23:54 +0200 Subject: [PATCH 01/14] Fix issue with multicode number formats (#333) --- data/special/number_format_multicode.xlsx | Bin 0 -> 4885 bytes tabulator/parsers/xlsx.py | 9 ++++++++- tests/formats/test_xlsx.py | 10 ++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 data/special/number_format_multicode.xlsx diff --git a/data/special/number_format_multicode.xlsx b/data/special/number_format_multicode.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..6d3b8f009f87024ce726d52f377dfe86b94b195d GIT binary patch literal 4885 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 literal 0 HcmV?d00001 diff --git a/tabulator/parsers/xlsx.py b/tabulator/parsers/xlsx.py index c6eadb8d..18d8a64d 100644 --- a/tabulator/parsers/xlsx.py +++ b/tabulator/parsers/xlsx.py @@ -360,7 +360,14 @@ def convert_excel_number_format_string(excel_number, value): percentage = True if excel_number == "General": return value - code = excel_number.split(".") + multi_codes = excel_number.split(";") + if value < 0 and len(multi_codes) > 1: + excel_number = multi_codes[1] + else: + excel_number = multi_codes[0] + + code = excel_number.split('.') + if len(code) > 2: return None if len(code) < 2: diff --git a/tests/formats/test_xlsx.py b/tests/formats/test_xlsx.py index 6c6f2108..e6bded68 100644 --- a/tests/formats/test_xlsx.py +++ b/tests/formats/test_xlsx.py @@ -121,6 +121,16 @@ def test_stream_xlsx_preserve_formatting_percentage(): ] + +def test_stream_xlsx_preserve_formatting_number_multicode(): + source = "data/special/number_format_multicode.xlsx" + with Stream( + source, headers=1, ignore_blank_headers=True, preserve_formatting=True + ) as stream: + assert stream.read() == [["4.5"], ["-9.032"], ["15.8"]] + + + def test_stream_xlsx_workbook_cache(): workbook_cache = {} source = BASE_URL % "data/special/sheets.xlsx" From 3936fcedb0afcb1841cc4bc412d1e7bcf426a2e7 Mon Sep 17 00:00:00 2001 From: roll Date: Thu, 18 Jun 2020 15:24:43 +0300 Subject: [PATCH 02/14] v1.52.2 --- tabulator/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabulator/VERSION b/tabulator/VERSION index 154cb93b..52952b7f 100644 --- a/tabulator/VERSION +++ b/tabulator/VERSION @@ -1 +1 @@ -1.52.1 +1.52.2 From e315425410114e35a910566cb326884b4d9158dd Mon Sep 17 00:00:00 2001 From: roll Date: Thu, 18 Jun 2020 15:54:30 +0300 Subject: [PATCH 03/14] Fixed pick_fields for keyed sources --- tabulator/stream.py | 8 ++------ tests/test_stream.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/tabulator/stream.py b/tabulator/stream.py index aa983526..723dd019 100644 --- a/tabulator/stream.py +++ b/tabulator/stream.py @@ -888,18 +888,14 @@ def builtin_processor(extended_rows): if headers and self.__headers: keyed_row = dict(zip(headers, row)) row = [keyed_row.get(header) for header in self.__headers] + elif self.__ignored_headers_indexes: + row = [value for index, value in enumerate(row) if index not in self.__ignored_headers_indexes] headers = self.__headers # Skip rows by numbers/comments if self.__check_if_row_for_skipping(row_number, headers, row): continue - # Ignore headers - if self.__ignored_headers_indexes: - for index in self.__ignored_headers_indexes: - if index < len(row): - row = row[:index] + row[index+1:] - yield (row_number, headers, row) # Skip nagative rows processor diff --git a/tests/test_stream.py b/tests/test_stream.py index 128a15fd..56276663 100644 --- a/tests/test_stream.py +++ b/tests/test_stream.py @@ -492,6 +492,22 @@ def test_stream_pick_fields_position_and_prefix(): ] +def test_stream_pick_fields_keyed_source(): + source = [{'id': 1, 'name': 'london'}, {'id': 2, 'name': 'paris'}] + with Stream(source, headers=1, skip_fields=['id']) as stream: + assert stream.headers == ['name'] + assert stream.read() == [['london'], ['paris']] + with Stream(source, headers=1, skip_fields=[1]) as stream: + assert stream.headers == ['name'] + assert stream.read() == [['london'], ['paris']] + with Stream(source, headers=1, skip_fields=['name']) as stream: + assert stream.headers == ['id'] + assert stream.read() == [[1], [2]] + with Stream(source, headers=1, skip_fields=[2]) as stream: + assert stream.headers == ['id'] + assert stream.read() == [[1], [2]] + + def test_stream_limit_fields(): source = 'text://header1,header2,header3\nvalue1,value2,value3' with Stream(source, format='csv', headers=1, limit_fields=1) as stream: From 27252c78ab476dc1e2b20896d88b293f2e43a454 Mon Sep 17 00:00:00 2001 From: roll Date: Thu, 18 Jun 2020 15:55:05 +0300 Subject: [PATCH 04/14] v1.52.3 --- tabulator/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabulator/VERSION b/tabulator/VERSION index 52952b7f..12dcd985 100644 --- a/tabulator/VERSION +++ b/tabulator/VERSION @@ -1 +1 @@ -1.52.2 +1.52.3 From 3f881a23cbdc21abd4b61e7f37eb29e2fe95212b Mon Sep 17 00:00:00 2001 From: roll Date: Sun, 28 Jun 2020 10:16:05 +0300 Subject: [PATCH 05/14] Added more field tests --- data/matrix.csv | 5 +++++ tests/test_stream.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 data/matrix.csv diff --git a/data/matrix.csv b/data/matrix.csv new file mode 100644 index 00000000..1fc260cc --- /dev/null +++ b/data/matrix.csv @@ -0,0 +1,5 @@ +f1,f2,f3,f4 +11,12,13,14 +21,22,23,24 +31,32,33,34 +41,42,43,44 diff --git a/tests/test_stream.py b/tests/test_stream.py index 56276663..7d53a9c2 100644 --- a/tests/test_stream.py +++ b/tests/test_stream.py @@ -538,6 +538,48 @@ def test_stream_limit_offset_fields(): ] +def test_stream_matrix_pick_fields(): + with Stream('data/matrix.csv', headers=1, pick_fields=[2, 'f3']) as stream: + assert stream.headers == ['f2', 'f3'] + assert stream.read() == [['12', '13'], ['22', '23'], ['32', '33'], ['42', '43']] + + +def test_stream_matrix_pick_fields_regex(): + with Stream('data/matrix.csv', headers=1, pick_fields=[{'type': 'regex', 'value': 'f[23]'}]) as stream: + assert stream.headers == ['f2', 'f3'] + assert stream.read() == [['12', '13'], ['22', '23'], ['32', '33'], ['42', '43']] + + +def test_stream_matrix_skip_fields(): + with Stream('data/matrix.csv', headers=1, skip_fields=[1, 'f4']) as stream: + assert stream.headers == ['f2', 'f3'] + assert stream.read() == [['12', '13'], ['22', '23'], ['32', '33'], ['42', '43']] + + +def test_stream_matrix_skip_fields_regex(): + with Stream('data/matrix.csv', headers=1, skip_fields=[{'type': 'regex', 'value': 'f[14]'}]) as stream: + assert stream.headers == ['f2', 'f3'] + assert stream.read() == [['12', '13'], ['22', '23'], ['32', '33'], ['42', '43']] + + +def test_stream_matrix_limit_fields(): + with Stream('data/matrix.csv', headers=1, limit_fields=1) as stream: + assert stream.headers == ['f1'] + assert stream.read() == [['11'], ['21'], ['31'], ['41']] + + +def test_stream_matrix_offset_fields(): + with Stream('data/matrix.csv', headers=1, offset_fields=3) as stream: + assert stream.headers == ['f4'] + assert stream.read() == [['14'], ['24'], ['34'], ['44']] + + +def test_stream_matrix_limit_and_offset_fields(): + with Stream('data/matrix.csv', headers=1, limit_fields=2, offset_fields=1) as stream: + assert stream.headers == ['f2', 'f3'] + assert stream.read() == [['12', '13'], ['22', '23'], ['32', '33'], ['42', '43']] + + # Pick/skip/limit/offset rows def test_stream_pick_rows(): From 39331db588910eb267056849c69443b965162d8a Mon Sep 17 00:00:00 2001 From: roll Date: Sat, 12 Sep 2020 12:02:26 +0300 Subject: [PATCH 06/14] Fixed Travis/py2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b3d32f80..7de8c7cd 100644 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ def read(*paths): TESTS_REQUIRE = [ 'mock', 'pylama', - 'pytest', + 'pytest<5', 'pytest-cov', 'moto[server]', 'tox', From 0622d04be5325021526230d0ba5fbb39ac61e5ab Mon Sep 17 00:00:00 2001 From: roll Date: Sat, 12 Sep 2020 12:08:47 +0300 Subject: [PATCH 07/14] Fixed Travis/py2 --- .travis.yml | 2 ++ setup.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ed67707d..4bd77c0b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,8 @@ env: - TOXENV="py${PYTHON_VERSION//./}" install: + # Requried by: travis/py2 + - pip install pyrsistent==0.16 - make install - pip install coveralls diff --git a/setup.py b/setup.py index 7de8c7cd..b3d32f80 100644 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ def read(*paths): TESTS_REQUIRE = [ 'mock', 'pylama', - 'pytest<5', + 'pytest', 'pytest-cov', 'moto[server]', 'tox', From 5dba5408b483c9d47b7e684a42d25f241024404e Mon Sep 17 00:00:00 2001 From: roll Date: Sat, 12 Sep 2020 12:19:48 +0300 Subject: [PATCH 08/14] Reverted Travis/py2 fix --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4bd77c0b..ed67707d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,8 +18,6 @@ env: - TOXENV="py${PYTHON_VERSION//./}" install: - # Requried by: travis/py2 - - pip install pyrsistent==0.16 - make install - pip install coveralls From 40f832b89e91c0013622ba2a08cf0c434007dba8 Mon Sep 17 00:00:00 2001 From: roll Date: Sat, 12 Sep 2020 13:26:26 +0300 Subject: [PATCH 09/14] Fixed Travis/py2 --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index b3d32f80..25bfa309 100644 --- a/setup.py +++ b/setup.py @@ -59,6 +59,9 @@ def read(*paths): 'pylama', 'pytest', 'pytest-cov', + # NOTE: Can be removed after a fix: + # https://github.com/tobgu/pyrsistent/issues/208 + 'pyrsistent<0.17', 'moto[server]', 'tox', ] From a80dd130fb8acd2924cc305b39856a2ef86fa268 Mon Sep 17 00:00:00 2001 From: roll Date: Sat, 12 Sep 2020 13:28:59 +0300 Subject: [PATCH 10/14] Fixed Travis/py2 --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 25bfa309..532c76b2 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,9 @@ def read(*paths): # Prepare PACKAGE = 'tabulator' INSTALL_REQUIRES = [ + # NOTE: Can be removed after a fix: + # https://github.com/tobgu/pyrsistent/issues/208 + 'pyrsistent<0.17', # General 'six>=1.9', 'click>=6.0', @@ -59,9 +62,6 @@ def read(*paths): 'pylama', 'pytest', 'pytest-cov', - # NOTE: Can be removed after a fix: - # https://github.com/tobgu/pyrsistent/issues/208 - 'pyrsistent<0.17', 'moto[server]', 'tox', ] From b6aebb0090e4b2b7e70e116475ada47ea754b7b1 Mon Sep 17 00:00:00 2001 From: roll Date: Sat, 12 Sep 2020 13:32:22 +0300 Subject: [PATCH 11/14] Disable Travis/py2 for now --- .travis.yml | 4 +++- setup.py | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index ed67707d..96696c72 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,9 @@ language: python python: - - 2.7 + # NOTE: Recover after a fix: + # https://github.com/tobgu/pyrsistent/issues/208 + # - 2.7 - 3.6 - 3.7 - 3.8 diff --git a/setup.py b/setup.py index 532c76b2..b3d32f80 100644 --- a/setup.py +++ b/setup.py @@ -20,9 +20,6 @@ def read(*paths): # Prepare PACKAGE = 'tabulator' INSTALL_REQUIRES = [ - # NOTE: Can be removed after a fix: - # https://github.com/tobgu/pyrsistent/issues/208 - 'pyrsistent<0.17', # General 'six>=1.9', 'click>=6.0', From f89e06493259866a9eec2cd56ecdba99da064ec8 Mon Sep 17 00:00:00 2001 From: roll Date: Sat, 26 Sep 2020 10:51:18 +0300 Subject: [PATCH 12/14] Added frictionless note to the readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 6d238fb8..a9bc1131 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,11 @@ A library for reading and writing tabular data (csv/xls/json/etc). +> **[Important Notice]** We have released our new data framework called `frictionless`. This framework is logical continuation of `tabulator` that was extended to be a complete data solution. The change in not breaking for the existing software so no actions are required. Please read the [Migration Guide](https://github.com/frictionlessdata/frictionless-py/blob/master/docs/target/migration-guide/README.md) to migrate from `tabulator` to Frictionless Framework. +> - we continue to bug-fix `tabulator@1.x` in this [repository](https://github.com/frictionlessdata/tabulator-py) as well as it's available on [PyPi](https://pypi.org/project/tabulator/) as it was before +> - please note that `frictionless@3.x` version's API, we're working on at the moment, is not stable +> - we will release `frictionless@4.x` by the end of 2020 to be the first SemVer/stable version + ## Features - **Supports most common tabular formats**: CSV, XLS, ODS, JSON, Google Sheets, SQL, and others. See complete list [below](#supported-file-formats). From ae8428d1fb355db303f5ecda6b1ae88d0ee5e4ab Mon Sep 17 00:00:00 2001 From: roll Date: Sat, 26 Sep 2020 10:54:07 +0300 Subject: [PATCH 13/14] Updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a9bc1131..b1c26b6c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A library for reading and writing tabular data (csv/xls/json/etc). -> **[Important Notice]** We have released our new data framework called `frictionless`. This framework is logical continuation of `tabulator` that was extended to be a complete data solution. The change in not breaking for the existing software so no actions are required. Please read the [Migration Guide](https://github.com/frictionlessdata/frictionless-py/blob/master/docs/target/migration-guide/README.md) to migrate from `tabulator` to Frictionless Framework. +> **[Important Notice]** We have released our new data framework called `frictionless`. This framework is logical continuation of `tabulator` that was extended to be a complete data solution. The change in not breaking for the existing software so no actions are required. Please read the [Migration Guide](https://github.com/frictionlessdata/frictionless-py/blob/master/docs/target/migration-guide/README.md) from `tabulator` to Frictionless Framework. > - we continue to bug-fix `tabulator@1.x` in this [repository](https://github.com/frictionlessdata/tabulator-py) as well as it's available on [PyPi](https://pypi.org/project/tabulator/) as it was before > - please note that `frictionless@3.x` version's API, we're working on at the moment, is not stable > - we will release `frictionless@4.x` by the end of 2020 to be the first SemVer/stable version From 9580d8d8ce08371d2ad8ac98fbd09a9b08a3e5cb Mon Sep 17 00:00:00 2001 From: roll Date: Sat, 26 Sep 2020 10:58:13 +0300 Subject: [PATCH 14/14] Updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b1c26b6c..c31f44f8 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A library for reading and writing tabular data (csv/xls/json/etc). -> **[Important Notice]** We have released our new data framework called `frictionless`. This framework is logical continuation of `tabulator` that was extended to be a complete data solution. The change in not breaking for the existing software so no actions are required. Please read the [Migration Guide](https://github.com/frictionlessdata/frictionless-py/blob/master/docs/target/migration-guide/README.md) from `tabulator` to Frictionless Framework. +> **[Important Notice]** We have released [Frictionless Framework](https://github.com/frictionlessdata/frictionless-py). This framework is logical continuation of `tabulator` that was extended to be a complete data solution. The change in not breaking for the existing software so no actions are required. Please read the [Migration Guide](https://github.com/frictionlessdata/frictionless-py/blob/master/docs/target/migration-guide/README.md) from `tabulator` to Frictionless Framework. > - we continue to bug-fix `tabulator@1.x` in this [repository](https://github.com/frictionlessdata/tabulator-py) as well as it's available on [PyPi](https://pypi.org/project/tabulator/) as it was before > - please note that `frictionless@3.x` version's API, we're working on at the moment, is not stable > - we will release `frictionless@4.x` by the end of 2020 to be the first SemVer/stable version