Compare commits

...

10 Commits

Author SHA1 Message Date
dmosberger e8f54811c7 Expose 'read_only' parameter for 'import_set' and 'import_book' (#483) 2020-12-04 10:10:02 +02:00
Nuno André e8774043ed Substitute tuples for dicts in __getstate__/__setstate__ to speed up the pickling 2020-11-29 22:11:46 +01:00
Jannis Leidel dc1729fc6f Move releases to GitHub actions. 2020-11-23 13:14:21 +01:00
Hugo van Kemenade 3dc62685f8 Reduce Travis CI testing (#479) 2020-11-23 11:01:10 +01:00
Hugo van Kemenade 22c88de90d Upload coverage from GHA (#480)
* Upload coverage from GHA

* Fix PytestConfigWarning: Unknown config option: python_paths
2020-11-14 23:51:05 +02:00
Hugo van Kemenade 615e308559 Docs: Add link to changelog/history (#478) 2020-11-12 10:30:57 +02:00
Hugo van Kemenade 8c5404591b Add support for Python 3.9, drop EOL 3.5 (#477) 2020-10-30 19:01:48 +02:00
Hugo van Kemenade 5fa4496f9d Suggest quotes when pip installing with optional dependencies (#474) 2020-08-12 16:12:57 +03:00
Ran Benita bc8438bda4 Stop using pkg_resources
tablib imports pkg_resources in order to find its own version. Importing
pkg_resources is very slow (100ms-250ms is common).

Avoid it by letting setuptools-scm generate a file with the version
instead.
2020-08-10 15:49:51 +02:00
Claude Paroz ce79e44d14 Fixes #469 - Prevented rst crash with only-space strings (#470)
Thanks nexone for the report.
2020-06-15 08:42:51 +03:00
32 changed files with 225 additions and 140 deletions
+21 -5
View File
@@ -2,23 +2,39 @@ name: Docs and lint
on: [push, pull_request] on: [push, pull_request]
env:
FORCE_COLOR: 1
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [3.8]
env: env:
- TOXENV: docs - TOXENV: docs
- TOXENV: lint - TOXENV: lint
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python
uses: actions/setup-python@v1 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: 3.9
- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache
uses: actions/cache@v2
with:
path: ${{ steps.pip-cache.outputs.dir }}
key:
${{ matrix.os }}-${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}
restore-keys: |
${{ matrix.os }}-${{ matrix.python-version }}-v1-
- name: Install dependencies - name: Install dependencies
run: | run: |
+56
View File
@@ -0,0 +1,56 @@
name: Release
on:
push:
branches:
- master
release:
types:
- published
jobs:
build:
if: github.repository == 'jazzband/tablib'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache
uses: actions/cache@v2
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: release-${{ hashFiles('**/setup.py') }}
restore-keys: |
release-
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install -U setuptools twine wheel
- name: Build package
run: |
python setup.py --version
python setup.py sdist --format=gztar bdist_wheel
twine check dist/*
- name: Upload packages to Jazzband
if: github.event.action == 'published'
uses: pypa/gh-action-pypi-publish@master
with:
user: jazzband
password: ${{ secrets.JAZZBAND_RELEASE_KEY }}
repository_url: https://jazzband.co/projects/tablib/upload
+25 -4
View File
@@ -2,24 +2,40 @@ name: Test
on: [push, pull_request] on: [push, pull_request]
env:
FORCE_COLOR: 1
jobs: jobs:
build: build:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: [3.5, 3.6, 3.7, 3.8] python-version: [3.6, 3.7, 3.8, 3.9]
os: [ubuntu-latest, macOS-latest, windows-latest] os: [ubuntu-latest, macOS-latest, windows-latest]
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache
uses: actions/cache@v2
with:
path: ${{ steps.pip-cache.outputs.dir }}
key:
${{ matrix.os }}-${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}
restore-keys: |
${{ matrix.os }}-${{ matrix.python-version }}-v1-
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
@@ -30,3 +46,8 @@ jobs:
shell: bash shell: bash
run: | run: |
tox -e py tox -e py
- name: Upload coverage
uses: codecov/codecov-action@v1
with:
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
+3
View File
@@ -38,3 +38,6 @@ htmlcov
# setuptools noise # setuptools noise
.eggs .eggs
*.egg-info *.egg-info
# generated by setuptools-scm
/src/tablib/_version.py
+6 -6
View File
@@ -1,24 +1,24 @@
repos: repos:
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v1.26.2 rev: v2.7.3
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: ["--py3-plus"] args: ["--py36-plus"]
- repo: https://github.com/pre-commit/mirrors-isort - repo: https://github.com/PyCQA/isort
rev: v4.3.21 rev: 5.6.4
hooks: hooks:
- id: isort - id: isort
additional_dependencies: [toml] additional_dependencies: [toml]
- repo: https://github.com/pre-commit/pygrep-hooks - repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.4.4 rev: v1.7.0
hooks: hooks:
- id: python-check-blanket-noqa - id: python-check-blanket-noqa
- id: rst-backticks - id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0 rev: v3.3.0
hooks: hooks:
- id: check-merge-conflict - id: check-merge-conflict
- id: check-toml - id: check-toml
-38
View File
@@ -1,38 +0,0 @@
language: python
cache:
pip: true
directories:
- $HOME/.cache/pre-commit
matrix:
fast_finish: true
include:
- python: 3.8
env: TOXENV=docs
- python: 3.8
env: TOXENV=lint
- python: 3.8
- python: 3.7
- python: 3.6
install: travis_retry pip install tox-travis
script: tox
after_success:
- |
if [[ "$TOXENV" != "docs" && "$TOXENV" != "lint" ]]; then
bash <(curl -s https://codecov.io/bash)
fi
deploy:
provider: pypi
user: jazzband
server: https://jazzband.co/projects/tablib/upload
distributions: sdist bdist_wheel
password:
secure: svV4fYtodwW+iTyFOm5ISEfhVwcA+6vTskD3x6peznc40TdMV9Ek8nT3Q/NB4lCbXoUw2qR4H6uhLCjesnv/VvVk/qbitCyD8ySlgwOV5n7NzJs8lC8EYaHSjGQjatTwJAokfGVYkPawkI7HXDqtDggLUQBK+Ag8HDW+XBSbQIU=
on:
tags: true
repo: jazzband/tablib
python: 3.7
+15
View File
@@ -1,5 +1,20 @@
# History # History
## Unreleased
### Breaking changes
- Dropped Python 3.5 support
### Improvements
- Added Python 3.9 support
- Added read_only option to xlsx file reader (#482).
### Bugfixes
- Prevented crash in rst export with only-space strings (#469).
## 2.0.0 (2020-05-16) ## 2.0.0 (2020-05-16)
### Breaking changes ### Breaking changes
-1
View File
@@ -4,7 +4,6 @@
[![PyPI version](https://img.shields.io/pypi/v/tablib.svg)](https://pypi.org/project/tablib/) [![PyPI version](https://img.shields.io/pypi/v/tablib.svg)](https://pypi.org/project/tablib/)
[![Supported Python versions](https://img.shields.io/pypi/pyversions/tablib.svg)](https://pypi.org/project/tablib/) [![Supported Python versions](https://img.shields.io/pypi/pyversions/tablib.svg)](https://pypi.org/project/tablib/)
[![PyPI downloads](https://img.shields.io/pypi/dm/tablib.svg)](https://pypistats.org/packages/tablib) [![PyPI downloads](https://img.shields.io/pypi/dm/tablib.svg)](https://pypistats.org/packages/tablib)
[![Travus CI status](https://img.shields.io/travis/jazzband/tablib/master?label=Travis%20CI&logo=travis)](https://travis-ci.org/jazzband/tablib)
[![GitHub Actions status](https://github.com/jazzband/tablib/workflows/Test/badge.svg)](https://github.com/jazzband/tablib/actions) [![GitHub Actions status](https://github.com/jazzband/tablib/workflows/Test/badge.svg)](https://github.com/jazzband/tablib/actions)
[![codecov](https://codecov.io/gh/jazzband/tablib/branch/master/graph/badge.svg)](https://codecov.io/gh/jazzband/tablib) [![codecov](https://codecov.io/gh/jazzband/tablib/branch/master/graph/badge.svg)](https://codecov.io/gh/jazzband/tablib)
[![GitHub](https://img.shields.io/github/license/jazzband/tablib.svg)](LICENSE) [![GitHub](https://img.shields.io/github/license/jazzband/tablib.svg)](LICENSE)
+3 -3
View File
@@ -3,9 +3,9 @@
Jazzband guidelines: https://jazzband.co/about/releases Jazzband guidelines: https://jazzband.co/about/releases
* [ ] Get master to the appropriate code release state. * [ ] Get master to the appropriate code release state.
[Travis CI](https://travis-ci.org/jazzband/tablib) [GitHub Actions](https://github.com/jazzband/tablib/actions)
should pass on master. should pass on master.
[![Build Status](https://travis-ci.org/jazzband/tablib.svg?branch=master)](https://travis-ci.org/jazzband/tablib) [![GitHub Actions status](https://github.com/jazzband/tablib/workflows/Test/badge.svg)](https://github.com/jazzband/tablib/actions)
* [ ] Check [HISTORY.md](https://github.com/jazzband/tablib/blob/master/HISTORY.md), * [ ] Check [HISTORY.md](https://github.com/jazzband/tablib/blob/master/HISTORY.md),
update version number and release date update version number and release date
@@ -16,7 +16,7 @@ git tag -a v0.14.0 -m v0.14.0
git push --tags git push --tags
``` ```
* [ ] Once Travis CI has built and uploaded distributions, check files at * [ ] Once GitHub Actions has built and uploaded distributions, check files at
[Jazzband](https://jazzband.co/projects/tablib) and release to [Jazzband](https://jazzband.co/projects/tablib) and release to
[PyPI](https://pypi.org/pypi/tablib) [PyPI](https://pypi.org/pypi/tablib)
+1
View File
@@ -8,4 +8,5 @@
<li><a href="https://pypi.org/project/tablib">Tablib @ PyPI</a></li> <li><a href="https://pypi.org/project/tablib">Tablib @ PyPI</a></li>
<li><a href="https://github.com/jazzband/tablib">Tablib @ GitHub</a></li> <li><a href="https://github.com/jazzband/tablib">Tablib @ GitHub</a></li>
<li><a href="https://github.com/jazzband/tablib/issues">Issue Tracker</a></li> <li><a href="https://github.com/jazzband/tablib/issues">Issue Tracker</a></li>
<li><a href="https://github.com/jazzband/tablib/blob/master/HISTORY.md">Changelog</a></li>
</ul> </ul>
+3 -3
View File
@@ -9,7 +9,7 @@
# #
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
from pkg_resources import get_distribution import tablib
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
@@ -49,9 +49,9 @@ copyright = '2019 Jazzband'
# built documents. # built documents.
# #
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = get_distribution('tablib').version release = tablib.__version__
# The short X.Y version. # The short X.Y version.
version = '.'.join(release.split('.')[:2]) version = '.'.join(tablib.__version__.split('.')[:2])
# for example take major/minor # for example take major/minor
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
+3 -3
View File
@@ -163,16 +163,16 @@ the easiest way to test your changes for potential issues is to simply run the t
Continuous Integration Continuous Integration
---------------------- ----------------------
Every pull request is automatically tested and inspected upon receipt with `Travis CI`_. Every pull request is automatically tested and inspected upon receipt with `GitHub Actions`_.
If you broke the build, you will receive an email accordingly. If you broke the build, you will receive an email accordingly.
Anyone may view the build status and history at any time. Anyone may view the build status and history at any time.
https://travis-ci.org/jazzband/tablib https://github.com/jazzband/tablib/actions
Additional reports will also be included here in the future, including :pep:`8` checks and stress reports for extremely large datasets. Additional reports will also be included here in the future, including :pep:`8` checks and stress reports for extremely large datasets.
.. _`Travis CI`: https://travis-ci.org/ .. _`GitHub Actions`: https://github.com/jazzband/tablib/actions
.. _docs: .. _docs:
+16 -7
View File
@@ -27,7 +27,7 @@ For example::
dataset.export("cli", tablefmt="github") dataset.export("cli", tablefmt="github")
dataset.export("cli", tablefmt="grid") dataset.export("cli", tablefmt="grid")
This format is optional, install Tablib with ``pip install tablib[cli]`` to This format is optional, install Tablib with ``pip install "tablib[cli]"`` to
make the format available. make the format available.
csv csv
@@ -83,7 +83,7 @@ df (DataFrame)
============== ==============
Import/export using the pandas_ DataFrame format. This format is optional, Import/export using the pandas_ DataFrame format. This format is optional,
install Tablib with ``pip install tablib[pandas]`` to make the format available. install Tablib with ``pip install "tablib[pandas]"`` to make the format available.
.. _pandas: https://pandas.pydata.org/ .. _pandas: https://pandas.pydata.org/
@@ -94,7 +94,7 @@ The ``html`` format is currently export-only. The exports produce an HTML page
with the data in a ``<table>``. If headers have been set, they will be used as with the data in a ``<table>``. If headers have been set, they will be used as
table headers. table headers.
This format is optional, install Tablib with ``pip install tablib[html]`` to This format is optional, install Tablib with ``pip install "tablib[html]"`` to
make the format available. make the format available.
jira jira
@@ -132,7 +132,7 @@ ods
Export data in OpenDocument Spreadsheet format. The ``ods`` format is currently Export data in OpenDocument Spreadsheet format. The ``ods`` format is currently
export-only. export-only.
This format is optional, install Tablib with ``pip install tablib[ods]`` to This format is optional, install Tablib with ``pip install "tablib[ods]"`` to
make the format available. make the format available.
.. admonition:: Binary Warning .. admonition:: Binary Warning
@@ -183,7 +183,7 @@ xls
Import/export data in Legacy Excel Spreadsheet representation. Import/export data in Legacy Excel Spreadsheet representation.
This format is optional, install Tablib with ``pip install tablib[xls]`` to This format is optional, install Tablib with ``pip install "tablib[xls]"`` to
make the format available. make the format available.
.. note:: .. note::
@@ -203,9 +203,18 @@ xlsx
Import/export data in Excel 07+ Spreadsheet representation. Import/export data in Excel 07+ Spreadsheet representation.
This format is optional, install Tablib with ``pip install tablib[xlsx]`` to This format is optional, install Tablib with ``pip install "tablib[xlsx]"`` to
make the format available. make the format available.
The ``import_set()`` and ``import_book()`` methods accept keyword
argument ``read_only``. If its value is ``True`` (the default), the
XLSX data source is read lazily. Lazy reading generally reduces time
and memory consumption, especially for large spreadsheets. However,
it relies on the XLSX data source declaring correct dimensions. Some
programs generate XLSX files with incorrect dimensions. Such files
may need to be loaded with this optimization turned off by passing
``read_only=False``.
.. note:: .. note::
When reading an ``xlsx`` file containing formulas in its cells, Tablib will When reading an ``xlsx`` file containing formulas in its cells, Tablib will
@@ -232,7 +241,7 @@ returned instead.
Import assumes (for now) that headers exist. Import assumes (for now) that headers exist.
This format is optional, install Tablib with ``pip install tablib[yaml]`` to This format is optional, install Tablib with ``pip install "tablib[yaml]"`` to
make the format available. make the format available.
.. _YAML: https://yaml.org .. _YAML: https://yaml.org
+3 -3
View File
@@ -26,19 +26,19 @@ formats available:
.. code-block:: console .. code-block:: console
$ pip install tablib[xlsx] $ pip install "tablib[xlsx]"
Or all possible formats: Or all possible formats:
.. code-block:: console .. code-block:: console
$ pip install tablib[all] $ pip install "tablib[all]"
which is equivalent to: which is equivalent to:
.. code-block:: console .. code-block:: console
$ pip install tablib[html, pandas, ods, xls, xlsx, yaml] $ pip install "tablib[html, pandas, ods, xls, xlsx, yaml]"
------------------- -------------------
Download the Source Download the Source
+1 -1
View File
@@ -57,7 +57,7 @@ THE SOFTWARE.
Pythons Supported Pythons Supported
----------------- -----------------
Python 3.5+ is officially supported. Python 3.6+ is officially supported.
Now, go :ref:`install Tablib <install>`. Now, go :ref:`install Tablib <install>`.
+1 -6
View File
@@ -1,7 +1,2 @@
[tool.isort] [tool.isort]
force_grid_wrap = 0 profile = "black"
include_trailing_comma = true
known_third_party = ["MarkupPy", "odf", "openpyxl", "pkg_resources", "setuptools", "tablib", "xlrd", "xlwt", "yaml"]
line_length = 88
multi_line_output = 3
use_parentheses = true
-1
View File
@@ -1,4 +1,3 @@
[pytest] [pytest]
norecursedirs = .git .* norecursedirs = .git .*
addopts = -rsxX --showlocals --tb=native --cov=tablib --cov=tests --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 = .
+5 -3
View File
@@ -4,7 +4,9 @@ from setuptools import find_packages, setup
setup( setup(
name='tablib', name='tablib',
use_scm_version=True, use_scm_version={
'write_to': 'src/tablib/_version.py',
},
setup_requires=['setuptools_scm'], setup_requires=['setuptools_scm'],
description='Format agnostic tabular data library (XLS, JSON, YAML, CSV)', description='Format agnostic tabular data library (XLS, JSON, YAML, CSV)',
long_description=( long_description=(
@@ -31,12 +33,12 @@ setup(
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
], ],
python_requires='>=3.5', python_requires='>=3.6',
extras_require={ extras_require={
'all': ['markuppy', 'odfpy', 'openpyxl>=2.6.0', 'pandas', 'pyyaml', 'tabulate', 'xlrd', 'xlwt'], 'all': ['markuppy', 'odfpy', 'openpyxl>=2.6.0', 'pandas', 'pyyaml', 'tabulate', 'xlrd', 'xlwt'],
'cli': ['tabulate'], 'cli': ['tabulate'],
+8 -7
View File
@@ -1,5 +1,12 @@
""" Tablib. """ """ Tablib. """
from pkg_resources import DistributionNotFound, get_distribution try:
# Generated by setuptools-scm.
from ._version import version as __version__
except ImportError:
# Some broken installation.
__version__ = None
from tablib.core import ( # noqa: F401 from tablib.core import ( # noqa: F401
Databook, Databook,
Dataset, Dataset,
@@ -10,9 +17,3 @@ from tablib.core import ( # noqa: F401
import_book, import_book,
import_set, import_set,
) )
try:
__version__ = get_distribution(__name__).version
except DistributionNotFound:
# package is not installed
__version__ = None
+22 -30
View File
@@ -57,18 +57,10 @@ class Row:
del self._row[i] del self._row[i]
def __getstate__(self): def __getstate__(self):
return self._row, self.tags
slots = dict()
for slot in self.__slots__:
attribute = getattr(self, slot)
slots[slot] = attribute
return slots
def __setstate__(self, state): def __setstate__(self, state):
for (k, v) in list(state.items()): self._row, self.tags = state
setattr(self, k, v)
def rpush(self, value): def rpush(self, value):
self.insert(len(self._row), value) self.insert(len(self._row), value)
@@ -147,9 +139,9 @@ class Dataset:
.. admonition:: Format Attributes Definition .. admonition:: Format Attributes Definition
If you look at the code, the various output/import formats are not If you look at the code, the various output/import formats are not
defined within the :class:`Dataset` object. To add support for a new format, see defined within the :class:`Dataset` object. To add support for a new format, see
:ref:`Adding New Formats <newformats>`. :ref:`Adding New Formats <newformats>`.
""" """
@@ -299,7 +291,7 @@ class Dataset:
def _get_headers(self): def _get_headers(self):
"""An *optional* list of strings to be used for header rows and attribute names. """An *optional* list of strings to be used for header rows and attribute names.
This must be set manually. The given list length must equal :class:`Dataset.width`. This must be set manually. The given list length must equal :attr:`Dataset.width`.
""" """
return self.__headers return self.__headers
@@ -335,7 +327,7 @@ class Dataset:
set, a list of Python dictionaries will be returned. If no headers have been set, a list of Python dictionaries will be returned. If no headers have been
set, a list of tuples (rows) will be returned instead. set, a list of tuples (rows) will be returned instead.
A dataset object can also be imported by setting the :class:`Dataset.dict` attribute. :: A dataset object can also be imported by setting the :attr:`Dataset.dict` attribute. ::
data = tablib.Dataset() data = tablib.Dataset()
data.dict = [{'age': 90, 'first_name': 'Kenneth', 'last_name': 'Reitz'}] data.dict = [{'age': 90, 'first_name': 'Kenneth', 'last_name': 'Reitz'}]
@@ -414,10 +406,10 @@ class Dataset:
fmt = registry.get_format(format) fmt = registry.get_format(format)
if not hasattr(fmt, 'import_set'): if not hasattr(fmt, 'import_set'):
raise UnsupportedFormat('Format {} cannot be imported.'.format(format)) raise UnsupportedFormat(f'Format {format} cannot be imported.')
if not import_set: if not import_set:
raise UnsupportedFormat('Format {} cannot be imported.'.format(format)) raise UnsupportedFormat(f'Format {format} cannot be imported.')
fmt.import_set(self, stream, **kwargs) fmt.import_set(self, stream, **kwargs)
return self return self
@@ -430,7 +422,7 @@ class Dataset:
""" """
fmt = registry.get_format(format) fmt = registry.get_format(format)
if not hasattr(fmt, 'export_set'): if not hasattr(fmt, 'export_set'):
raise UnsupportedFormat('Format {} cannot be exported.'.format(format)) raise UnsupportedFormat(f'Format {format} cannot be exported.')
return fmt.export_set(self, **kwargs) return fmt.export_set(self, **kwargs)
@@ -452,28 +444,28 @@ class Dataset:
def rpush(self, row, tags=list()): def rpush(self, row, tags=list()):
"""Adds a row to the end of the :class:`Dataset`. """Adds a row to the end of the :class:`Dataset`.
See :class:`Dataset.insert` for additional documentation. See :method:`Dataset.insert` for additional documentation.
""" """
self.insert(self.height, row=row, tags=tags) self.insert(self.height, row=row, tags=tags)
def lpush(self, row, tags=list()): def lpush(self, row, tags=list()):
"""Adds a row to the top of the :class:`Dataset`. """Adds a row to the top of the :class:`Dataset`.
See :class:`Dataset.insert` for additional documentation. See :method:`Dataset.insert` for additional documentation.
""" """
self.insert(0, row=row, tags=tags) self.insert(0, row=row, tags=tags)
def append(self, row, tags=list()): def append(self, row, tags=list()):
"""Adds a row to the :class:`Dataset`. """Adds a row to the :class:`Dataset`.
See :class:`Dataset.insert` for additional documentation. See :method:`Dataset.insert` for additional documentation.
""" """
self.rpush(row, tags) self.rpush(row, tags)
def extend(self, rows, tags=list()): def extend(self, rows, tags=list()):
"""Adds a list of rows to the :class:`Dataset` using """Adds a list of rows to the :class:`Dataset` using
:class:`Dataset.append` :method:`Dataset.append`
""" """
for row in rows: for row in rows:
@@ -515,20 +507,20 @@ class Dataset:
data.append_col(col=random.randint) data.append_col(col=random.randint)
If inserting a column, and :class:`Dataset.headers` is set, the If inserting a column, and :attr:`Dataset.headers` is set, the
header attribute must be set, and will be considered the header for header attribute must be set, and will be considered the header for
that row. that row.
See :ref:`dyncols` for an in-depth example. See :ref:`dyncols` for an in-depth example.
.. versionchanged:: 0.9.0 .. versionchanged:: 0.9.0
If inserting a column, and :class:`Dataset.headers` is set, the If inserting a column, and :attr:`Dataset.headers` is set, the
header attribute must be set, and will be considered the header for header attribute must be set, and will be considered the header for
that row. that row.
.. versionadded:: 0.9.0 .. versionadded:: 0.9.0
If inserting a row, you can add :ref:`tags <tags>` to the row you are inserting. If inserting a row, you can add :ref:`tags <tags>` to the row you are inserting.
This gives you the ability to :class:`filter <Dataset.filter>` your This gives you the ability to :method:`filter <Dataset.filter>` your
:class:`Dataset` later. :class:`Dataset` later.
""" """
@@ -565,14 +557,14 @@ class Dataset:
def rpush_col(self, col, header=None): def rpush_col(self, col, header=None):
"""Adds a column to the end of the :class:`Dataset`. """Adds a column to the end of the :class:`Dataset`.
See :class:`Dataset.insert` for additional documentation. See :method:`Dataset.insert` for additional documentation.
""" """
self.insert_col(self.width, col, header=header) self.insert_col(self.width, col, header=header)
def lpush_col(self, col, header=None): def lpush_col(self, col, header=None):
"""Adds a column to the top of the :class:`Dataset`. """Adds a column to the top of the :class:`Dataset`.
See :class:`Dataset.insert` for additional documentation. See :method:`Dataset.insert` for additional documentation.
""" """
self.insert_col(0, col, header=header) self.insert_col(0, col, header=header)
@@ -596,7 +588,7 @@ class Dataset:
def append_col(self, col, header=None): def append_col(self, col, header=None):
"""Adds a column to the :class:`Dataset`. """Adds a column to the :class:`Dataset`.
See :class:`Dataset.insert_col` for additional documentation. See :method:`Dataset.insert_col` for additional documentation.
""" """
self.rpush_col(col, header) self.rpush_col(col, header)
@@ -875,7 +867,7 @@ class Databook:
fmt = registry.get_format(format) fmt = registry.get_format(format)
if not hasattr(fmt, 'import_book'): if not hasattr(fmt, 'import_book'):
raise UnsupportedFormat('Format {} cannot be loaded.'.format(format)) raise UnsupportedFormat(f'Format {format} cannot be loaded.')
fmt.import_book(self, stream, **kwargs) fmt.import_book(self, stream, **kwargs)
return self return self
@@ -888,7 +880,7 @@ class Databook:
""" """
fmt = registry.get_format(format) fmt = registry.get_format(format)
if not hasattr(fmt, 'export_book'): if not hasattr(fmt, 'export_book'):
raise UnsupportedFormat('Format {} cannot be exported.'.format(format)) raise UnsupportedFormat(f'Format {format} cannot be exported.')
return fmt.export_book(self, **kwargs) return fmt.export_book(self, **kwargs)
+2 -2
View File
@@ -28,7 +28,7 @@ def load_format_class(dotted_path):
module_path, class_name = dotted_path.rsplit('.', 1) module_path, class_name = dotted_path.rsplit('.', 1)
return getattr(import_module(module_path), class_name) return getattr(import_module(module_path), class_name)
except (ValueError, AttributeError) as err: except (ValueError, AttributeError) as err:
raise ImportError("Unable to load format class '{}' ({})".format(dotted_path, err)) raise ImportError(f"Unable to load format class '{dotted_path}' ({err})")
class FormatDescriptorBase: class FormatDescriptorBase:
@@ -122,7 +122,7 @@ class Registry:
if key in uninstalled_format_messages: if key in uninstalled_format_messages:
raise UnsupportedFormat( raise UnsupportedFormat(
"The '{key}' format is not available. You may want to install the " "The '{key}' format is not available. You may want to install the "
"{package_name} (or `pip install tablib[{extras_name}]`).".format( "{package_name} (or `pip install \"tablib[{extras_name}]\"`).".format(
**uninstalled_format_messages[key], key=key **uninstalled_format_messages[key], key=key
) )
) )
+1 -1
View File
@@ -30,7 +30,7 @@ class DataFrameFormat:
if DataFrame is None: if DataFrame is None:
raise NotImplementedError( raise NotImplementedError(
'DataFrame Format requires `pandas` to be installed.' 'DataFrame Format requires `pandas` to be installed.'
' Try `pip install tablib[pandas]`.') ' Try `pip install "tablib[pandas]"`.')
dataframe = DataFrame(dset.dict, columns=dset.headers) dataframe = DataFrame(dset.dict, columns=dset.headers)
return dataframe return dataframe
+1 -1
View File
@@ -55,7 +55,7 @@ class HTMLFormat:
for i, dset in enumerate(databook._datasets): for i, dset in enumerate(databook._datasets):
title = (dset.title if dset.title else 'Set %s' % (i)) title = (dset.title if dset.title else 'Set %s' % (i))
wrapper.write('<{}>{}</{}>\n'.format(cls.BOOK_ENDINGS, title, cls.BOOK_ENDINGS)) wrapper.write(f'<{cls.BOOK_ENDINGS}>{title}</{cls.BOOK_ENDINGS}>\n')
wrapper.write(dset.html) wrapper.write(dset.html)
wrapper.write('\n') wrapper.write('\n')
+1 -1
View File
@@ -21,7 +21,7 @@ class JIRAFormat:
header = cls._get_header(dataset.headers) if dataset.headers else '' header = cls._get_header(dataset.headers) if dataset.headers else ''
body = cls._get_body(dataset) body = cls._get_body(dataset)
return '{}\n{}'.format(header, body) if header else body return f'{header}\n{body}' if header else body
@classmethod @classmethod
def _get_body(cls, dataset): def _get_body(cls, dataset):
+1 -1
View File
@@ -24,7 +24,7 @@ def _max_word_len(text):
>>> _max_word_len('Python Module for Tabular Datasets') >>> _max_word_len('Python Module for Tabular Datasets')
8 8
""" """
return max(len(word) for word in text.split()) if text else 0 return max([len(word) for word in text.split()], default=0) if text else 0
class ReSTFormat: class ReSTFormat:
+2 -1
View File
@@ -3,11 +3,12 @@
from io import BytesIO from io import BytesIO
import tablib
import xlrd import xlrd
import xlwt import xlwt
from xlrd.xldate import xldate_as_datetime from xlrd.xldate import xldate_as_datetime
import tablib
# special styles # special styles
wrap = xlwt.easyxf("alignment: wrap on") wrap = xlwt.easyxf("alignment: wrap on")
bold = xlwt.easyxf("font: bold on") bold = xlwt.easyxf("font: bold on")
+7 -6
View File
@@ -3,13 +3,14 @@
from io import BytesIO from io import BytesIO
import tablib
from openpyxl.reader.excel import ExcelReader, load_workbook from openpyxl.reader.excel import ExcelReader, load_workbook
from openpyxl.styles import Alignment, Font from openpyxl.styles import Alignment, Font
from openpyxl.utils import get_column_letter from openpyxl.utils import get_column_letter
from openpyxl.workbook import Workbook from openpyxl.workbook import Workbook
from openpyxl.writer.excel import ExcelWriter from openpyxl.writer.excel import ExcelWriter
import tablib
class XLSXFormat: class XLSXFormat:
title = 'xlsx' title = 'xlsx'
@@ -58,12 +59,12 @@ class XLSXFormat:
return stream.getvalue() return stream.getvalue()
@classmethod @classmethod
def import_set(cls, dset, in_stream, headers=True): def import_set(cls, dset, in_stream, headers=True, read_only=True):
"""Returns databook from XLS stream.""" """Returns databook from XLS stream."""
dset.wipe() dset.wipe()
xls_book = load_workbook(in_stream, read_only=True, data_only=True) xls_book = load_workbook(in_stream, read_only=read_only, data_only=True)
sheet = xls_book.active sheet = xls_book.active
dset.title = sheet.title dset.title = sheet.title
@@ -76,12 +77,12 @@ class XLSXFormat:
dset.append(row_vals) dset.append(row_vals)
@classmethod @classmethod
def import_book(cls, dbook, in_stream, headers=True): def import_book(cls, dbook, in_stream, headers=True, read_only=True):
"""Returns databook from XLS stream.""" """Returns databook from XLS stream."""
dbook.wipe() dbook.wipe()
xls_book = load_workbook(in_stream, read_only=True, data_only=True) xls_book = load_workbook(in_stream, read_only=read_only, data_only=True)
for sheet in xls_book.worksheets: for sheet in xls_book.worksheets:
data = tablib.Dataset() data = tablib.Dataset()
@@ -114,7 +115,7 @@ class XLSXFormat:
row_number = i + 1 row_number = i + 1
for j, col in enumerate(row): for j, col in enumerate(row):
col_idx = get_column_letter(j + 1) col_idx = get_column_letter(j + 1)
cell = ws['{}{}'.format(col_idx, row_number)] cell = ws[f'{col_idx}{row_number}']
# bold headers # bold headers
if (row_number == 1) and dataset.headers: if (row_number == 1) and dataset.headers:
+2 -1
View File
@@ -1,9 +1,10 @@
""" Tablib - YAML Support. """ Tablib - YAML Support.
""" """
import tablib
import yaml import yaml
import tablib
class YAMLFormat: class YAMLFormat:
title = 'yaml' title = 'yaml'
+1 -1
View File
@@ -315,7 +315,7 @@ class DbfLogicalFieldDef(DbfFieldDef):
return False return False
if value in "YyTt": if value in "YyTt":
return True return True
raise ValueError("[{}] Invalid logical value {!r}".format(self.name, value)) raise ValueError(f"[{self.name}] Invalid logical value {value!r}")
def encodeValue(self, value): def encodeValue(self, value):
"""Return a character from the "TF?" set. """Return a character from the "TF?" set.
Binary file not shown.
+12 -2
View File
@@ -11,8 +11,9 @@ from io import BytesIO, StringIO
from pathlib import Path from pathlib import Path
from uuid import uuid4 from uuid import uuid4
import tablib
from MarkupPy import markup from MarkupPy import markup
import tablib
from tablib.core import Row, detect_format from tablib.core import Row, detect_format
from tablib.exceptions import UnsupportedFormat from tablib.exceptions import UnsupportedFormat
from tablib.formats import registry from tablib.formats import registry
@@ -57,7 +58,7 @@ class TablibTestCase(BaseTestCase):
# A known format but uninstalled # A known format but uninstalled
del registry._formats['ods'] del registry._formats['ods']
msg = (r"The 'ods' format is not available. You may want to install the " msg = (r"The 'ods' format is not available. You may want to install the "
"odfpy package \\(or `pip install tablib\\[ods\\]`\\).") "odfpy package \\(or `pip install \"tablib\\[ods\\]\"`\\).")
with self.assertRaisesRegex(UnsupportedFormat, msg): with self.assertRaisesRegex(UnsupportedFormat, msg):
data.export('ods') data.export('ods')
@@ -659,6 +660,7 @@ class RSTTests(BaseTestCase):
data.headers = self.headers data.headers = self.headers
data.append(self.john) data.append(self.john)
data.append(('Wendy', '', 43)) data.append(('Wendy', '', 43))
data.append(('Esther', ' ', 31))
self.assertEqual( self.assertEqual(
data.export('rst'), data.export('rst'),
'========== ========= ===\n' '========== ========= ===\n'
@@ -666,6 +668,7 @@ class RSTTests(BaseTestCase):
'========== ========= ===\n' '========== ========= ===\n'
'John Adams 90 \n' 'John Adams 90 \n'
'Wendy 43 \n' 'Wendy 43 \n'
'Esther 31 \n'
'========== ========= ===' '========== ========= ==='
) )
@@ -1037,6 +1040,13 @@ class XLSXTests(BaseTestCase):
data = tablib.Dataset().load(fh) data = tablib.Dataset().load(fh)
self.assertEqual(data.headers[0], 'Hello World') self.assertEqual(data.headers[0], 'Hello World')
def test_xlsx_bad_dimensions(self):
"""Test loading file with bad dimension. Must be done with
read_only=False."""
xls_source = Path(__file__).parent / 'files' / 'bad_dimensions.xlsx'
with xls_source.open('rb') as fh:
data = tablib.Dataset().load(fh, read_only=False)
self.assertEqual(data.height, 3)
class JSONTests(BaseTestCase): class JSONTests(BaseTestCase):
def test_json_format_detect(self): def test_json_format_detect(self):
+3 -2
View File
@@ -4,12 +4,14 @@ minversion = 2.4
envlist = envlist =
docs docs
lint lint
py{35,36,37,38} py{36,37,38,39}
[testenv] [testenv]
deps = deps =
-rtests/requirements.txt -rtests/requirements.txt
extras = pandas extras = pandas
passenv =
FORCE_COLOR
commands = commands =
pytest {posargs:tests} pytest {posargs:tests}
@@ -35,4 +37,3 @@ skip_install = true
[flake8] [flake8]
exclude = exclude =
.tox .tox
ignore=E501,E127,E128,E124