diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..0faea60 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +github: chfw +patreon: chfw diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..8996445 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +With your PR, here is a check list: + +- [ ] Has test cases written? +- [ ] Has all code lines tested? +- [ ] Has `make format` been run? +- [ ] Please update CHANGELOG.yml(not CHANGELOG.rst) +- [ ] Has fair amount of documentation if your change is complex +- [ ] Agree on NEW BSD License for your contribution diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..03122a1 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,20 @@ +name: lint + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + name: lint code + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: lint + run: | + pip install flake8 + pip install -r tests/requirements.txt + flake8 --exclude=.moban.d,docs,setup.py --builtins=unicode,xrange,long . + python setup.py checkdocs diff --git a/.github/workflows/moban-update.yml b/.github/workflows/moban-update.yml new file mode 100644 index 0000000..706fd82 --- /dev/null +++ b/.github/workflows/moban-update.yml @@ -0,0 +1,29 @@ +on: [push] + +jobs: + run_moban: + runs-on: ubuntu-latest + name: synchronize templates via moban + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: '3.7' + - name: check changes + run: | + pip install moban gitfs2 pypifs moban-jinja2-github moban-ansible + moban + git status + git diff --exit-code + - name: Auto-commit + if: failure() + uses: docker://cdssnc/auto-commit-github-action + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + args: >- + This is an auto-commit, updating project meta data, + such as changelog.rst, contributors.rst diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml new file mode 100644 index 0000000..9e7ec42 --- /dev/null +++ b/.github/workflows/pythonpublish.yml @@ -0,0 +1,26 @@ +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..5d03855 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,32 @@ +name: run_tests + +on: [push, pull_request] + +jobs: + test: + strategy: + fail-fast: false + matrix: + python-version: [3.6, 3.7, 3.8, 3.9] + os: [macOs-latest, ubuntu-latest, windows-latest] + + runs-on: ${{ matrix.os }} + name: run tests + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: install + run: | + pip install -r requirements.txt + pip install -r tests/requirements.txt + - name: test + run: | + pip freeze + nosetests --verbosity=3 --with-coverage --cover-package pyexcel_ods --cover-package tests tests --with-doctest --doctest-extension=.rst README.rst docs/source pyexcel_ods + - name: Upload coverage + uses: codecov/codecov-action@v1 + with: + name: ${{ matrix.os }} Python ${{ matrix.python-version }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index d98e374..e8b12f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,546 @@ -*.pyc -*~ +# moban hashes +.moban.hashes + +# Extra rules from https://github.com/github/gitignore/ +# Python rules +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ .coverage -pyexcel*-info -build -dist \ No newline at end of file +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# VirtualEnv rules +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +.Python +[Bb]in +[Ii]nclude +[Ll]ib +[Ll]ib64 +[Ll]ocal +[Ss]cripts +pyvenv.cfg +.venv +pip-selfcheck.json + +# Linux rules +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# Windows rules +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# macOS rules +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Emacs rules +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + +# cask packages +.cask/ +dist/ + +# Flycheck +flycheck_*.el + +# server auth directory +/server/ + +# projectiles files +.projectile + +# directory configuration +.dir-locals.el + +# network security +/network-security.data + + +# Vim rules +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +# JetBrains rules +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# SublimeText rules +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Workspace files are user-specific +*.sublime-workspace + +# Project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using Sublime Text +# *.sublime-project + +# SFTP configuration file +sftp-config.json +sftp-config-alt*.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings + +# KDevelop4 rules +*.kdev4 +.kdev4/ + +# Kate rules +# Swap Files # +.*.kate-swp +.swp.* + +# TextMate rules +*.tmproj +*.tmproject +tmtags + +# VisualStudioCode rules +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Xcode rules +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Gcc Patch +/*.gcno + +# Eclipse rules +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ +.apt_generated_test/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +# Uncomment this line if you wish to ignore the project description file. +# Typically, this file would be tracked if it contains build/dependency configurations: +#.project + +# TortoiseGit rules +# Project-level settings +/.tgitconfig + +# Tags rules +# Ignore tags created by etags, ctags, gtags (GNU global) and cscope +TAGS +.TAGS +!TAGS/ +tags +.tags +!tags/ +gtags.files +GTAGS +GRTAGS +GPATH +GSYMS +cscope.files +cscope.out +cscope.in.out +cscope.po.out + + +# remove moban hash dictionary +.moban.hashes diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..9d0ef8b --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,10 @@ +[settings] +line_length=79 +known_first_party=lml, pyexcel_io, odf +known_third_party=nose +indent=' ' +multi_line_output=3 +length_sort=1 +default_section=FIRSTPARTY +no_lines_before=LOCALFOLDER +sections=FUTURE,STDLIB,FIRSTPARTY,THIRDPARTY,LOCALFOLDER diff --git a/.moban.d/README.rst b/.moban.d/README.rst deleted file mode 100644 index e3d0faf..0000000 --- a/.moban.d/README.rst +++ /dev/null @@ -1,16 +0,0 @@ -{%extends 'README.rst.jj2' %} - -{%block description%} -**pyexcel-ods** is a tiny wrapper library to read, manipulate and write data in -ods format using python 2.6 and python 2.7. You are likely to use it with -`pyexcel `_. -`pyexcel-ods3 `_ is a sister library that -does the same thing but supports Python 3.3 and 3.4 and depends on lxml. -{%endblock%} - -{%block extras %} -Credits -================================================================================ - -ODSReader is originally written by `Marco Conti `_ -{%endblock%} diff --git a/.moban.d/custom_README.rst.jj2 b/.moban.d/custom_README.rst.jj2 new file mode 100644 index 0000000..83502eb --- /dev/null +++ b/.moban.d/custom_README.rst.jj2 @@ -0,0 +1,30 @@ +{%extends 'README.rst.jj2' %} + +{%block documentation_link%} +{%endblock%} + +{%block description%} +**pyexcel-ods** is a tiny wrapper library to read, manipulate and write data in +ods format using python 2.6 and python 2.7. You are likely to use it with +`pyexcel `_. +`pyexcel-ods3 `_ is a sister library that +depends on ezodf and lxml. `pyexcel-odsr `_ +is the other sister library that has no external dependency but do ods reading only +{%endblock%} + +{% block pagination_note%} +Special notice 30/01/2017: due to the constraints of the underlying 3rd party +library, it will read the whole file before returning the paginated data. So +at the end of day, the only benefit is less data returned from the reading +function. No major performance improvement will be seen. + +With that said, please install `pyexcel-odsr `_ +and it gives better performance in pagination. +{%endblock%} + +{%block extras %} +Credits +================================================================================ + +ODSReader is originally written by `Marco Conti `_ +{%endblock%} diff --git a/.moban.d/custom_setup.py.jj2 b/.moban.d/custom_setup.py.jj2 new file mode 100644 index 0000000..b276d5a --- /dev/null +++ b/.moban.d/custom_setup.py.jj2 @@ -0,0 +1,7 @@ +{% extends 'setup.py.jj2' %} + +{%block platform_block%} +{%endblock%} + +{%block compat_block%} +{%endblock%} diff --git a/.moban.d/setup.py b/.moban.d/setup.py deleted file mode 100644 index aa56454..0000000 --- a/.moban.d/setup.py +++ /dev/null @@ -1,13 +0,0 @@ -{% extends 'setup.py.jj2' %} - -{%block platform_block%} -{%endblock%} - -{%block compat_block%} -{%endblock%} - -{%block additional_classifiers%} - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7' -{%endblock%}} - diff --git a/.moban.d/tests/requirements.txt b/.moban.d/tests/requirements.txt index 3f0c5aa..c2b0c7e 100644 --- a/.moban.d/tests/requirements.txt +++ b/.moban.d/tests/requirements.txt @@ -1,5 +1,9 @@ {% extends 'tests/requirements.txt.jj2' %} {%block extras %} +moban +black;python_version>="3.6" +isort;python_version>="3.6" +psutil pyexcel pyexcel-xls {%endblock%} diff --git a/.moban.d/tests/test_formatters.py b/.moban.d/tests/test_formatters.py new file mode 100644 index 0000000..919d6fb --- /dev/null +++ b/.moban.d/tests/test_formatters.py @@ -0,0 +1,4 @@ +{% extends 'tests/test_formatters.py.jj2' %} + +{% block test_date_format %} +{% endblock %} diff --git a/.moban.d/travis.yml b/.moban.d/travis.yml deleted file mode 100644 index 0f9b438..0000000 --- a/.moban.d/travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "travis.yml.jj2" %} - -{%block custom_python_versions%} -python: - - 2.6 - - 2.7 - - 3.3 - - 3.4 - - 3.5 -{%endblock%} - diff --git a/.moban.yml b/.moban.yml index 8294aab..46b2d76 100644 --- a/.moban.yml +++ b/.moban.yml @@ -1,17 +1,9 @@ +overrides: "git://github.com/pyexcel/pyexcel-mobans!/mobanfile.yaml" configuration: - configuration_dir: "commons/config" - template_dir: - - "commons/templates" - - ".moban.d" - configuration: pyexcel_ods.yaml + configuration: pyexcel-ods.yml targets: - - README.rst: README.rst - - setup.py: setup.py + - README.rst: custom_README.rst.jj2 + - setup.py: custom_setup.py.jj2 - "docs/source/conf.py": "docs/source/conf.py.jj2" - - .travis.yml: travis.yml - - requirements.txt: requirements.txt - - LICENSE: LICENSE.jj2 + - .gitignore: gitignore.jj2 - MANIFEST.in: MANIFEST.in.jj2 - - "tests/requirements.txt": "tests/requirements.txt" - - test.sh: test.sh.jj2 - - test.bat: test.sh.jj2 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4f15504..0000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -sudo: false -language: python -notifications: - email: false -python: - - 2.6 - - 2.7 - - 3.3 - - 3.4 - - 3.5 -before_install: - - if [[ $TRAVIS_PYTHON_VERSION == "2.6" ]]; then pip install flake8==2.6.2; fi - - if [[ -f min_requirements.txt && "$MINREQ" -eq 1 ]]; then - mv min_requirements.txt requirements.txt ; - fi - - pip install --upgrade setuptools "pip==7.1" - - test ! -f rnd_requirements.txt || pip install --no-deps -r rnd_requirements.txt - - test ! -f rnd_requirements.txt || pip install -r rnd_requirements.txt ; - - pip install -r tests/requirements.txt -script: - - make test -after_success: - codecov diff --git a/CHANGELOG.rst b/CHANGELOG.rst index eaedf8c..c2249ed 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,11 +1,144 @@ Change log ================================================================================ +0.6.0 - 9.10.2020 +-------------------------------------------------------------------------------- + +**added** + +#. new style reader and writer plugins. works with pyexcel-io v0.6.2 + +0.5.6 - 19.03.2019 +-------------------------------------------------------------------------------- + +**added** + +#. `#32 `_, fix odfpy pinning +#. `#33 `_, fix + IntegerAccuracyLossError on i586 + +0.5.5 - 16.03.2019 +-------------------------------------------------------------------------------- + +**added** + +#. `#32 `_, fix odfpy pinning + +0.5.4 - 27.11.2018 +-------------------------------------------------------------------------------- + +**added** + +#. `#30 `_, long type will not + be written in ods. please use string type. And if the integer is equal or + greater than 10 to the power of 16, it will not be written either in ods. In + both situation, IntegerPrecisionLossError will be raised. + +0.5.3 - unreleased +-------------------------------------------------------------------------------- + +**added** + +#. `#24 `_, ignore + comments() in cell +#. `#27 `_, exception raised + when currency type is missing +#. fix odfpy version on 1.3.5. + +0.5.2 - 23.10.2017 +-------------------------------------------------------------------------------- + +**updated** + +#. pyexcel `pyexcel#105 `_, + remove gease from setup_requires, introduced by 0.5.1. +#. remove python2.6 test support + +0.5.1 - 20.10.2017 +-------------------------------------------------------------------------------- + +**added** + +#. `pyexcel#103 `_, include + LICENSE file in MANIFEST.in, meaning LICENSE file will appear in the released + tar ball. + +0.5.0 - 30.08.2017 +-------------------------------------------------------------------------------- + +**Updated** + +#. put dependency on pyexcel-io 0.5.0, which uses cStringIO instead of StringIO. + Hence, there will be performance boost in handling files in memory. + +**Relocated** + +#. All ods type conversion code lives in pyexcel_io.service module + +0.4.1 - 25.08.2017 +-------------------------------------------------------------------------------- + +**Updated** + +#. `pyexcel#23 `_, handle + unseekable stream given by http response +#. PR `#22 `_, hanle white + spaces in a cell. + +0.4.0 - 19.06.2017 +-------------------------------------------------------------------------------- + +**Updated** + +#. `pyexcel#14 `_, close file + handle +#. pyexcel-io plugin interface now updated to use `lml + `_. + +0.3.3 - 07.05.2017 +-------------------------------------------------------------------------------- + +**Updated** + +#. issue `pyexcel#19 `_, not all + texts in a multi-node cell were extracted. + +0.3.2 - 13.04.2017 +-------------------------------------------------------------------------------- + +**Updated** + +#. issue `pyexcel#17 `_, empty new + line is ignored +#. issue `pyexcel#6 `_, + PT288H00M00S is valid duration + +0.3.1 - 02.02.2017 +-------------------------------------------------------------------------------- + +**Added** + +#. Recognize currency type + +0.3.0 - 22.12.2016 +-------------------------------------------------------------------------------- + +**Updated** + +#. Code refactoring with pyexcel-io v 0.3.0 + +0.2.2 - 24.10.2016 +-------------------------------------------------------------------------------- + +**Updated** + +#. issue `pyexcel#14 `_, index + error when reading a ods file that has non-uniform columns repeated property. + 0.2.1 - 31.08.2016 -------------------------------------------------------------------------------- -Added -******************************************************************************** +**Added** #. support pagination. two pairs: start_row, row_limit and start_column, column_limit help you deal with large files. @@ -14,115 +147,103 @@ Added 0.2.0 - 01.06.2016 -------------------------------------------------------------------------------- -Added -******************************************************************************** +**Added** #. By default, `float` will be converted to `int` where fits. `auto_detect_int`, a flag to switch off the autoatic conversion from `float` to `int`. #. 'library=pyexcel-ods' was added so as to inform pyexcel to use it instead of other libraries, in the situation where multiple plugins were installed. - -Updated -******************************************************************************** +**Updated** #. support the auto-import feature of pyexcel-io 0.2.0 - 0.1.1 - 30.01.2016 -------------------------------------------------------------------------------- -Added -******************************************************************************** +**Added** -#. 'streaming' is an extra option given to get_data. Only when 'streaming' - is explicitly set to True, the data will be consisted of generators, - hence will break your existing code. +#. 'streaming' is an extra option given to get_data. Only when 'streaming' is + explicitly set to True, the data will be consisted of generators, hence will + break your existing code. #. uses yield in to_array and returns a generator #. support multi-line text cell #5 #. feature migration from pyexcel-ods3 pyexcel/pyexcel-ods3#5 -Updated -******************************************************************************** -#. compatibility with pyexcel-io 0.1.1 +**Updated** +#. compatibility with pyexcel-io 0.1.1 0.0.12 - 10.10.2015 -------------------------------------------------------------------------------- -Updated -******************************************************************************** -#. Bug fix: excessive trailing columns with empty values +**Updated** +#. Bug fix: excessive trailing columns with empty values 0.0.11 - 26.09.2015 -------------------------------------------------------------------------------- -Updated -******************************************************************************** -#. Complete fix for libreoffice datetime field +**Updated** +#. Complete fix for libreoffice datetime field 0.0.10 - 15.09.2015 -------------------------------------------------------------------------------- -Updated -******************************************************************************** -#. Bug fix: date field could have datetime from libreoffice +**Updated** +#. Bug fix: date field could have datetime from libreoffice 0.0.9 - 21.08.2015 -------------------------------------------------------------------------------- -Updated -******************************************************************************** -#. Bug fix: utf-8 string throw unicode exceptions +**Updated** +#. Bug fix: utf-8 string throw unicode exceptions 0.0.8 - 28.06.2015 -------------------------------------------------------------------------------- -Updated -******************************************************************************** -#. Pin dependency odfpy 0.9.6 to avoid buggy odfpy 1.3.0 +**Updated** +#. Pin dependency odfpy 0.9.6 to avoid buggy odfpy 1.3.0 0.0.7 - 28.05.2015 -------------------------------------------------------------------------------- -Updated -******************************************************************************** -#. Bug fix: "number-columns-repeated" is now respected +**Updated** +#. Bug fix: "number-columns-repeated" is now respected 0.0.6 - 21.05.2015 -------------------------------------------------------------------------------- -Updated -******************************************************************************** +**Updated** + #. get_data and save_data are seen across pyexcel-* extensions. remember them once and use them across all extensions. - 0.0.5 - 22.02.2015 -------------------------------------------------------------------------------- -Added -******************************************************************************** +**Added** #. Loads only one sheet from a multiple sheet book #. Use New BSD License - 0.0.4 - 14.12.2014 -------------------------------------------------------------------------------- -Updated -******************************************************************************** -#. IO interface update as pyexcel-io introduced keywords. +**Updated** +#. IO interface update as pyexcel-io introduced keywords. +#. initial release 0.0.3 - 08.12.2014 -------------------------------------------------------------------------------- +**Updated** + +#. IO interface update as pyexcel-io introduced keywords. #. initial release diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst new file mode 100644 index 0000000..ccdfc96 --- /dev/null +++ b/CONTRIBUTORS.rst @@ -0,0 +1,10 @@ + + +3 contributors +================================================================================ + +In alphabetical order: + +* `Azamat H. Hackimov `_ +* `John Vandenberg `_ +* `Mateusz Konieczny `_ diff --git a/LICENSE b/LICENSE index 6e42f18..a0a4488 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015-2016 by Onni Software Ltd. and its contributors +Copyright (c) 2015-2020 by Onni Software Ltd. and its contributors All rights reserved. Redistribution and use in source and binary forms of the software as well @@ -27,4 +27,4 @@ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH -DAMAGE. \ No newline at end of file +DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in index 5f13ef0..e86ae54 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,6 @@ include README.rst +include LICENSE include CHANGELOG.rst +include CONTRIBUTORS.rst +recursive-include tests * +recursive-include docs * diff --git a/README.rst b/README.rst index 112dd94..b74e157 100644 --- a/README.rst +++ b/README.rst @@ -2,27 +2,72 @@ pyexcel-ods - Let you focus on data, instead of ods format ================================================================================ -.. image:: https://api.travis-ci.org/pyexcel/pyexcel-ods.png - :target: http://travis-ci.org/pyexcel/pyexcel-ods +.. image:: https://raw.githubusercontent.com/pyexcel/pyexcel.github.io/master/images/patreon.png + :target: https://www.patreon.com/chfw -.. image:: https://codecov.io/github/pyexcel/pyexcel-ods/coverage.png - :target: https://codecov.io/github/pyexcel/pyexcel-ods +.. image:: https://raw.githubusercontent.com/pyexcel/pyexcel-mobans/master/images/awesome-badge.svg + :target: https://awesome-python.com/#specific-formats-processing + +.. image:: https://github.com/pyexcel/pyexcel-ods/workflows/run_tests/badge.svg + :target: http://github.com/pyexcel/pyexcel-ods/actions + +.. image:: https://codecov.io/gh/pyexcel/pyexcel-ods/branch/master/graph/badge.svg + :target: https://codecov.io/gh/pyexcel/pyexcel-ods + +.. image:: https://badge.fury.io/py/pyexcel-ods.svg + :target: https://pypi.org/project/pyexcel-ods + + +.. image:: https://pepy.tech/badge/pyexcel-ods/month + :target: https://pepy.tech/project/pyexcel-ods + + +.. image:: https://img.shields.io/gitter/room/gitterHQ/gitter.svg + :target: https://gitter.im/pyexcel/Lobby + +.. image:: https://img.shields.io/static/v1?label=continuous%20templating&message=%E6%A8%A1%E7%89%88%E6%9B%B4%E6%96%B0&color=blue&style=flat-square + :target: https://moban.readthedocs.io/en/latest/#at-scale-continous-templating-for-open-source-projects + +.. image:: https://img.shields.io/static/v1?label=coding%20style&message=black&color=black&style=flat-square + :target: https://github.com/psf/black **pyexcel-ods** is a tiny wrapper library to read, manipulate and write data in ods format using python 2.6 and python 2.7. You are likely to use it with `pyexcel `_. `pyexcel-ods3 `_ is a sister library that -does the same thing but supports Python 3.3 and 3.4 and depends on lxml. +depends on ezodf and lxml. `pyexcel-odsr `_ +is the other sister library that has no external dependency but do ods reading only + +Support the project +================================================================================ + +If your company has embedded pyexcel and its components into a revenue generating +product, please support me on github, `patreon `_ +or `bounty source `_ to maintain +the project and develop it further. + +If you are an individual, you are welcome to support me too and for however long +you feel like. As my backer, you will receive +`early access to pyexcel related contents `_. + +And your issues will get prioritized if you would like to become my patreon as `pyexcel pro user`. + +With your financial support, I will be able to invest +a little bit more time in coding, documentation and writing interesting posts. + Known constraints ================== Fonts, colors and charts are not supported. +Nor to read password protected xls, xlsx and ods files. + Installation ================================================================================ -You can install it via pip: + +You can install pyexcel-ods via pip: .. code-block:: bash @@ -33,7 +78,7 @@ or clone it and install it: .. code-block:: bash - $ git clone http://github.com/pyexcel/pyexcel-ods.git + $ git clone https://github.com/pyexcel/pyexcel-ods.git $ cd pyexcel-ods $ python setup.py install @@ -43,9 +88,6 @@ Usage As a standalone library -------------------------------------------------------------------------------- -Write to an ods file -******************************************************************************** - .. testcode:: :hide: @@ -62,6 +104,11 @@ Write to an ods file ... from collections import OrderedDict +Write to an ods file +******************************************************************************** + + + Here's the sample code to write a dictionary to an ods file: .. code-block:: python @@ -72,6 +119,7 @@ Here's the sample code to write a dictionary to an ods file: >>> data.update({"Sheet 2": [["row 1", "row 2", "row 3"]]}) >>> save_data("your_file.ods", data) + Read from an ods file ******************************************************************************** @@ -105,6 +153,7 @@ Here's the sample code to write a dictionary to an ods file: + Read from an ods from memory ******************************************************************************** @@ -123,6 +172,14 @@ Continue from previous example: Pagination feature ******************************************************************************** +Special notice 30/01/2017: due to the constraints of the underlying 3rd party +library, it will read the whole file before returning the paginated data. So +at the end of day, the only benefit is less data returned from the reading +function. No major performance improvement will be seen. + +With that said, please install `pyexcel-odsr `_ +and it gives better performance in pagination. + Let's assume the following file is a huge ods file: .. code-block:: python @@ -179,16 +236,6 @@ No longer, explicit import is needed since pyexcel version 0.2.2. Instead, this library is auto-loaded. So if you want to read data in ods format, installing it is enough. -Any version under pyexcel 0.2.2, you have to keep doing the following: - -Import it in your file to enable this plugin: - -.. code-block:: python - - from pyexcel.ext import ods - -Please note only pyexcel version 0.0.4+ support this. - Reading from an ods file ******************************************************************************** @@ -198,7 +245,6 @@ Here is the sample code: .. code-block:: python >>> import pyexcel as pe - >>> # from pyexcel.ext import ods >>> sheet = pe.get_book(file_name="your_file.ods") >>> sheet Sheet 1: @@ -269,6 +315,7 @@ You need to pass a StringIO instance to Writer: >>> # In reality, you might give it to your http response >>> # object for downloading + License ================================================================================ @@ -284,7 +331,7 @@ Development steps for code changes Upgrade your setup tools and pip. They are needed for development and testing only: -#. pip install --upgrade setuptools "pip==7.1" +#. pip install --upgrade setuptools pip Then install relevant development requirements: @@ -292,28 +339,16 @@ Then install relevant development requirements: #. pip install -r requirements.txt #. pip install -r tests/requirements.txt +Once you have finished your changes, please provide test case(s), relevant documentation +and update CHANGELOG.rst. -In order to update test environment, and documentation, additional setps are -required: +.. note:: -#. pip install moban -#. git clone https://github.com/pyexcel/pyexcel-commons.git -#. make your changes in `.moban.d` directory, then issue command `moban` + As to rnd_requirements.txt, usually, it is created when a dependent + library is not released. Once the dependecy is installed + (will be released), the future + version of the dependency in the requirements.txt will be valid. -What is rnd_requirements.txt -------------------------------- - -Usually, it is created when a dependent library is not released. Once the dependecy is installed(will be released), the future version of the dependency in the requirements.txt will be valid. - -What is pyexcel-commons ---------------------------------- - -Many information that are shared across pyexcel projects, such as: this developer guide, license info, etc. are stored in `pyexcel-commons` project. - -What is .moban.d ---------------------------------- - -`.moban.d` stores the specific meta data for the library. How to test your contribution ------------------------------ @@ -322,12 +357,23 @@ Although `nose` and `doctest` are both used in code testing, it is adviable that On Linux/Unix systems, please launch your tests like this:: - $ make test + $ make On Windows systems, please issue this command:: > test.bat + +Before you commit +------------------------------ + +Please run:: + + $ make format + +so as to beautify your code otherwise travis-ci may fail your unit test. + + Credits ================================================================================ diff --git a/changelog.yml b/changelog.yml new file mode 100644 index 0000000..1692c01 --- /dev/null +++ b/changelog.yml @@ -0,0 +1,204 @@ +name: pyexcel-ods +organisation: pyexcel +releases: +- changes: + - action: added + details: + - 'new style reader and writer plugins. works with pyexcel-io v0.6.2' + date: 9.10.2020 + version: 0.6.0 +- changes: + - action: added + details: + - '`#32`, fix odfpy pinning' + - '`#33`, fix IntegerAccuracyLossError on i586' + date: 19.03.2019 + version: 0.5.6 +- changes: + - action: added + details: + - '`#32`, fix odfpy pinning' + date: 16.03.2019 + version: 0.5.5 +- changes: + - action: added + details: + - '`#30`, long type will not be written in ods. please use string type. And if the integer is equal or greater than 10 to the power of 16, it will not be written either in ods. In both situation, IntegerPrecisionLossError will be raised.' + date: 27.11.2018 + version: 0.5.4 +- changes: + - action: added + details: + - '`#24`, ignore comments() in cell' + - '`#27`, exception raised when currency type is missing' + - fix odfpy version on 1.3.5. + date: unreleased + version: 0.5.3 +- changes: + - action: updated + details: + - pyexcel `pyexcel#105`, remove gease from setup_requires, introduced by 0.5.1. + - remove python2.6 test support + date: 23.10.2017 + version: 0.5.2 +- changes: + - action: added + details: + - '`pyexcel#103`, include LICENSE file in MANIFEST.in, meaning LICENSE file will + appear in the released tar ball.' + date: 20.10.2017 + version: 0.5.1 +- changes: + - action: Updated + details: + - put dependency on pyexcel-io 0.5.0, which uses cStringIO instead of StringIO. Hence, + there will be performance boost in handling files in memory. + - action: Relocated + details: + - All ods type conversion code lives in pyexcel_io.service module + date: 30.08.2017 + version: 0.5.0 +- changes: + - action: Updated + details: + - '`pyexcel#pyexcel-ods#23`, handle unseekable stream given by http response' + - PR `pyexcel#pyexcel-ods#PR#22`, hanle white spaces in a cell. + date: 25.08.2017 + version: 0.4.1 +- changes: + - action: Updated + details: + - '`pyexcel#pyexcel-xlsx#14`, close file handle' + - pyexcel-io plugin interface now updated to use `lml `_. + date: 19.06.2017 + version: 0.4.0 +- changes: + - action: Updated + details: + - issue `pyexcel#pyexcel-odsr#19`, not all texts in a multi-node cell were extracted. + date: 07.05.2017 + version: 0.3.3 +- changes: + - action: Updated + details: + - issue `pyexcel#pyexcel-ods#17`, empty new line is ignored + - issue `pyexcel#pyexcel-ods#6`, PT288H00M00S is valid duration + date: 13.04.2017 + version: 0.3.2 +- changes: + - action: Added + details: + - Recognize currency type + date: 02.02.2017 + version: 0.3.1 +- changes: + - action: Updated + details: + - Code refactoring with pyexcel-io v 0.3.0 + date: 22.12.2016 + version: 0.3.0 +- changes: + - action: Updated + details: + - issue `pyexcel#pyexcel-ods#14`, index error when reading a ods file that has + non-uniform columns repeated property. + date: 24.10.2016 + version: 0.2.2 +- changes: + - action: Added + details: + - 'support pagination. two pairs: start_row, row_limit and start_column, column_limit + help you deal with large files.' + - use odfpy 1.3.3 as compulsory package. offically support python 3 + date: 31.08.2016 + version: 0.2.1 +- changes: + - action: Added + details: + - By default, `float` will be converted to `int` where fits. `auto_detect_int`, a + flag to switch off the autoatic conversion from `float` to `int`. + - '''library=pyexcel-ods'' was added so as to inform pyexcel to use it instead + of other libraries, in the situation where multiple plugins were installed.' + - action: Updated + details: + - support the auto-import feature of pyexcel-io 0.2.0 + date: 01.06.2016 + version: 0.2.0 +- changes: + - action: Added + details: + - '''streaming'' is an extra option given to get_data. Only when ''streaming'' is + explicitly set to True, the data will be consisted of generators, hence will + break your existing code.' + - uses yield in to_array and returns a generator + - 'support multi-line text cell #5' + - feature migration from pyexcel-ods3 pyexcel/pyexcel-ods3#5 + - action: Updated + details: + - compatibility with pyexcel-io 0.1.1 + date: 30.01.2016 + version: 0.1.1 +- changes: + - action: Updated + details: + - 'Bug fix: excessive trailing columns with empty values' + date: 10.10.2015 + version: 0.0.12 +- changes: + - action: Updated + details: + - Complete fix for libreoffice datetime field + date: 26.09.2015 + version: 0.0.11 +- changes: + - action: Updated + details: + - 'Bug fix: date field could have datetime from libreoffice' + date: 15.09.2015 + version: 0.0.10 +- changes: + - action: Updated + details: + - 'Bug fix: utf-8 string throw unicode exceptions' + date: 21.08.2015 + version: 0.0.9 +- changes: + - action: Updated + details: + - Pin dependency odfpy 0.9.6 to avoid buggy odfpy 1.3.0 + date: 28.06.2015 + version: 0.0.8 +- changes: + - action: Updated + details: + - 'Bug fix: "number-columns-repeated" is now respected' + date: 28.05.2015 + version: 0.0.7 +- changes: + - action: Updated + details: + - get_data and save_data are seen across pyexcel-* extensions. remember them once + and use them across all extensions. + date: 21.05.2015 + version: 0.0.6 +- changes: + - action: Added + details: + - Loads only one sheet from a multiple sheet book + - Use New BSD License + date: 22.02.2015 + version: 0.0.5 +- changes: + - action: Updated + details: + - IO interface update as pyexcel-io introduced keywords. + - initial release + date: 14.12.2014 + version: 0.0.4 +- changes: + - action: Updated + details: + - IO interface update as pyexcel-io introduced keywords. + - initial release + date: 08.12.2014 + version: 0.0.3 diff --git a/docs/source/conf.py b/docs/source/conf.py index 1f3f712..46df225 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,39 +1,86 @@ # -*- coding: utf-8 -*- -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.viewcode', -] +DESCRIPTION = ( + 'A wrapper library to read, manipulate and write data in ods format' + + '' +) +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html -intersphinx_mapping = { - 'pyexcel': ('http://pyexcel.readthedocs.org/en/latest/', None) -} -spelling_word_list_filename = 'spelling_wordlist.txt' +# -- Path setup -------------------------------------------------------------- + +# 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 +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + +# -- Project information ----------------------------------------------------- + +project = 'pyexcel-ods' +copyright = '2015-2020 Onni Software Ltd.' +author = 'C.W.' +# The short X.Y version +version = '0.6.0' +# The full version, including alpha/beta/rc tags +release = '0.6.0' + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode',] + +# Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] -source_suffix = '.rst' -master_doc = 'index' -project = u'pyexcel-ods' -copyright = u'2015-2016 Onni Software Ltd.' -version = '0.2.0' -release = '0.2.1' +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. exclude_patterns = [] -pygments_style = 'sphinx' -html_theme = 'default' + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] -htmlhelp_basename = 'pyexcel-odsdoc' -latex_elements = {} -latex_documents = [ - ('index', 'pyexcel-ods.tex', u'pyexcel-ods Documentation', - 'Onni Software Ltd.', 'manual'), -] -man_pages = [ - ('index', 'pyexcel-ods', u'pyexcel-ods Documentation', - [u'Onni Software Ltd.'], 1) -] + +# -- Extension configuration ------------------------------------------------- +# -- Options for intersphinx extension --------------------------------------- + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'https://docs.python.org/3/': None} +# TODO: html_theme not configurable upstream +html_theme = 'default' + +# TODO: DESCRIPTION not configurable upstream texinfo_documents = [ - ('index', 'pyexcel-ods', u'pyexcel-ods Documentation', - 'Onni Software Ltd.', 'pyexcel-ods', 'One line description of project.', + ('index', 'pyexcel-ods', + 'pyexcel-ods Documentation', + 'Onni Software Ltd.', 'pyexcel-ods', + DESCRIPTION, 'Miscellaneous'), ] +intersphinx_mapping.update({ + 'pyexcel': ('http://pyexcel.readthedocs.io/en/latest/', None), +}) +master_doc = "index" diff --git a/format.sh b/format.sh new file mode 100644 index 0000000..3a0ecb9 --- /dev/null +++ b/format.sh @@ -0,0 +1,3 @@ +isort $(find pyexcel_ods -name "*.py"|xargs echo) $(find tests -name "*.py"|xargs echo) +black -l 79 pyexcel_ods +black -l 79 tests diff --git a/lint.sh b/lint.sh new file mode 100644 index 0000000..d31eeaa --- /dev/null +++ b/lint.sh @@ -0,0 +1,2 @@ +pip install flake8 +flake8 --exclude=.moban.d,docs,setup.py --builtins=unicode,xrange,long . && python setup.py checkdocs \ No newline at end of file diff --git a/pyexcel-ods.yml b/pyexcel-ods.yml new file mode 100644 index 0000000..5eefaa6 --- /dev/null +++ b/pyexcel-ods.yml @@ -0,0 +1,18 @@ +overrides: "pyexcel.yaml" +name: "pyexcel-ods" +nick_name: ods +version: 0.6.0 +current_version: 0.6.0 +release: 0.6.0 +copyright_year: 2015-2020 +file_type: ods +dependencies: + - pyexcel-io>=0.6.2 + - odfpy>=1.3.5 +test_dependencies: + - pyexcel + - psutil + - pyexcel-xls +description: A wrapper library to read, manipulate and write data in ods format +python_requires: ">=3.6" +min_python_version: "3.6" diff --git a/pyexcel_ods.yaml b/pyexcel_ods.yaml deleted file mode 100644 index 667d2b3..0000000 --- a/pyexcel_ods.yaml +++ /dev/null @@ -1,10 +0,0 @@ -overrides: "pyexcel.yaml" -name: "pyexcel-ods" -nick_name: ods -version: 0.2.1 -release: 0.2.0 -file_type: ods -dependencies: - - pyexcel-io>=0.2.0 - - odfpy>=1.3.3 -description: A wrapper library to read, manipulate and write data in ods format diff --git a/pyexcel_ods/__init__.py b/pyexcel_ods/__init__.py index 5bc061e..63b02a0 100644 --- a/pyexcel_ods/__init__.py +++ b/pyexcel_ods/__init__.py @@ -2,28 +2,48 @@ pyexcel_ods ~~~~~~~~~~~~~~~~~~~ The lower level ods file format handler using odfpy - :copyright: (c) 2015-2016 by Onni Software Ltd & its contributors + :copyright: (c) 2015-2020 by Onni Software Ltd & its contributors :license: New BSD License """ + # flake8: noqa +from pyexcel_io.io import get_data as read_data +from pyexcel_io.io import isstream +from pyexcel_io.io import store_data as write_data + # this line has to be place above all else # because of dynamic import -_FILE_TYPE = 'ods' -__pyexcel_io_plugins__ = [_FILE_TYPE] +from pyexcel_io.plugins import IOPluginInfoChain, IOPluginInfoChainV2 - -from pyexcel_io.io import get_data as read_data, isstream, store_data as write_data +__FILE_TYPE__ = "ods" +IOPluginInfoChain(__name__) +IOPluginInfoChainV2(__name__).add_a_reader( + relative_plugin_class_path="odsr.ODSBook", + locations=["file", "memory"], + file_types=[__FILE_TYPE__], + stream_type="binary", +).add_a_reader( + relative_plugin_class_path="odsr.ODSBookInContent", + locations=["content"], + file_types=[__FILE_TYPE__], + stream_type="binary", +).add_a_writer( + relative_plugin_class_path="odsw.ODSWriter", + locations=["file", "memory"], + file_types=[__FILE_TYPE__], + stream_type="binary", +) def save_data(afile, data, file_type=None, **keywords): """standalone module function for writing module supported file type""" if isstream(afile) and file_type is None: - file_type = _FILE_TYPE + file_type = __FILE_TYPE__ write_data(afile, data, file_type=file_type, **keywords) def get_data(afile, file_type=None, **keywords): """standalone module function for reading module supported file type""" if isstream(afile) and file_type is None: - file_type = _FILE_TYPE + file_type = __FILE_TYPE__ return read_data(afile, file_type=file_type, **keywords) diff --git a/pyexcel_ods/converter.py b/pyexcel_ods/converter.py deleted file mode 100644 index fc65958..0000000 --- a/pyexcel_ods/converter.py +++ /dev/null @@ -1,133 +0,0 @@ -import sys -import datetime - -PY2 = sys.version_info[0] == 2 - - -def float_value(value): - """convert a value to float""" - ret = float(value) - return ret - - -def date_value(value): - """convert to data value accroding ods specification""" - ret = "invalid" - try: - # catch strptime exceptions only - if len(value) == 10: - ret = datetime.datetime.strptime( - value, - "%Y-%m-%d") - ret = ret.date() - elif len(value) == 19: - ret = datetime.datetime.strptime( - value, - "%Y-%m-%dT%H:%M:%S") - elif len(value) > 19: - ret = datetime.datetime.strptime( - value[0:26], - "%Y-%m-%dT%H:%M:%S.%f") - except ValueError: - pass - if ret == "invalid": - raise Exception("Bad date value %s" % value) - return ret - - -def ods_date_value(value): - return value.strftime("%Y-%m-%d") - - -def time_value(value): - """convert to time value accroding the specification""" - hour = int(value[2:4]) - minute = int(value[5:7]) - second = int(value[8:10]) - if hour < 24: - ret = datetime.time(hour, minute, second) - else: - ret = datetime.timedelta(hours=hour, minutes=minute, seconds=second) - return ret - - -def ods_time_value(value): - return value.strftime("PT%HH%MM%SS") - - -def boolean_value(value): - """get bolean value""" - if value == "true": - ret = True - else: - ret = False - return ret - - -def ods_bool_value(value): - """convert a boolean value to text""" - if value is True: - return "true" - else: - return "false" - - -def ods_timedelta_value(cell): - """convert a cell value to time delta""" - hours = cell.days * 24 + cell.seconds // 3600 - minutes = (cell.seconds // 60) % 60 - seconds = cell.seconds % 60 - return "PT%02dH%02dM%02dS" % (hours, minutes, seconds) - - -ODS_FORMAT_CONVERSION = { - "float": float, - "date": datetime.date, - "time": datetime.time, - 'timedelta': datetime.timedelta, - "boolean": bool, - "percentage": float, - "currency": float -} - - -ODS_WRITE_FORMAT_COVERSION = { - float: "float", - int: "float", - str: "string", - datetime.date: "date", - datetime.time: "time", - datetime.timedelta: "timedelta", - bool: "boolean" -} - -if PY2: - ODS_WRITE_FORMAT_COVERSION[unicode] = "string" - -VALUE_CONVERTERS = { - "float": float_value, - "date": date_value, - "time": time_value, - "timedelta": time_value, - "boolean": boolean_value, - "percentage": float_value, - "currency": float_value -} - -ODS_VALUE_CONVERTERS = { - "date": ods_date_value, - "time": ods_time_value, - "boolean": ods_bool_value, - "timedelta": ods_timedelta_value -} - - -VALUE_TOKEN = { - "float": "value", - "date": "date-value", - "time": "time-value", - "boolean": "boolean-value", - "percentage": "value", - "currency": "value", - "timedelta": "time-value" -} diff --git a/pyexcel_ods/ods.py b/pyexcel_ods/ods.py deleted file mode 100644 index 1f6ed67..0000000 --- a/pyexcel_ods/ods.py +++ /dev/null @@ -1,287 +0,0 @@ -# Copyright 2011 Marco Conti - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Thanks to grt for the fixes -import sys -import math - -from odf.table import TableRow, TableCell, Table -from odf.text import P -from odf.namespaces import OFFICENS -from odf.opendocument import OpenDocumentSpreadsheet, load - -from pyexcel_io.book import BookReader, BookWriter -from pyexcel_io.sheet import SheetReader, SheetWriter - -import pyexcel_ods.converter as converter - -PY2 = sys.version_info[0] == 2 - -PY27_BELOW = PY2 and sys.version_info[1] < 7 -if PY27_BELOW: - from ordereddict import OrderedDict -else: - from collections import OrderedDict - - -class ODSSheet(SheetReader): - """native ods sheet""" - def __init__(self, sheet, auto_detect_int=True, **keywords): - SheetReader.__init__(self, sheet, **keywords) - self.auto_detect_int = auto_detect_int - self.rows = self.native_sheet.getElementsByType(TableRow) - self.cached_rows = {} - self._number_of_rows = len(self.rows) - self._number_of_columns = self._find_columns() - - def number_of_rows(self): - return self._number_of_rows - - def number_of_columns(self): - return self._number_of_columns - - @property - def name(self): - return self.native_sheet.getAttribute("name") - - def _cell_value(self, row, column): - current_row = self.rows[row] - cells = current_row.getElementsByType(TableCell) - cell_value = None - if str(row) in self.cached_rows: - row_cache = self.cached_rows[str(row)] - cell_value = row_cache[column] - return cell_value - - try: - cell = cells[column] - cell_value = self._read_cell(cell) - except IndexError: - cell_value = None - return cell_value - - def _read_row(self, cells): - tmp_row = [] - for cell in cells: - # repeated value? - repeat = cell.getAttribute("numbercolumnsrepeated") - cell_value = self._read_cell(cell) - if repeat: - number_of_repeat = int(repeat) - tmp_row += [cell_value] * number_of_repeat - else: - tmp_row.append(cell_value) - return tmp_row - - def _read_text_cell(self, cell): - text_content = [] - paragraphs = cell.getElementsByType(P) - # for each text node - for paragraph in paragraphs: - for node in paragraph.childNodes: - if (node.nodeType == 3): - if PY2: - text_content.append(unicode(node.data)) - else: - text_content.append(node.data) - return '\n'.join(text_content) - - def _read_cell(self, cell): - cell_type = cell.getAttrNS(OFFICENS, "value-type") - value_token = converter.VALUE_TOKEN.get(cell_type, "value") - ret = None - if cell_type == "string": - text_content = self._read_text_cell(cell) - ret = text_content - else: - if cell_type in converter.VALUE_CONVERTERS: - value = cell.getAttrNS(OFFICENS, value_token) - n_value = converter.VALUE_CONVERTERS[cell_type](value) - if cell_type == 'float' and self.auto_detect_int: - if is_integer_ok_for_xl_float(n_value): - n_value = int(n_value) - ret = n_value - else: - text_content = self._read_text_cell(cell) - ret = text_content - return ret - - def _find_columns(self): - max = -1 - for row_index, row in enumerate(self.rows): - cells = row.getElementsByType(TableCell) - if self._check_for_column_repeat(cells): - row_cache = self._read_row(cells) - self.cached_rows.update({str(row_index): row_cache}) - length = len(row_cache) - else: - length = len(cells) - if length > max: - max = length - return max - - def _check_for_column_repeat(self, cells): - found_repeated_columns = False - for cell in cells: - repeat = cell.getAttribute("numbercolumnsrepeated") - if repeat: - found_repeated_columns = True - break - return found_repeated_columns - - -class ODSBook(BookReader): - """read ods book""" - - def open(self, file_name, **keywords): - """open ods file""" - BookReader.open(self, file_name, **keywords) - self._load_from_file() - - def open_stream(self, file_stream, **keywords): - """open ods file stream""" - BookReader.open_stream(self, file_stream, **keywords) - self._load_from_memory() - - def read_sheet_by_name(self, sheet_name): - """read a named sheet""" - tables = self.native_book.spreadsheet.getElementsByType(Table) - rets = [table for table in tables - if table.getAttribute('name') == sheet_name] - if len(rets) == 0: - raise ValueError("%s cannot be found" % sheet_name) - else: - return self.read_sheet(rets[0]) - - def read_sheet_by_index(self, sheet_index): - """read a sheet at a specified index""" - tables = self.native_book.spreadsheet.getElementsByType(Table) - length = len(tables) - if sheet_index < length: - return self.read_sheet(tables[sheet_index]) - else: - raise IndexError("Index %d of out bound %d" % ( - sheet_index, length)) - - def read_all(self): - """read all sheets""" - result = OrderedDict() - for sheet in self.native_book.spreadsheet.getElementsByType(Table): - ods_sheet = ODSSheet(sheet, **self.keywords) - result[ods_sheet.name] = ods_sheet.to_array() - - return result - - def read_sheet(self, native_sheet): - """read one native sheet""" - sheet = ODSSheet(native_sheet, **self.keywords) - return {sheet.name: sheet.to_array()} - - def _load_from_memory(self): - self.native_book = load(self.file_stream) - - def _load_from_file(self): - self.native_book = load(self.file_name) - pass - - -class ODSSheetWriter(SheetWriter): - """ - ODS sheet writer - """ - def set_sheet_name(self, name): - """initialize the native table""" - self.native_sheet = Table(name=name) - - def set_size(self, size): - """not used in this class but used in ods3""" - pass - - def write_cell(self, row, cell): - """write a native cell""" - cell_to_be_written = TableCell() - cell_type = type(cell) - cell_odf_type = converter.ODS_WRITE_FORMAT_COVERSION.get( - cell_type, "string") - cell_to_be_written.setAttrNS(OFFICENS, "value-type", cell_odf_type) - cell_odf_value_token = converter.VALUE_TOKEN.get( - cell_odf_type, "value") - converter_func = converter.ODS_VALUE_CONVERTERS.get( - cell_odf_type, None) - if converter_func: - cell = converter_func(cell) - if cell_odf_type != 'string': - cell_to_be_written.setAttrNS(OFFICENS, cell_odf_value_token, cell) - cell_to_be_written.addElement(P(text=cell)) - else: - lines = cell.split('\n') - for line in lines: - cell_to_be_written.addElement(P(text=line)) - row.addElement(cell_to_be_written) - - def write_row(self, array): - """ - write a row into the file - """ - row = TableRow() - self.native_sheet.addElement(row) - for cell in array: - self.write_cell(row, cell) - - def close(self): - """ - This call writes file - - """ - self.native_book.spreadsheet.addElement(self.native_sheet) - - -class ODSWriter(BookWriter): - """ - open document spreadsheet writer - - """ - def __init__(self): - BookWriter.__init__(self) - self.native_book = OpenDocumentSpreadsheet() - - def create_sheet(self, name): - """ - write a row into the file - """ - return ODSSheetWriter(self.native_book, None, name) - - def close(self): - """ - This call writes file - - """ - self.native_book.write(self.file_alike_object) - - -def is_integer_ok_for_xl_float(value): - """check if a float had zero value in digits""" - return value == math.floor(value) - - -_ods_registry = { - "file_type": "ods", - "reader": ODSBook, - "writer": ODSWriter, - "stream_type": "binary", - "mime_type": "application/vnd.oasis.opendocument.spreadsheet", - "library": "pyexcel-ods" -} - -exports = (_ods_registry,) diff --git a/pyexcel_ods/odsr.py b/pyexcel_ods/odsr.py new file mode 100644 index 0000000..178fd4d --- /dev/null +++ b/pyexcel_ods/odsr.py @@ -0,0 +1,130 @@ +""" + pyexcel_ods.odsr + ~~~~~~~~~~~~~~~~~~~~~ + + ods reader + + :copyright: (c) 2014-2020 by Onni Software Ltd. + :license: New BSD License, see LICENSE for more details +""" +# Copyright 2011 Marco Conti + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from io import BytesIO + +import pyexcel_io.service as service +from odf.text import P +from odf.table import Table, TableRow, TableCell + +# Thanks to grt for the fixes +from odf.teletype import extractText +from odf.namespaces import OFFICENS +from odf.opendocument import load +from pyexcel_io.plugin_api import ISheet, IReader, NamedContent + + +class ODSSheet(ISheet): + """native ods sheet""" + + def __init__(self, sheet, auto_detect_int=True, **keywords): + self._native_sheet = sheet + self._keywords = keywords + self.__auto_detect_int = auto_detect_int + + @property + def name(self): + return self._native_sheet.getAttribute("name") + + def row_iterator(self): + return self._native_sheet.getElementsByType(TableRow) + + def column_iterator(self, row): + cells = row.getElementsByType(TableCell) + for cell in cells: + repeat = cell.getAttribute("numbercolumnsrepeated") + cell_value = self.__read_cell(cell) + if repeat: + number_of_repeat = int(repeat) + for i in range(number_of_repeat): + yield cell_value + else: + yield cell_value + + def __read_cell(self, cell): + cell_type = cell.getAttrNS(OFFICENS, "value-type") + value_token = service.VALUE_TOKEN.get(cell_type, "value") + ret = None + if cell_type == "string": + text_content = self.__read_text_cell(cell) + ret = text_content + elif cell_type == "currency": + value = cell.getAttrNS(OFFICENS, value_token) + currency = cell.getAttrNS(OFFICENS, cell_type) + if currency: + ret = value + " " + currency + else: + ret = value + else: + if cell_type in service.VALUE_CONVERTERS: + value = cell.getAttrNS(OFFICENS, value_token) + n_value = service.VALUE_CONVERTERS[cell_type](value) + if cell_type == "float" and self.__auto_detect_int: + if service.has_no_digits_in_float(n_value): + n_value = int(n_value) + ret = n_value + else: + text_content = self.__read_text_cell(cell) + ret = text_content + return ret + + def __read_text_cell(self, cell): + text_content = [] + paragraphs = cell.getElementsByType(P) + # for each text node + for paragraph in paragraphs: + name_space, tag = paragraph.parentNode.qname + if tag != str("annotation"): + data = extractText(paragraph) + text_content.append(data) + return "\n".join(text_content) + + +class ODSBook(IReader): + """read ods book""" + + def __init__(self, file_alike_object, _, **keywords): + self._native_book = load(file_alike_object) + self._keywords = keywords + self.content_array = [ + NamedContent(table.getAttribute("name"), table) + for table in self._native_book.spreadsheet.getElementsByType(Table) + ] + + def read_sheet(self, sheet_index): + """read a sheet at a specified index""" + table = self.content_array[sheet_index].payload + sheet = ODSSheet(table, **self._keywords) + return sheet + + def close(self): + self._native_book = None + + +class ODSBookInContent(ODSBook): + """ + Open xlsx as read only mode + """ + + def __init__(self, file_content, file_type, **keywords): + io = BytesIO(file_content) + super().__init__(io, file_type, **keywords) diff --git a/pyexcel_ods/odsw.py b/pyexcel_ods/odsw.py new file mode 100644 index 0000000..1cb9762 --- /dev/null +++ b/pyexcel_ods/odsw.py @@ -0,0 +1,91 @@ +""" + pyexcel_ods.odsw + ~~~~~~~~~~~~~~~~~~~~~ + + ods writer + + :copyright: (c) 2014-2020 by Onni Software Ltd. + :license: New BSD License, see LICENSE for more details +""" +import pyexcel_io.service as converter +from odf.text import P +from odf.table import Table, TableRow, TableCell +from odf.namespaces import OFFICENS +from odf.opendocument import OpenDocumentSpreadsheet +from pyexcel_io.plugin_api import IWriter, ISheetWriter + + +class ODSSheetWriter(ISheetWriter): + """ + ODS sheet writer + """ + + def __init__(self, ods_book, sheet_name): + self._native_book = ods_book + self._native_sheet = Table(name=sheet_name) + + def write_cell(self, row, cell): + """write a native cell""" + cell_to_be_written = TableCell() + cell_type = type(cell) + cell_odf_type = converter.ODS_WRITE_FORMAT_COVERSION.get( + cell_type, "string" + ) + cell_to_be_written.setAttrNS(OFFICENS, "value-type", cell_odf_type) + cell_odf_value_token = converter.VALUE_TOKEN.get( + cell_odf_type, "value" + ) + converter_func = converter.ODS_VALUE_CONVERTERS.get( + cell_odf_type, None + ) + if converter_func: + cell = converter_func(cell) + if cell_odf_type != "string": + cell_to_be_written.setAttrNS(OFFICENS, cell_odf_value_token, cell) + cell_to_be_written.addElement(P(text=cell)) + else: + lines = cell.split("\n") + for line in lines: + cell_to_be_written.addElement(P(text=line)) + row.addElement(cell_to_be_written) + + def write_row(self, array): + """ + write a row into the file + """ + row = TableRow() + self._native_sheet.addElement(row) + for cell in array: + self.write_cell(row, cell) + + def close(self): + """ + This call writes file + + """ + self._native_book.spreadsheet.addElement(self._native_sheet) + + +class ODSWriter(IWriter): + """ + open document spreadsheet writer + + """ + + def __init__(self, file_alike_object, file_type, **_): + self.file_alike_object = file_alike_object + self._native_book = OpenDocumentSpreadsheet() + + def create_sheet(self, name): + """ + write a row into the file + """ + return ODSSheetWriter(self._native_book, name) + + def close(self): + """ + This call writes file + + """ + self._native_book.write(self.file_alike_object) + self._native_book = None diff --git a/requirements.txt b/requirements.txt index 59763ff..10c1e1d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -pyexcel-io>=0.2.0 -odfpy>=1.3.3 +pyexcel-io>=0.6.2 +odfpy>=1.3.5 diff --git a/rnd_requirements.txt b/rnd_requirements.txt index cabc29e..a4748b0 100644 --- a/rnd_requirements.txt +++ b/rnd_requirements.txt @@ -1,3 +1 @@ -https://github.com/pyexcel/pyexcel-io/archive/master.zip -https://github.com/pyexcel/pyexcel-xls/archive/master.zip -https://github.com/pyexcel/pyexcel/archive/master.zip +https://github.com/pyexcel/pyexcel-io/archive/dev.zip diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..8c8abfd --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[metadata] +description-file = README.rst +[bdist_wheel] +universal = 1 diff --git a/setup.py b/setup.py index fb9b466..b9f9462 100644 --- a/setup.py +++ b/setup.py @@ -1,76 +1,203 @@ +#!/usr/bin/env python3 + +""" +Template by pypi-mobans +""" + +import os +import sys +import codecs +import locale +import platform +from shutil import rmtree + +from setuptools import Command, setup, find_packages + + +# Work around mbcs bug in distutils. +# http://bugs.python.org/issue10945 +# This work around is only if a project supports Python < 3.4 + +# Work around for locale not being set try: - from setuptools import setup, find_packages -except ImportError: - from ez_setup import use_setuptools - use_setuptools() - from setuptools import setup, find_packages + lc = locale.getlocale() + pf = platform.system() + if pf != "Windows" and lc == (None, None): + locale.setlocale(locale.LC_ALL, "C.UTF-8") +except (ValueError, UnicodeError, locale.Error): + locale.setlocale(locale.LC_ALL, "en_US.UTF-8") -NAME = 'pyexcel-ods' -AUTHOR = 'C.W.' -VERSION = '0.2.1' -EMAIL = 'wangc_2011 (at) hotmail.com' -LICENSE = 'New BSD' -PACKAGES = find_packages(exclude=['ez_setup', 'examples', 'tests']) +NAME = "pyexcel-ods" +AUTHOR = "C.W." +VERSION = "0.6.0" +EMAIL = "info@pyexcel.org" +LICENSE = "New BSD" DESCRIPTION = ( - 'A wrapper library to read, manipulate and write data in ods format' + - '' + "A wrapper library to read, manipulate and write data in ods format" ) +URL = "https://github.com/pyexcel/pyexcel-ods" +DOWNLOAD_URL = "%s/archive/0.6.0.tar.gz" % URL +FILES = ["README.rst", "CHANGELOG.rst"] KEYWORDS = [ - 'excel', - 'python', - 'pyexcel', + "python", ] -INSTALL_REQUIRES = [ - 'pyexcel-io>=0.2.0', - 'odfpy>=1.3.3', -] - - -EXTRAS_REQUIRE = { -} - CLASSIFIERS = [ - 'Topic :: Office/Business', - 'Topic :: Utilities', - 'Topic :: Software Development :: Libraries', - 'Programming Language :: Python', - 'License :: OSI Approved :: BSD License', - 'Intended Audience :: Developers', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7' + "Topic :: Software Development :: Libraries", + "Programming Language :: Python", + "Intended Audience :: Developers", + + "Programming Language :: Python :: 3 :: Only", + + + + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + ] +PYTHON_REQUIRES = ">=3.6" + +INSTALL_REQUIRES = [ + "pyexcel-io>=0.6.2", + "odfpy>=1.3.5", +] +SETUP_COMMANDS = {} + +PACKAGES = find_packages(exclude=["ez_setup", "examples", "tests", "tests.*"]) +EXTRAS_REQUIRE = { +} +# You do not need to read beyond this line +PUBLISH_COMMAND = "{0} setup.py sdist bdist_wheel upload -r pypi".format(sys.executable) +HERE = os.path.abspath(os.path.dirname(__file__)) + +GS_COMMAND = ("gease pyexcel-ods v0.6.0 " + + "Find 0.6.0 in changelog for more details") +NO_GS_MESSAGE = ("Automatic github release is disabled. " + + "Please install gease to enable it.") +UPLOAD_FAILED_MSG = ( + 'Upload failed. please run "%s" yourself.' % PUBLISH_COMMAND) + + +class PublishCommand(Command): + """Support setup.py upload.""" + + description = "Build and publish the package on github and pypi" + user_options = [] + + @staticmethod + def status(s): + """Prints things in bold.""" + print("\033[1m{0}\033[0m".format(s)) + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + try: + self.status("Removing previous builds...") + rmtree(os.path.join(HERE, "dist")) + rmtree(os.path.join(HERE, "build")) + rmtree(os.path.join(HERE, "pyexcel_ods.egg-info")) + except OSError: + pass + + self.status("Building Source and Wheel (universal) distribution...") + run_status = True + if has_gease(): + run_status = os.system(GS_COMMAND) == 0 + else: + self.status(NO_GS_MESSAGE) + if run_status: + if os.system(PUBLISH_COMMAND) != 0: + self.status(UPLOAD_FAILED_MSG) + + sys.exit() + + +SETUP_COMMANDS.update({ + "publish": PublishCommand +}) + +def has_gease(): + """ + test if github release command is installed + + visit http://github.com/moremoban/gease for more info + """ + try: + import gease # noqa + return True + except ImportError: + return False + def read_files(*files): """Read files into setup""" text = "" for single_file in files: - text = text + read(single_file) + "\n" + content = read(single_file) + text = text + content + "\n" return text def read(afile): """Read a file into setup""" - with open(afile, 'r') as opened_file: - return opened_file.read() + the_relative_file = os.path.join(HERE, afile) + with codecs.open(the_relative_file, "r", "utf-8") as opened_file: + content = filter_out_test_code(opened_file) + content = "".join(list(content)) + return content -if __name__ == '__main__': +def filter_out_test_code(file_handle): + found_test_code = False + for line in file_handle.readlines(): + if line.startswith(".. testcode:"): + found_test_code = True + continue + if found_test_code is True: + if line.startswith(" "): + continue + else: + empty_line = line.strip() + if len(empty_line) == 0: + continue + else: + found_test_code = False + yield line + else: + for keyword in ["|version|", "|today|"]: + if keyword in line: + break + else: + yield line + + +if __name__ == "__main__": setup( + test_suite="tests", name=NAME, author=AUTHOR, version=VERSION, author_email=EMAIL, description=DESCRIPTION, - install_requires=INSTALL_REQUIRES, + url=URL, + download_url=DOWNLOAD_URL, + long_description=read_files(*FILES), + license=LICENSE, keywords=KEYWORDS, + python_requires=PYTHON_REQUIRES, extras_require=EXTRAS_REQUIRE, + tests_require=["nose"], + install_requires=INSTALL_REQUIRES, packages=PACKAGES, include_package_data=True, - long_description=read_files('README.rst', 'CHANGELOG.rst'), zip_safe=False, - tests_require=['nose'], - license=LICENSE, - classifiers=CLASSIFIERS + classifiers=CLASSIFIERS, + cmdclass=SETUP_COMMANDS ) diff --git a/test.bat b/test.bat index dbacc63..ecba1ba 100644 --- a/test.bat +++ b/test.bat @@ -1,3 +1,2 @@ - pip freeze -nosetests --with-cov --cover-package pyexcel_ods --cover-package tests --with-doctest --doctest-extension=.rst tests README.rst pyexcel_ods && flake8 . --exclude=.moban.d --builtins=unicode,xrange,long +nosetests --with-coverage --cover-package pyexcel_ods --cover-package tests tests --with-doctest --doctest-extension=.rst README.rst docs/source pyexcel_ods diff --git a/tests/base.py b/tests/base.py index 974de35..3d40097 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,11 +1,13 @@ -import os +import os # noqa +import datetime # noqa + import pyexcel -from nose.tools import raises -import datetime + +from nose.tools import eq_, raises # noqa def create_sample_file1(file): - data = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 1.1, 1] + data = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", 1.1, 1] table = [] table.append(data[:4]) table.append(data[4:8]) @@ -17,17 +19,17 @@ class PyexcelHatWriterBase: """ Abstract functional test for hat writers """ + content = { "X": [1, 2, 3, 4, 5], "Y": [6, 7, 8, 9, 10], - "Z": [11, 12, 13, 14, 15] + "Z": [11, 12, 13, 14, 15], } def test_series_table(self): pyexcel.save_as(adict=self.content, dest_file_name=self.testfile) r = pyexcel.get_sheet(file_name=self.testfile, name_columns_by_row=0) - actual = pyexcel.utils.to_dict(r) - assert actual == self.content + eq_(r.dict, self.content) class PyexcelWriterBase: @@ -37,11 +39,12 @@ class PyexcelWriterBase: testfile and testfile2 have to be initialized before it is used for testing """ + content = [ [1, 2, 3, 4, 5], [1, 2, 3, 4, 5], [1, 2, 3, 4, 5], - [1, 2, 3, 4, 5] + [1, 2, 3, 4, 5], ] def _create_a_file(self, file): @@ -50,12 +53,11 @@ class PyexcelWriterBase: def test_write_array(self): self._create_a_file(self.testfile) r = pyexcel.get_sheet(file_name=self.testfile) - actual = pyexcel.utils.to_array(r.rows()) + actual = list(r.rows()) assert actual == self.content class PyexcelMultipleSheetBase: - def _write_test_file(self, filename): pyexcel.save_book_as(bookdict=self.content, dest_file_name=filename) @@ -74,85 +76,66 @@ class PyexcelMultipleSheetBase: def test_reading_through_sheets(self): b = pyexcel.BookReader(self.testfile) - data = pyexcel.utils.to_array(b["Sheet1"].rows()) + data = list(b["Sheet1"].rows()) expected = [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]] assert data == expected - data = pyexcel.utils.to_array(b["Sheet2"].rows()) + data = list(b["Sheet2"].rows()) expected = [[4, 4, 4, 4], [5, 5, 5, 5], [6, 6, 6, 6]] assert data == expected - data = pyexcel.utils.to_array(b["Sheet3"].rows()) - expected = [[u'X', u'Y', u'Z'], [1, 4, 7], [2, 5, 8], [3, 6, 9]] + data = list(b["Sheet3"].rows()) + expected = [[u"X", u"Y", u"Z"], [1, 4, 7], [2, 5, 8], [3, 6, 9]] assert data == expected sheet3 = b["Sheet3"] sheet3.name_columns_by_row(0) - data = pyexcel.utils.to_array(b["Sheet3"].rows()) + data = list(b["Sheet3"].rows()) expected = [[1, 4, 7], [2, 5, 8], [3, 6, 9]] assert data == expected - def test_iterate_through_sheets(self): - b = pyexcel.BookReader(self.testfile) - for s in b: - data = pyexcel.utils.to_array(s) - assert self.content[s.name] == data - si = pyexcel.sheets.iterators.SheetIterator(b) - for s in si: - data = pyexcel.utils.to_array(s) - assert self.content[s.name] == data - - def test_random_access_operator(self): - r = pyexcel.BookReader(self.testfile) - value = r["Sheet1"][0, 1] - assert value == 1 - value = r["Sheet3"][0, 1] - assert value == 'Y' - r["Sheet3"].name_columns_by_row(0) - assert r["Sheet3"][0, 1] == 4 - class ODSCellTypes: - def test_formats(self): # date formats date_format = "%d/%m/%Y" - assert self.data["Sheet1"][0][0] == "Date" - assert self.data["Sheet1"][1][0].strftime(date_format) == "11/11/2014" - assert self.data["Sheet1"][2][0].strftime(date_format) == "01/01/2001" - assert self.data["Sheet1"][3][0] == "" + eq_(self.data["Sheet1"][0][0], "Date") + eq_(self.data["Sheet1"][1][0].strftime(date_format), "11/11/2014") + eq_(self.data["Sheet1"][2][0].strftime(date_format), "01/01/2001") + eq_(self.data["Sheet1"][3][0], "") # time formats time_format = "%S:%M:%H" - assert self.data["Sheet1"][0][1] == "Time" - assert self.data["Sheet1"][1][1].strftime(time_format) == "12:12:11" - assert self.data["Sheet1"][2][1].strftime(time_format) == "12:00:00" - assert self.data["Sheet1"][3][1] == 0 - assert self.data["Sheet1"][4][1] == datetime.timedelta(hours=27, - minutes=17, - seconds=54) - assert self.data["Sheet1"][5][1] == "Other" + eq_(self.data["Sheet1"][0][1], "Time") + eq_(self.data["Sheet1"][1][1].strftime(time_format), "12:12:11") + eq_(self.data["Sheet1"][2][1].strftime(time_format), "12:00:00") + eq_(self.data["Sheet1"][3][1], 0) + eq_( + self.data["Sheet1"][4][1], + datetime.timedelta(hours=27, minutes=17, seconds=54), + ) + eq_(self.data["Sheet1"][5][1], "Other") # boolean - assert self.data["Sheet1"][0][2] == "Boolean" - assert self.data["Sheet1"][1][2] is True - assert self.data["Sheet1"][2][2] is False + eq_(self.data["Sheet1"][0][2], "Boolean") + eq_(self.data["Sheet1"][1][2], True) + eq_(self.data["Sheet1"][2][2], False) # Float - assert self.data["Sheet1"][0][3] == "Float" - assert self.data["Sheet1"][1][3] == 11.11 + eq_(self.data["Sheet1"][0][3], "Float") + eq_(self.data["Sheet1"][1][3], 11.11) # Currency - assert self.data["Sheet1"][0][4] == "Currency" - assert self.data["Sheet1"][1][4] == 1 - assert self.data["Sheet1"][2][4] == -10000 + eq_(self.data["Sheet1"][0][4], "Currency") + eq_(self.data["Sheet1"][1][4], "1 GBP") + eq_(self.data["Sheet1"][2][4], "-10000 GBP") # Percentage - assert self.data["Sheet1"][0][5] == "Percentage" - assert self.data["Sheet1"][1][5] == 2 + eq_(self.data["Sheet1"][0][5], "Percentage") + eq_(self.data["Sheet1"][1][5], 2) # int - assert self.data["Sheet1"][0][6] == "Int" - assert self.data["Sheet1"][1][6] == 3 - assert self.data["Sheet1"][4][6] == 11 + eq_(self.data["Sheet1"][0][6], "Int") + eq_(self.data["Sheet1"][1][6], 3) + eq_(self.data["Sheet1"][4][6], 11) # Scientifed not supported - assert self.data["Sheet1"][1][7] == 100000 + eq_(self.data["Sheet1"][1][7], 100000) # Fraction - assert self.data["Sheet1"][1][8] == 1.25 + eq_(self.data["Sheet1"][1][8], 1.25) # Text - assert self.data["Sheet1"][1][9] == "abc" + eq_(self.data["Sheet1"][1][9], "abc") @raises(IndexError) def test_no_excessive_trailing_columns(self): - assert self.data["Sheet1"][2][6] == "" + eq_(self.data["Sheet1"][2][6], "") diff --git a/tests/fixtures/12_day_as_time.ods b/tests/fixtures/12_day_as_time.ods new file mode 100644 index 0000000..b7f7197 Binary files /dev/null and b/tests/fixtures/12_day_as_time.ods differ diff --git a/tests/fixtures/comment-in-cell.ods b/tests/fixtures/comment-in-cell.ods new file mode 100644 index 0000000..b87ce6e Binary files /dev/null and b/tests/fixtures/comment-in-cell.ods differ diff --git a/tests/fixtures/issue_27.ods b/tests/fixtures/issue_27.ods new file mode 100644 index 0000000..86296d2 Binary files /dev/null and b/tests/fixtures/issue_27.ods differ diff --git a/tests/fixtures/issue_61.ods b/tests/fixtures/issue_61.ods new file mode 100644 index 0000000..34c917a Binary files /dev/null and b/tests/fixtures/issue_61.ods differ diff --git a/tests/fixtures/multilineods.ods b/tests/fixtures/multilineods.ods index c03f645..3cf556b 100644 Binary files a/tests/fixtures/multilineods.ods and b/tests/fixtures/multilineods.ods differ diff --git a/tests/fixtures/pyexcel_81_ods_19.ods b/tests/fixtures/pyexcel_81_ods_19.ods new file mode 100755 index 0000000..a1954e0 Binary files /dev/null and b/tests/fixtures/pyexcel_81_ods_19.ods differ diff --git a/tests/fixtures/white_space.ods b/tests/fixtures/white_space.ods new file mode 100644 index 0000000..8e891c2 Binary files /dev/null and b/tests/fixtures/white_space.ods differ diff --git a/tests/requirements.txt b/tests/requirements.txt index f13a9b5..ae03e89 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,6 +1,14 @@ nose +mock;python_version<"3" codecov coverage flake8 +black +isort +collective.checkdocs +pygments +moban +moban_jinja2_github pyexcel +psutil pyexcel-xls diff --git a/tests/test_bug_fixes.py b/tests/test_bug_fixes.py index 67b515f..c644529 100644 --- a/tests/test_bug_fixes.py +++ b/tests/test_bug_fixes.py @@ -1,13 +1,21 @@ #!/usr/bin/python # -*- encoding: utf-8 -*- import os + +import psutil +import pyexcel as pe from pyexcel_ods import get_data, save_data -from nose.tools import raises, eq_ +from pyexcel_io.exceptions import IntegerAccuracyLossError + +from nose import SkipTest +from nose.tools import eq_, raises + +IN_TRAVIS = "TRAVIS" in os.environ def test_bug_fix_for_issue_1(): - data = get_data(os.path.join("tests", "fixtures", "repeated.ods")) - eq_(data["Sheet1"], [['repeated', 'repeated', 'repeated', 'repeated']]) + data = get_data(get_fixtures("repeated.ods")) + eq_(data["Sheet1"], [["repeated", "repeated", "repeated", "repeated"]]) def test_bug_fix_for_issue_2(): @@ -16,31 +24,13 @@ def test_bug_fix_for_issue_2(): data.update({"Sheet 2": [[u"row 1", u"Héllô!", u"HolÁ!"]]}) save_data("your_file.ods", data) new_data = get_data("your_file.ods") - assert new_data["Sheet 2"] == [[u'row 1', u'H\xe9ll\xf4!', u'Hol\xc1!']] - - -def test_date_util_parse(): - from pyexcel_ods.converter import date_value - value = "2015-08-17T19:20:00" - d = date_value(value) - assert d.strftime("%Y-%m-%dT%H:%M:%S") == "2015-08-17T19:20:00" - value = "2015-08-17" - d = date_value(value) - assert d.strftime("%Y-%m-%d") == "2015-08-17" - value = "2015-08-17T19:20:59.999999" - d = date_value(value) - assert d.strftime("%Y-%m-%dT%H:%M:%S") == "2015-08-17T19:20:59" - value = "2015-08-17T19:20:59.99999" - d = date_value(value) - assert d.strftime("%Y-%m-%dT%H:%M:%S") == "2015-08-17T19:20:59" - value = "2015-08-17T19:20:59.999999999999999" - d = date_value(value) - assert d.strftime("%Y-%m-%dT%H:%M:%S") == "2015-08-17T19:20:59" + assert new_data["Sheet 2"] == [[u"row 1", u"H\xe9ll\xf4!", u"Hol\xc1!"]] @raises(Exception) def test_invalid_date(): from pyexcel_ods.ods import date_value + value = "2015-08-" date_value(value) @@ -48,31 +38,127 @@ def test_invalid_date(): @raises(Exception) def test_fake_date_time_10(): from pyexcel_ods.ods import date_value + date_value("1234567890") @raises(Exception) def test_fake_date_time_19(): from pyexcel_ods.ods import date_value + date_value("1234567890123456789") @raises(Exception) def test_fake_date_time_20(): from pyexcel_ods.ods import date_value + date_value("12345678901234567890") def test_issue_13(): test_file = "test_issue_13.ods" - data = [ - [1, 2], - [], - [], - [], - [3, 4] - ] + data = [[1, 2], [], [], [], [3, 4]] save_data(test_file, {test_file: data}) written_data = get_data(test_file, skip_empty_rows=False) eq_(data, written_data[test_file]) os.unlink(test_file) + + +def test_issue_14(): + # pyexcel issue 61 + test_file = "issue_61.ods" + data = get_data(get_fixtures(test_file), skip_empty_rows=True) + eq_(data["S-LMC"], [[u"aaa"], [0]]) + + +def test_issue_6(): + test_file = "12_day_as_time.ods" + data = get_data(get_fixtures(test_file), skip_empty_rows=True) + eq_(data["Sheet1"][0][0].days, 12) + + +def test_issue_19(): + test_file = "pyexcel_81_ods_19.ods" + data = get_data(get_fixtures(test_file), skip_empty_rows=True) + eq_(data["product.template"][1][1], "PRODUCT NAME PMP") + + +def test_issue_83_ods_file_handle(): + # this proves that odfpy + # does not leave a file handle open at all + proc = psutil.Process() + test_file = get_fixtures("issue_61.ods") + open_files_l1 = proc.open_files() + + # start with a csv file + data = pe.iget_array(file_name=test_file, library="pyexcel-ods") + open_files_l2 = proc.open_files() + delta = len(open_files_l2) - len(open_files_l1) + # cannot catch open file handle + assert delta == 0 + + # now the file handle get opened when we run through + # the generator + list(data) + open_files_l3 = proc.open_files() + delta = len(open_files_l3) - len(open_files_l1) + # cannot catch open file handle + assert delta == 0 + + # free the fish + pe.free_resources() + open_files_l4 = proc.open_files() + # this confirms that no more open file handle + eq_(open_files_l1, open_files_l4) + + +def test_pr_22(): + test_file = get_fixtures("white_space.ods") + data = get_data(test_file) + eq_(data["Sheet1"][0][0], "paragraph with tab(\t), space, \nnew line") + + +def test_issue_23(): + if not IN_TRAVIS: + raise SkipTest() + pe.get_book( + url=( + "https://github.com/pyexcel/pyexcel-ods/" + + "raw/master/tests/fixtures/white_space.ods" + ) + ) + + +def test_issue_24(): + test_file = get_fixtures("comment-in-cell.ods") + data = get_data(test_file) + eq_(data["Sheet1"], [["test"]]) + + +def test_issue_27(): + test_file = get_fixtures("issue_27.ods") + data = get_data(test_file, skip_empty_rows=True) + eq_(data["VGPMX"], [["", "Cost Basis", "0"]]) + + +def test_issue_30(): + test_file = "issue_30.ods" + sheet = pe.Sheet() + sheet[0, 0] = 999999999999999 + sheet.save_as(test_file) + sheet2 = pe.get_sheet(file_name=test_file) + eq_(sheet[0, 0], sheet2[0, 0]) + os.unlink(test_file) + + +@raises(IntegerAccuracyLossError) +def test_issue_30_precision_loss(): + test_file = "issue_30_2.ods" + sheet = pe.Sheet() + sheet[0, 0] = 9999999999999999 + sheet.save_as(test_file) + + +def get_fixtures(filename): + return os.path.join("tests", "fixtures", filename) diff --git a/tests/test_filter.py b/tests/test_filter.py index b74bc4c..029e04b 100644 --- a/tests/test_filter.py +++ b/tests/test_filter.py @@ -1,6 +1,7 @@ import os from pyexcel_io import get_data, save_data + from nose.tools import eq_ @@ -13,49 +14,58 @@ class TestFilter: [3, 23, 33], [4, 24, 34], [5, 25, 35], - [6, 26, 36] + [6, 26, 36], ] save_data(self.test_file, sample) self.sheet_name = "pyexcel_sheet1" def test_filter_row(self): - filtered_data = get_data(self.test_file, start_row=3, - library="pyexcel-ods") + filtered_data = get_data( + self.test_file, start_row=3, library="pyexcel-ods" + ) expected = [[4, 24, 34], [5, 25, 35], [6, 26, 36]] eq_(filtered_data[self.sheet_name], expected) def test_filter_row_2(self): - filtered_data = get_data(self.test_file, start_row=3, row_limit=1, - library="pyexcel-ods") + filtered_data = get_data( + self.test_file, start_row=3, row_limit=1, library="pyexcel-ods" + ) expected = [[4, 24, 34]] eq_(filtered_data[self.sheet_name], expected) def test_filter_column(self): - filtered_data = get_data(self.test_file, start_column=1, - library="pyexcel-ods") - expected = [[21, 31], [22, 32], [23, 33], - [24, 34], [25, 35], [26, 36]] + filtered_data = get_data( + self.test_file, start_column=1, library="pyexcel-ods" + ) + expected = [[21, 31], [22, 32], [23, 33], [24, 34], [25, 35], [26, 36]] eq_(filtered_data[self.sheet_name], expected) def test_filter_column_2(self): - filtered_data = get_data(self.test_file, - start_column=1, column_limit=1, - library="pyexcel-ods") + filtered_data = get_data( + self.test_file, + start_column=1, + column_limit=1, + library="pyexcel-ods", + ) expected = [[21], [22], [23], [24], [25], [26]] eq_(filtered_data[self.sheet_name], expected) def test_filter_both_ways(self): - filtered_data = get_data(self.test_file, - start_column=1, start_row=3, - library="pyexcel-ods") + filtered_data = get_data( + self.test_file, start_column=1, start_row=3, library="pyexcel-ods" + ) expected = [[24, 34], [25, 35], [26, 36]] eq_(filtered_data[self.sheet_name], expected) def test_filter_both_ways_2(self): - filtered_data = get_data(self.test_file, - start_column=1, column_limit=1, - start_row=3, row_limit=1, - library="pyexcel-ods") + filtered_data = get_data( + self.test_file, + start_column=1, + column_limit=1, + start_row=3, + row_limit=1, + library="pyexcel-ods", + ) expected = [[24]] eq_(filtered_data[self.sheet_name], expected) diff --git a/tests/test_formatters.py b/tests/test_formatters.py index 79af6b0..ae1e6e8 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -1,51 +1,68 @@ import os -from unittest import TestCase from textwrap import dedent import pyexcel as pe +from nose.tools import eq_ -class TestAutoDetectInt(TestCase): + +class TestAutoDetectInt: def setUp(self): self.content = [[1, 2, 3.1]] self.test_file = "test_auto_detect_init.ods" pe.save_as(array=self.content, dest_file_name=self.test_file) def test_auto_detect_int(self): - sheet = pe.get_sheet(file_name=self.test_file) - expected = dedent(""" + sheet = pe.get_sheet(file_name=self.test_file, library="pyexcel-ods") + expected = dedent( + """ pyexcel_sheet1: +---+---+-----+ | 1 | 2 | 3.1 | - +---+---+-----+""").strip() - self.assertEqual(str(sheet), expected) + +---+---+-----+""" + ).strip() + eq_(str(sheet), expected) def test_get_book_auto_detect_int(self): - book = pe.get_book(file_name=self.test_file) - expected = dedent(""" + book = pe.get_book(file_name=self.test_file, library="pyexcel-ods") + expected = dedent( + """ pyexcel_sheet1: +---+---+-----+ | 1 | 2 | 3.1 | - +---+---+-----+""").strip() - self.assertEqual(str(book), expected) + +---+---+-----+""" + ).strip() + eq_(str(book), expected) def test_auto_detect_int_false(self): - sheet = pe.get_sheet(file_name=self.test_file, auto_detect_int=False) - expected = dedent(""" + sheet = pe.get_sheet( + file_name=self.test_file, + auto_detect_int=False, + library="pyexcel-ods", + ) + expected = dedent( + """ pyexcel_sheet1: +-----+-----+-----+ | 1.0 | 2.0 | 3.1 | - +-----+-----+-----+""").strip() - self.assertEqual(str(sheet), expected) + +-----+-----+-----+""" + ).strip() + eq_(str(sheet), expected) def test_get_book_auto_detect_int_false(self): - book = pe.get_book(file_name=self.test_file, auto_detect_int=False) - expected = dedent(""" + book = pe.get_book( + file_name=self.test_file, + auto_detect_int=False, + library="pyexcel-ods", + ) + expected = dedent( + """ pyexcel_sheet1: +-----+-----+-----+ | 1.0 | 2.0 | 3.1 | - +-----+-----+-----+""").strip() - self.assertEqual(str(book), expected) + +-----+-----+-----+""" + ).strip() + eq_(str(book), expected) def tearDown(self): os.unlink(self.test_file) diff --git a/tests/test_multiline_feature.py b/tests/test_multiline_feature.py index 7a3afc1..aa2c870 100644 --- a/tests/test_multiline_feature.py +++ b/tests/test_multiline_feature.py @@ -1,15 +1,17 @@ import os + import pyexcel def test_reading_multiline_ods(): testfile = os.path.join("tests", "fixtures", "multilineods.ods") sheet = pyexcel.get_sheet(file_name=testfile) - assert sheet[0, 0] == '1\n2\n3\n4' + assert sheet[0, 0] == "1\n2\n3\n4" + assert sheet[1, 0] == "Line 1\n\nLine 2" def test_writing_multiline_ods(): - content = "2\n3\n4\n993939\na" + content = "2\n3\n4\n993939\n\na" testfile = "writemultiline.ods" array = [[content, "test"]] pyexcel.save_as(array=array, dest_file_name=testfile) diff --git a/tests/test_mutliple_sheets.py b/tests/test_multiple_sheets.py similarity index 88% rename from tests/test_mutliple_sheets.py rename to tests/test_multiple_sheets.py index bebbf19..f82b212 100644 --- a/tests/test_mutliple_sheets.py +++ b/tests/test_multiple_sheets.py @@ -1,9 +1,11 @@ -from base import PyexcelMultipleSheetBase -import pyexcel import os -from nose.tools import raises import sys +import pyexcel +from base import PyexcelMultipleSheetBase + +from nose.tools import raises + if sys.version_info[0] == 2 and sys.version_info[1] < 7: from ordereddict import OrderedDict else: @@ -54,20 +56,20 @@ class TestAddBooks: def test_load_a_single_sheet(self): b1 = pyexcel.get_book(file_name=self.testfile, sheet_name="Sheet1") assert len(b1.sheet_names()) == 1 - assert b1['Sheet1'].to_array() == self.content['Sheet1'] + assert b1["Sheet1"].to_array() == self.content["Sheet1"] def test_load_a_single_sheet2(self): b1 = pyexcel.load_book(self.testfile, sheet_index=0) assert len(b1.sheet_names()) == 1 - assert b1['Sheet1'].to_array() == self.content['Sheet1'] + assert b1["Sheet1"].to_array() == self.content["Sheet1"] @raises(IndexError) def test_load_a_single_sheet3(self): - pyexcel.load_book(self.testfile, sheet_index=10000) + pyexcel.get_book(file_name=self.testfile, sheet_index=10000) @raises(ValueError) def test_load_a_single_sheet4(self): - pyexcel.load_book(self.testfile, sheet_name="Not exist") + pyexcel.get_book(file_name=self.testfile, sheet_name="Not exist") def test_delete_sheets(self): b1 = pyexcel.load_book(self.testfile) @@ -104,7 +106,7 @@ class TestAddBooks: b1 = pyexcel.get_book(file_name=self.testfile) b2 = pyexcel.get_book(file_name=self.testfile2) b3 = b1 + b2 - content = pyexcel.utils.to_dict(b3) + content = b3.dict sheet_names = content.keys() assert len(sheet_names) == 6 for name in sheet_names: @@ -122,7 +124,7 @@ class TestAddBooks: b1 = pyexcel.BookReader(self.testfile) b2 = pyexcel.BookReader(self.testfile2) b1 += b2 - content = pyexcel.utils.to_dict(b1) + content = b1.dict sheet_names = content.keys() assert len(sheet_names) == 6 for name in sheet_names: @@ -140,7 +142,7 @@ class TestAddBooks: b1 = pyexcel.BookReader(self.testfile) b2 = pyexcel.BookReader(self.testfile2) b3 = b1 + b2["Sheet3"] - content = pyexcel.utils.to_dict(b3) + content = b3.dict sheet_names = content.keys() assert len(sheet_names) == 4 for name in sheet_names: @@ -158,7 +160,7 @@ class TestAddBooks: b1 = pyexcel.BookReader(self.testfile) b2 = pyexcel.BookReader(self.testfile2) b1 += b2["Sheet3"] - content = pyexcel.utils.to_dict(b1) + content = b1.dict sheet_names = content.keys() assert len(sheet_names) == 4 for name in sheet_names: @@ -176,7 +178,7 @@ class TestAddBooks: b1 = pyexcel.BookReader(self.testfile) b2 = pyexcel.BookReader(self.testfile2) b3 = b1["Sheet1"] + b2["Sheet3"] - content = pyexcel.utils.to_dict(b3) + content = b3.dict sheet_names = content.keys() assert len(sheet_names) == 2 assert content["Sheet3"] == self.content["Sheet3"] @@ -189,7 +191,7 @@ class TestAddBooks: b1 = pyexcel.BookReader(self.testfile) b2 = pyexcel.BookReader(self.testfile2) b3 = b1["Sheet1"] + b2 - content = pyexcel.utils.to_dict(b3) + content = b3.dict sheet_names = content.keys() assert len(sheet_names) == 4 for name in sheet_names: @@ -228,17 +230,17 @@ class TestMultiSheetReader: self.testfile = "file_with_an_empty_sheet.ods" def test_reader_with_correct_sheets(self): - r = pyexcel.BookReader(os.path.join("tests", "fixtures", - self.testfile)) + r = pyexcel.BookReader( + os.path.join("tests", "fixtures", self.testfile) + ) assert r.number_of_sheets() == 3 def _produce_ordered_dict(): data_dict = OrderedDict() - data_dict.update({ - "Sheet1": [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]]}) - data_dict.update({ - "Sheet2": [[4, 4, 4, 4], [5, 5, 5, 5], [6, 6, 6, 6]]}) - data_dict.update({ - "Sheet3": [[u'X', u'Y', u'Z'], [1, 4, 7], [2, 5, 8], [3, 6, 9]]}) + data_dict.update({"Sheet1": [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]]}) + data_dict.update({"Sheet2": [[4, 4, 4, 4], [5, 5, 5, 5], [6, 6, 6, 6]]}) + data_dict.update( + {"Sheet3": [[u"X", u"Y", u"Z"], [1, 4, 7], [2, 5, 8], [3, 6, 9]]} + ) return data_dict diff --git a/tests/test_ods_reader.py b/tests/test_ods_reader.py index 551bda3..f236bf5 100644 --- a/tests/test_ods_reader.py +++ b/tests/test_ods_reader.py @@ -1,14 +1,16 @@ import os -from pyexcel_ods import ods + from base import ODSCellTypes +from pyexcel_ods.odsr import ODSBook +from pyexcel_ods.odsw import ODSWriter +from pyexcel_io.reader import Reader class TestODSReader(ODSCellTypes): def setUp(self): - r = ods.ODSBook() - r.open(os.path.join("tests", - "fixtures", - "ods_formats.ods")) + r = Reader("ods") + r.reader_class = ODSBook + r.open(os.path.join("tests", "fixtures", "ods_formats.ods")) self.data = r.read_all() for key in self.data.keys(): self.data[key] = list(self.data[key]) @@ -17,19 +19,17 @@ class TestODSReader(ODSCellTypes): class TestODSWriter(ODSCellTypes): def setUp(self): - r = ods.ODSBook() - r.open(os.path.join("tests", - "fixtures", - "ods_formats.ods")) + r = Reader("ods") + r.reader_class = ODSBook + r.open(os.path.join("tests", "fixtures", "ods_formats.ods")) + r.close() self.data1 = r.read_all() self.testfile = "odswriter.ods" - w = ods.ODSWriter() - w.open(self.testfile) + w = ODSWriter(self.testfile, "ods") w.write(self.data1) w.close() - r2 = ods.ODSBook() - r2.open(self.testfile) - self.data = r2.read_all() + r.open(self.testfile) + self.data = r.read_all() for key in self.data.keys(): self.data[key] = list(self.data[key]) diff --git a/tests/test_stringio.py b/tests/test_stringio.py index 203d5d5..45e0ec8 100644 --- a/tests/test_stringio.py +++ b/tests/test_stringio.py @@ -1,30 +1,32 @@ import os -from unittest import TestCase + import pyexcel from base import create_sample_file1 +from nose.tools import eq_ -class TestStringIO(TestCase): +class TestStringIO: def test_ods_stringio(self): - odsfile = "cute.ods" - create_sample_file1(odsfile) - with open(odsfile, "rb") as f: + testfile = "cute.ods" + create_sample_file1(testfile) + with open(testfile, "rb") as f: content = f.read() - r = pyexcel.get_sheet(file_type="ods", file_content=content) - result = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 1.1, 1] - actual = pyexcel.utils.to_array(r.enumerate()) - self.assertEqual(result, actual) - if os.path.exists(odsfile): - os.unlink(odsfile) + r = pyexcel.get_sheet( + file_type="ods", file_content=content, library="pyexcel-ods" + ) + result = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", 1.1, 1] + actual = list(r.enumerate()) + eq_(result, actual) + if os.path.exists(testfile): + os.unlink(testfile) - def test_xls_output_stringio(self): - data = [ - [1, 2, 3], - [4, 5, 6] - ] - io = pyexcel.save_as(dest_file_type='ods', array=data) - r = pyexcel.get_sheet(file_type="ods", file_content=io.getvalue()) + def test_ods_output_stringio(self): + data = [[1, 2, 3], [4, 5, 6]] + io = pyexcel.save_as(dest_file_type="ods", array=data) + r = pyexcel.get_sheet( + file_type="ods", file_content=io.getvalue(), library="pyexcel-ods" + ) result = [1, 2, 3, 4, 5, 6] - actual = pyexcel.utils.to_array(r.enumerate()) - self.assertEqual(result, actual) + actual = list(r.enumerate()) + eq_(result, actual) diff --git a/tests/test_writer.py b/tests/test_writer.py index 4ee967e..1402ce5 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -1,6 +1,8 @@ import os -from pyexcel_ods import ods + from base import PyexcelWriterBase, PyexcelHatWriterBase +from pyexcel_io import get_data +from pyexcel_ods.odsw import ODSWriter as Writer class TestNativeODSWriter: @@ -8,18 +10,13 @@ class TestNativeODSWriter: self.content = { "Sheet1": [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]], "Sheet2": [[4, 4, 4, 4], [5, 5, 5, 5], [6, 6, 6, 6]], - "Sheet3": [[u'X', u'Y', u'Z'], [1, 4, 7], [2, 5, 8], [3, 6, 9]] + "Sheet3": [[u"X", u"Y", u"Z"], [1, 4, 7], [2, 5, 8], [3, 6, 9]], } - self.testfile = "odswriter.ods" - writer = ods.ODSWriter() - writer.open(self.testfile) + self.testfile = "writer.ods" + writer = Writer(self.testfile, "ods") writer.write(self.content) writer.close() - reader = ods.ODSBook() - reader.open(self.testfile) - content = reader.read_all() - for key in content.keys(): - content[key] = list(content[key]) + content = get_data(self.testfile, library="pyexcel-ods") assert content == self.content def tearDown(self): @@ -27,9 +24,9 @@ class TestNativeODSWriter: os.unlink(self.testfile) -class TestODSnCSVWriter(PyexcelWriterBase): +class TestodsnCSVWriter(PyexcelWriterBase): def setUp(self): - self.testfile = "testods.ods" + self.testfile = "test.ods" self.testfile2 = "test.csv" def tearDown(self): @@ -39,9 +36,9 @@ class TestODSnCSVWriter(PyexcelWriterBase): os.unlink(self.testfile2) -class TestODSHatWriter(PyexcelHatWriterBase): +class TestodsHatWriter(PyexcelHatWriterBase): def setUp(self): - self.testfile = "testhat.ods" + self.testfile = "test.ods" def tearDown(self): if os.path.exists(self.testfile):