From 626a06274750669b12f794c6800c1abcfd48a0a6 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 7 Nov 2019 23:05:40 +0100 Subject: [PATCH] Fixes #421 - Make all dependencies optional Thanks Hugo van Kemenade for the review. --- HISTORY.md | 2 ++ docs/formats.rst | 18 +++++++++++- docs/install.rst | 20 ++++++++++++- setup.py | 17 ++++------- src/tablib/core.py | 27 +++++------------- src/tablib/exceptions.py | 18 ++++++++++++ src/tablib/formats/__init__.py | 52 ++++++++++++++++++++++++++++++---- tests/test_tablib.py | 13 ++++++++- 8 files changed, 127 insertions(+), 40 deletions(-) create mode 100644 src/tablib/exceptions.py diff --git a/HISTORY.md b/HISTORY.md index 7d26335..f3a1a34 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -5,6 +5,8 @@ ### Breaking changes - Dropped Python 2 support +- Dependencies are now all optional. To install `tablib` as before with all + possible supported formats, run `pip install tablib[all]` ### Improvements diff --git a/docs/formats.rst b/docs/formats.rst index 6dac434..28ed5f9 100644 --- a/docs/formats.rst +++ b/docs/formats.rst @@ -51,7 +51,8 @@ Import/export using the dBASE_ format. df (DataFrame) ============== -Import/export using the pandas_ DataFrame format. +Import/export using the pandas_ DataFrame format. This format is optional, +install Tablib with ``pip install tablib[pandas]`` to make the format available. .. _pandas: https://pandas.pydata.org/ @@ -62,6 +63,9 @@ The ``html`` format is currently export-only. The exports produce an HTML page with the data in a ````. If headers have been set, they will be used as table headers. +This format is optional, install Tablib with ``pip install tablib[html]`` to +make the format available. + jira ==== @@ -97,6 +101,9 @@ ods Export data in OpenDocument Spreadsheet format. The ``ods`` format is currently export-only. +This format is optional, install Tablib with ``pip install tablib[ods]`` to +make the format available. + .. admonition:: Binary Warning :class:`Dataset.ods` contains binary data, so make sure to write in binary mode:: @@ -145,6 +152,9 @@ xls Import/export data in Legacy Excel Spreadsheet representation. +This format is optional, install Tablib with ``pip install tablib[xls]`` to +make the format available. + .. note:: XLS files are limited to a maximum of 65,000 rows. Use xlsx_ to avoid this @@ -162,6 +172,9 @@ xlsx Import/export data in Excel 07+ Spreadsheet representation. +This format is optional, install Tablib with ``pip install tablib[xlsx]`` to +make the format available. + .. admonition:: Binary Warning The `xlsx` file format is binary, so make sure to write in binary mode:: @@ -179,4 +192,7 @@ returned instead. Import assumes (for now) that headers exist. +This format is optional, install Tablib with ``pip install tablib[yaml]`` to +make the format available. + .. _YAML: https://yaml.org diff --git a/docs/install.rst b/docs/install.rst index b42fe07..62e486d 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -19,8 +19,26 @@ Of course, the recommended way to install Tablib is with `pip =2.4.0', - 'markuppy', - 'xlrd', - 'xlwt', - 'pyyaml', -] - - setup( name='tablib', use_scm_version=True, @@ -42,8 +32,13 @@ setup( 'Programming Language :: Python :: 3.8', ], python_requires='>=3.5', - install_requires=install, extras_require={ + 'all': ['markuppy', 'odfpy', 'openpyxl>=2.4.0', 'pandas', 'pyyaml', 'xlrd', 'xlwt'], + 'html': ['markuppy'], + 'ods': ['odfpy'], 'pandas': ['pandas'], + 'xls': ['xlrd', 'xlwt'], + 'xlsx': ['openpyxl>=2.4.0'], + 'yaml': ['pyyaml'], }, ) diff --git a/src/tablib/core.py b/src/tablib/core.py index d17e1e8..9fcbebd 100644 --- a/src/tablib/core.py +++ b/src/tablib/core.py @@ -13,6 +13,13 @@ from copy import copy from operator import itemgetter from tablib import formats +from tablib.exceptions import ( + HeadersNeeded, + InvalidDatasetIndex, + InvalidDatasetType, + InvalidDimensions, + UnsupportedFormat, +) from tablib.formats import registry __title__ = 'tablib' @@ -903,24 +910,4 @@ def import_book(stream, format=None, **kwargs): return Databook().load(stream, format, **kwargs) -class InvalidDatasetType(Exception): - "Only Datasets can be added to a DataBook" - - -class InvalidDimensions(Exception): - "Invalid size" - - -class InvalidDatasetIndex(Exception): - "Outside of Dataset size" - - -class HeadersNeeded(Exception): - "Header parameter must be given when appending a column in this Dataset." - - -class UnsupportedFormat(NotImplementedError): - "Format is not supported" - - registry.register_builtins() diff --git a/src/tablib/exceptions.py b/src/tablib/exceptions.py new file mode 100644 index 0000000..dee2b29 --- /dev/null +++ b/src/tablib/exceptions.py @@ -0,0 +1,18 @@ +class InvalidDatasetType(Exception): + "Only Datasets can be added to a DataBook" + + +class InvalidDimensions(Exception): + "Invalid size" + + +class InvalidDatasetIndex(Exception): + "Outside of Dataset size" + + +class HeadersNeeded(Exception): + "Header parameter must be given when appending a column in this Dataset." + + +class UnsupportedFormat(NotImplementedError): + "Format is not supported" diff --git a/src/tablib/formats/__init__.py b/src/tablib/formats/__init__.py index a462ce0..c324484 100644 --- a/src/tablib/formats/__init__.py +++ b/src/tablib/formats/__init__.py @@ -2,6 +2,9 @@ """ from collections import OrderedDict from functools import partialmethod +from importlib.util import find_spec + +from tablib.exceptions import UnsupportedFormat from ._csv import CSVFormat from ._dbf import DBFFormat @@ -17,6 +20,33 @@ from ._xls import XLSFormat from ._xlsx import XLSXFormat from ._yaml import YAMLFormat +uninstalled_format_messages = { + 'df': ( + "The 'df' format is not available. You may want to install the pandas " + "package (or `pip install tablib[pandas]`)." + ), + 'html': ( + "The 'html' format is not available. You may want to install the MarkupPy " + "package (or `pip install tablib[html]`)." + ), + 'ods': ( + "The 'ods' format is not available. You may want to install the odfpy " + "package (or `pip install tablib[ods]`)." + ), + 'xls': ( + "The 'xls' format is not available. You may want to install the xlrd and " + "xlwt packages (or `pip install tablib[xls]`)." + ), + 'xlsx': ( + "The 'xlsx' format is not available. You may want to install the openpyxl " + "package (or `pip install tablib[xlsx]`)." + ), + 'yaml': ( + "The 'yaml' format is not available. You may want to install the pyyaml " + "package (or `pip install tablib[yaml]`)." + ), +} + class Registry: _formats = OrderedDict() @@ -53,17 +83,23 @@ class Registry: # Registration ordering matters for autodetection. self.register('json', JSONFormat()) # xlsx before as xls (xlrd) can also read xlsx - self.register('xlsx', XLSXFormat()) - self.register('xls', XLSFormat()) - self.register('yaml', YAMLFormat()) + if find_spec('openpyxl'): + self.register('xlsx', XLSXFormat()) + if find_spec('xlrd') and find_spec('xlwt'): + self.register('xls', XLSFormat()) + if find_spec('yaml'): + self.register('yaml', YAMLFormat()) self.register('csv', CSVFormat()) self.register('tsv', TSVFormat()) - self.register('ods', ODSFormat()) + if find_spec('odf'): + self.register('ods', ODSFormat()) self.register('dbf', DBFFormat()) - self.register('html', HTMLFormat()) + if find_spec('MarkupPy'): + self.register('html', HTMLFormat()) self.register('jira', JIRAFormat()) self.register('latex', LATEXFormat()) - self.register('df', DataFrameFormat()) + if find_spec('pandas'): + self.register('df', DataFrameFormat()) self.register('rst', ReSTFormat()) def formats(self): @@ -71,6 +107,10 @@ class Registry: yield frm def get_format(self, key): + if key not in self._formats: + if key in uninstalled_format_messages: + raise UnsupportedFormat(uninstalled_format_messages[key]) + raise UnsupportedFormat("Tablib has no format '%s' or it is not registered." % key) return self._formats[key] diff --git a/tests/test_tablib.py b/tests/test_tablib.py index 0a01e12..fe793df 100755 --- a/tests/test_tablib.py +++ b/tests/test_tablib.py @@ -12,7 +12,8 @@ from uuid import uuid4 import tablib from MarkupPy import markup -from tablib.core import Row, UnsupportedFormat, detect_format +from tablib.core import Row, detect_format +from tablib.exceptions import UnsupportedFormat from tablib.formats import registry @@ -49,6 +50,16 @@ class TablibTestCase(BaseTestCase): continue dataset.export(format_) + def test_unknown_format(self): + with self.assertRaises(UnsupportedFormat): + data.export('??') + # A known format but uninstalled + del registry._formats['ods'] + msg = (r"The 'ods' format is not available. You may want to install the " + "odfpy package \\(or `pip install tablib\\[ods\\]`\\).") + with self.assertRaisesRegex(UnsupportedFormat, msg): + data.export('ods') + def test_empty_append(self): """Verify append() correctly adds tuple with no headers.""" new_row = (1, 2, 3)