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..3789494 --- /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 --use-deprecated=legacy-resolver install flake8 + pip --use-deprecated=legacy-resolver 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..73a3aed --- /dev/null +++ b/.github/workflows/moban-update.yml @@ -0,0 +1,28 @@ +on: [push] + +jobs: + run_moban: + runs-on: ubuntu-latest + name: synchronize templates via moban + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.PAT }} + - 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: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: >- + 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..914c82a --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,35 @@ +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] + exclude: + - os: macOs-latest + python-version: 3.6 + + 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 --use-deprecated=legacy-resolver install -r requirements.txt + pip --use-deprecated=legacy-resolver install -r tests/requirements.txt + - name: test + run: | + pip freeze + nosetests --verbosity=3 --with-coverage --cover-package pyexcel_xls --cover-package tests tests --with-doctest --doctest-extension=.rst README.rst docs/source pyexcel_xls + - 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 c2ece11..e8b12f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +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 +.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..8bdda20 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,10 @@ +[settings] +line_length=79 +known_first_party=pyexcel,xlrd,xlwt +known_third_party=mock,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 5dee5c3..0000000 --- a/.moban.d/README.rst +++ /dev/null @@ -1,13 +0,0 @@ -{%extends 'README.rst.jj2' %} - -{%block description%} -**pyexcel-{{file_type}}** is a tiny wrapper library to read, manipulate and write data in {{file_type}} format and it can read xlsx and xlsm fromat. You are likely to use it with `pyexcel `_. -{%endblock%} - -{%block extras %} -Known Issues -============= - -* If a zero was typed in a DATE formatted field in xls, you will get "01/01/1900". -* If a zero was typed in a TIME formatted field in xls, you will get "00:00:00". -{%endblock%} diff --git a/.moban.d/custom_README.rst.jj2 b/.moban.d/custom_README.rst.jj2 new file mode 100644 index 0000000..719ed79 --- /dev/null +++ b/.moban.d/custom_README.rst.jj2 @@ -0,0 +1,50 @@ +{%extends 'README.rst.jj2' %} + +{% block documentation_link %} +{% endblock %} + +{%block description%} +**pyexcel-{{file_type}}** is a tiny wrapper library to read, manipulate and +write data in {{file_type}} format and it can read xlsx and xlsm fromat. +You are likely to use it with `pyexcel `_. + +Oct 2021 - Update: +=================== + +1. v0.7.0 removed the pin on xlrd < 2. If you have xlrd >= 2, this +library will NOT read 'xlsx' format and you need to install pyexcel-xlsx. Othwise, +this library can use xlrd < 2 to read xlsx format for you. So 'xlsx' support +in this library will vary depending on the installed version of xlrd. + +2. v0.7.0 can write datetime.timedelta. but when the value is read out, +you will get datetime.datetime. so you as the developer decides what to do with it. + +Past news +=========== + +`detect_merged_cells` allows you to spread the same value among +all merged cells. But be aware that this may slow down its reading +performance. + +`skip_hidden_row_and_column` allows you to skip hidden rows +and columns and is defaulted to **True**. It may slow down its reading +performance. And it is only valid for 'xls' files. For 'xlsx' files, +please use pyexcel-xlsx. + +Warning +================================================================================ + +**xls file cannot contain more than 65,000 rows**. You are risking the reputation +of yourself/your company/ +`your country `_ if you keep +using xls and are not aware of its row limit. + +{%endblock%} + +{%block extras %} +Known Issues +============= + +* If a zero was typed in a DATE formatted field in xls, you will get "01/01/1900". +* If a zero was typed in a TIME formatted field in xls, you will get "00:00:00". +{%endblock%} diff --git a/.moban.d/custom_setup.py.jj2 b/.moban.d/custom_setup.py.jj2 new file mode 100644 index 0000000..81aed01 --- /dev/null +++ b/.moban.d/custom_setup.py.jj2 @@ -0,0 +1,16 @@ +{% extends 'setup.py.jj2' %} + +{%block platform_block%} +{%endblock%} + +{%block additional_keywords %} + 'xls', + 'xlsx', + 'xlsm' +{%endblock%} + +{% block morefiles %}"CONTRIBUTORS.rst",{% endblock %} + +{%block additional_classifiers%} + 'Programming Language :: Python :: Implementation :: PyPy' +{%endblock%} diff --git a/.moban.d/setup.py b/.moban.d/setup.py deleted file mode 100644 index fce0619..0000000 --- a/.moban.d/setup.py +++ /dev/null @@ -1,20 +0,0 @@ -{% extends 'setup.py.jj2' %} - -{%block platform_block%} -{%endblock%} - -{%block additional_keywords %} - 'xls', - 'xlsx', - 'xlsm' -{%endblock%} - -{%block additional_classifiers%} - 'Development Status :: 3 - Alpha', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: Implementation :: PyPy' -{%endblock%} diff --git a/.moban.d/tests/custom_base.py.jj2 b/.moban.d/tests/custom_base.py.jj2 new file mode 100644 index 0000000..156f96c --- /dev/null +++ b/.moban.d/tests/custom_base.py.jj2 @@ -0,0 +1,4 @@ +{% extends 'tests/base.py.jj2' %} + +{%block ods_types%} +{%endblock%} diff --git a/.moban.d/tests/requirements.txt b/.moban.d/tests/custom_requirements.txt.jj2 similarity index 52% rename from .moban.d/tests/requirements.txt rename to .moban.d/tests/custom_requirements.txt.jj2 index da5ea12..6ca5326 100644 --- a/.moban.d/tests/requirements.txt +++ b/.moban.d/tests/custom_requirements.txt.jj2 @@ -1,4 +1,8 @@ {% extends 'tests/requirements.txt.jj2' %} {%block extras %} pyexcel +xlrd==1.1.0 +moban +black;python_version>="3.6" +isort;python_version>="3.6" {%endblock%} diff --git a/.moban.d/travis.yml b/.moban.d/travis.yml deleted file mode 100644 index 9839e3c..0000000 --- a/.moban.d/travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -{% extends "travis.yml.jj2" %} -{%block test_other_python_versions%} - 2.6 - - 3.3 - - 3.4 - - 3.5 - - pypy -{%endblock%} - -{%block extra_matrix%} -matrix: - include: - - python: 2.7 - dist: trusty - sudo: required - virtualenv: - system_site_packages: true - addons: - apt: - packages: - - python-xlwt - - python-xlrd - - python-coverage - - python-nose - - python-mock -{%endblock%} - -{%block custom_install %} - if [[ $TRAVIS_PYTHON_VERSION == "2.6" ]]; then pip install ordereddict; fi -{%endblock%} diff --git a/.moban.yml b/.moban.yml index a88db65..94ecc3d 100644 --- a/.moban.yml +++ b/.moban.yml @@ -1,17 +1,10 @@ +overrides: "git://github.com/pyexcel/pyexcel-mobans!/mobanfile.yaml" configuration: - configuration_dir: "commons/config" - template_dir: - - "commons/templates" - - ".moban.d" - configuration: pyexcel_xls.yaml + configuration: pyexcel-xls.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 + - "tests/requirements.txt": "tests/custom_requirements.txt.jj2" diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..a379070 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,18 @@ +# .readthedocs.yml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/source/conf.py + +# Optionally build your docs in additional formats such as PDF +formats: + - pdf + +# Optionally set the version of Python and requirements required to build your docs +python: + version: 3.7 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e2a876e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,41 +0,0 @@ -sudo: false -language: python -notifications: - email: false -python: - - pypy - - 2.6 - - 2.7 - - pypy3 - - 3.3 - - 3.4 - - 3.5 -matrix: - include: - - python: 2.7 - dist: trusty - sudo: required - virtualenv: - system_site_packages: true - addons: - apt: - packages: - - python-xlwt - - python-xlrd - - python-coverage - - python-nose - - python-mock -before_install: - - if [[ $TRAVIS_PYTHON_VERSION == "2.6" ]]; then pip install ordereddict; fi - - 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 b8ddd29..205f54f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,11 +1,191 @@ Change log ================================================================================ +0.7.0 - 07.10.2021 +-------------------------------------------------------------------------------- + +**Removed** + +#. `#46 `_: remove the hard + pin on xlrd version < 2.0 + +**Added** + +#. `#47 `_: limit support to + persist datetime.timedelta. see more details in doc + +0.6.2 - 12.12.2020 +-------------------------------------------------------------------------------- + +**Updated** + +#. lock down xlrd version less than version 2.0, because 2.0+ does not support + xlsx read + +0.6.1 - 21.10.2020 +-------------------------------------------------------------------------------- + +**Updated** + +#. Restrict this library to get installed on python 3.6+, because pyexcel-io + 0.6.0+ supports only python 3.6+. + +0.6.0 - 8.10.2020 +-------------------------------------------------------------------------------- + +**Updated** + +#. New style xlsx plugins, promoted by pyexcel-io v0.6.2. + +0.5.9 - 29.08.2020 +-------------------------------------------------------------------------------- + +**Added** + +#. `#35 `_, include tests + +0.5.8 - 22.08.2018 +-------------------------------------------------------------------------------- + +**Added** + +#. `pyexcel#151 `_, read cell + error as #N/A. + +0.5.7 - 15.03.2018 +-------------------------------------------------------------------------------- + +**Added** + +#. `pyexcel#54 `_, Book.datemode + attribute of that workbook should be passed always. + +0.5.6 - 15.03.2018 +-------------------------------------------------------------------------------- + +**Added** + +#. `pyexcel#120 `_, xlwt cannot + save a book without any sheet. So, let's raise an exception in this case in + order to warn the developers. + +0.5.5 - 8.11.2017 +-------------------------------------------------------------------------------- + +**Added** + +#. `#25 `_, detect merged cell + in .xls + +0.5.4 - 2.11.2017 +-------------------------------------------------------------------------------- + +**Added** + +#. `#24 `_, xlsx format cannot + use skip_hidden_row_and_column. please use pyexcel-xlsx instead. + +0.5.3 - 2.11.2017 +-------------------------------------------------------------------------------- + +**Added** + +#. `#21 `_, skip hidden rows + and columns under 'skip_hidden_row_and_column' flag. + +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 +#. update its dependecy on pyexcel-io to 0.5.3 + +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** + +#. `#20 `_, is handled in + pyexcel-io +#. 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. + +0.4.1 - 25.08.2017 +-------------------------------------------------------------------------------- + +**Updated** + +#. `#20 `_, handle unseekable + stream given by http response. + +0.4.0 - 19.06.2017 +-------------------------------------------------------------------------------- + +**Updated** + +#. `pyexcel-xlsx#15 `_, close + file handle +#. pyexcel-io plugin interface now updated to use `lml + `_. + +0.3.3 - 30/05/2017 +-------------------------------------------------------------------------------- + +**Updated** + +#. `#18 `_, pass on + encoding_override and others to xlrd. + +0.3.2 - 18.05.2017 +-------------------------------------------------------------------------------- + +**Updated** + +#. `#16 `_, allow mmap to be + passed as file content + +0.3.1 - 16.01.2017 +-------------------------------------------------------------------------------- + +**Updated** + +#. `#14 `_, Python 3.6 - + cannot use LOCALE flag with a str pattern +#. fix its dependency on pyexcel-io 0.3.0 + +0.3.0 - 22.12.2016 +-------------------------------------------------------------------------------- + +**Updated** + +#. `#13 `_, alert on empyty + file content +#. Support pyexcel-io v0.3.0 + +0.2.3 - 20.09.2016 +-------------------------------------------------------------------------------- + +**Updated** + +#. `#10 `_, To support + generator as member of the incoming two dimensional data + 0.2.2 - 31.08.2016 -------------------------------------------------------------------------------- -Added -******************************************************************************** +**Added** #. support pagination. two pairs: start_row, row_limit and start_column, column_limit help you deal with large files. @@ -13,29 +193,24 @@ Added 0.2.1 - 13.07.2016 -------------------------------------------------------------------------------- -Added -******************************************************************************** +**Added** #. `#9 `_, `skip_hidden_sheets` is added. By default, hidden sheets are skipped when reading all sheets. Reading sheet by name or by index are not affected. - 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-xls' was added so as to inform pyexcel to use it instead of - other libraries, in the situation where there are more than one plugin for - a file type, e.g. xlsm + other libraries, in the situation where there are more than one plugin for a + file type, e.g. xlsm - -Updated -******************************************************************************** +**Updated** #. support the auto-import feature of pyexcel-io 0.2.0 #. xlwt is now used for python 2 implementation while xlwt-future is used for @@ -44,10 +219,8 @@ Updated 0.1.0 - 17.01.2016 -------------------------------------------------------------------------------- -Added -******************************************************************************** +**Added** #. Passing "streaming=True" to get_data, you will get the two dimensional array as a generator #. Passing "data=your_generator" to save_data is acceptable too. - diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst new file mode 100644 index 0000000..b45268a --- /dev/null +++ b/CONTRIBUTORS.rst @@ -0,0 +1,10 @@ + + +3 contributors +================================================================================ + +In alphabetical order: + +* `John Vandenberg `_ +* `Peter Carnesciali `_ +* `vinraspa `_ diff --git a/LICENSE b/LICENSE index f5d9673..409a51e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015-2016 by Onni Software Ltd. and its contributors +Copyright (c) 2015-2021 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 48efabb..4dbbeae 100644 --- a/README.rst +++ b/README.rst @@ -2,23 +2,105 @@ pyexcel-xls - Let you focus on data, instead of xls format ================================================================================ -.. image:: https://api.travis-ci.org/pyexcel/pyexcel-xls.png - :target: http://travis-ci.org/pyexcel/pyexcel-xls +.. 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-xls/coverage.png - :target: https://codecov.io/github/pyexcel/pyexcel-xls +.. 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-xls/workflows/run_tests/badge.svg + :target: http://github.com/pyexcel/pyexcel-xls/actions + +.. image:: https://codecov.io/gh/pyexcel/pyexcel-xls/branch/master/graph/badge.svg + :target: https://codecov.io/gh/pyexcel/pyexcel-xls + +.. image:: https://badge.fury.io/py/pyexcel-xls.svg + :target: https://pypi.org/project/pyexcel-xls + +.. image:: https://anaconda.org/conda-forge/pyexcel-xls/badges/version.svg + :target: https://anaconda.org/conda-forge/pyexcel-xls + +.. image:: https://pepy.tech/badge/pyexcel-xls/month + :target: https://pepy.tech/project/pyexcel-xls + +.. image:: https://anaconda.org/conda-forge/pyexcel-xls/badges/downloads.svg + :target: https://anaconda.org/conda-forge/pyexcel-xls + +.. 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-xls** is a tiny wrapper library to read, manipulate and +write data in xls format and it can read xlsx and xlsm fromat. +You are likely to use it with `pyexcel `_. + +Oct 2021 - Update: +=================== + +1. v0.7.0 removed the pin on xlrd < 2. If you have xlrd >= 2, this +library will NOT read 'xlsx' format and you need to install pyexcel-xlsx. Othwise, +this library can use xlrd < 2 to read xlsx format for you. So 'xlsx' support +in this library will vary depending on the installed version of xlrd. + +2. v0.7.0 can write datetime.timedelta. but when the value is read out, +you will get datetime.datetime. so you as the developer decides what to do with it. + +Past news +=========== + +`detect_merged_cells` allows you to spread the same value among +all merged cells. But be aware that this may slow down its reading +performance. + +`skip_hidden_row_and_column` allows you to skip hidden rows +and columns and is defaulted to **True**. It may slow down its reading +performance. And it is only valid for 'xls' files. For 'xlsx' files, +please use pyexcel-xlsx. + +Warning +================================================================================ + +**xls file cannot contain more than 65,000 rows**. You are risking the reputation +of yourself/your company/ +`your country `_ if you keep +using xls and are not aware of its row limit. + + +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. -**pyexcel-xls** is a tiny wrapper library to read, manipulate and write data in xls format and it can read xlsx and xlsm fromat. You are likely to use it with `pyexcel `_. 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-xls via pip: .. code-block:: bash @@ -29,7 +111,7 @@ or clone it and install it: .. code-block:: bash - $ git clone http://github.com/pyexcel/pyexcel-xls.git + $ git clone https://github.com/pyexcel/pyexcel-xls.git $ cd pyexcel-xls $ python setup.py install @@ -39,9 +121,6 @@ Usage As a standalone library -------------------------------------------------------------------------------- -Write to an xls file -******************************************************************************** - .. testcode:: :hide: @@ -58,6 +137,11 @@ Write to an xls file ... from collections import OrderedDict +Write to an xls file +******************************************************************************** + + + Here's the sample code to write a dictionary to an xls file: .. code-block:: python @@ -68,6 +152,7 @@ Here's the sample code to write a dictionary to an xls file: >>> data.update({"Sheet 2": [["row 1", "row 2", "row 3"]]}) >>> save_data("your_file.xls", data) + Read from an xls file ******************************************************************************** @@ -101,6 +186,7 @@ Here's the sample code to write a dictionary to an xls file: + Read from an xls from memory ******************************************************************************** @@ -119,6 +205,8 @@ Continue from previous example: Pagination feature ******************************************************************************** + + Let's assume the following file is a huge xls file: .. code-block:: python @@ -175,16 +263,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 xls 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 xls - -Please note only pyexcel version 0.0.4+ support this. - Reading from an xls file ******************************************************************************** @@ -194,7 +272,6 @@ Here is the sample code: .. code-block:: python >>> import pyexcel as pe - >>> # from pyexcel.ext import xls >>> sheet = pe.get_book(file_name="your_file.xls") >>> sheet Sheet 1: @@ -265,6 +342,7 @@ You need to pass a StringIO instance to Writer: >>> # In reality, you might give it to your http response >>> # object for downloading + License ================================================================================ @@ -280,7 +358,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: @@ -288,28 +366,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.yml -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 ------------------------------ @@ -318,12 +384,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:: +On Windows, please issue this command:: > test.bat + +Before you commit +------------------------------ + +Please run:: + + $ make format + +so as to beautify your code otherwise your build may fail your unit test. + + Known Issues ============= diff --git a/changelog.yml b/changelog.yml new file mode 100644 index 0000000..7193acf --- /dev/null +++ b/changelog.yml @@ -0,0 +1,178 @@ +name: pyexcel-xls +organisation: pyexcel +releases: +- changes: + - action: Removed + details: + - "`#46`: remove the hard pin on xlrd version < 2.0" + - action: Added + details: + - "`#47`: limit support to persist datetime.timedelta. see more details in doc" + date: 07.10.2021 + version: 0.7.0 +- changes: + - action: Updated + details: + - "lock down xlrd version less than version 2.0, because 2.0+ does not support xlsx read" + date: 12.12.2020 + version: 0.6.2 +- changes: + - action: Updated + details: + - "Restrict this library to get installed on python 3.6+, because pyexcel-io 0.6.0+ supports only python 3.6+." + date: 21.10.2020 + version: 0.6.1 +- changes: + - action: Updated + details: + - 'New style xlsx plugins, promoted by pyexcel-io v0.6.2.' + date: 8.10.2020 + version: 0.6.0 +- changes: + - action: Added + details: + - "`#35`, include tests" + date: 29.08.2020 + version: 0.5.9 +- changes: + - action: Added + details: + - "`pyexcel#151`, read cell error as #N/A." + date: 22.08.2018 + version: 0.5.8 +- changes: + - action: Added + details: + - "`pyexcel#54`, Book.datemode attribute of that workbook should be passed always." + date: 15.03.2018 + version: 0.5.7 +- changes: + - action: Added + details: + - "`pyexcel#120`, xlwt cannot save a book without any sheet. So, let's raise an exception in this case in order to warn the developers." + date: 15.03.2018 + version: 0.5.6 +- changes: + - action: Added + details: + - '`#25`, detect merged cell in .xls' + date: 8.11.2017 + version: 0.5.5 +- changes: + - action: Added + details: + - '`#24`, xlsx format cannot use skip_hidden_row_and_column. please use pyexcel-xlsx + instead.' + date: 2.11.2017 + version: 0.5.4 +- changes: + - action: Added + details: + - '`#21`, skip hidden rows and columns under ''skip_hidden_row_and_column'' flag.' + date: 2.11.2017 + 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 + - update its dependecy on pyexcel-io to 0.5.3 + 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: + - '`#20`, is handled in pyexcel-io' + - 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. + date: 30.08.2017 + version: 0.5.0 +- changes: + - action: Updated + details: + - '`#20`, handle unseekable stream given by http response.' + date: 25.08.2017 + version: 0.4.1 +- changes: + - action: Updated + details: + - '`pyexcel-xlsx#15`, close file handle' + - pyexcel-io plugin interface now updated to use `lml `_. + date: 19.06.2017 + version: 0.4.0 +- changes: + - action: Updated + details: + - '`#18`, pass on encoding_override and others to xlrd.' + date: 30/05/2017 + version: 0.3.3 +- changes: + - action: Updated + details: + - '`#16`, allow mmap to be passed as file content' + date: 18.05.2017 + version: 0.3.2 +- changes: + - action: Updated + details: + - '`#14`, Python 3.6 - cannot use LOCALE flag with a str pattern' + - fix its dependency on pyexcel-io 0.3.0 + date: 16.01.2017 + version: 0.3.1 +- changes: + - action: Updated + details: + - '`#13`, alert on empyty file content' + - Support pyexcel-io v0.3.0 + date: 22.12.2016 + version: 0.3.0 +- changes: + - action: Updated + details: + - '`#10`, To support generator as member of the incoming two dimensional data' + date: 20.09.2016 + version: 0.2.3 +- changes: + - action: Added + details: + - 'support pagination. two pairs: start_row, row_limit and start_column, column_limit + help you deal with large files.' + date: 31.08.2016 + version: 0.2.2 +- changes: + - action: Added + details: + - '`#9`, `skip_hidden_sheets` is added. By default, hidden sheets are skipped + when reading all sheets. Reading sheet by name or by index are not affected.' + date: 13.07.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-xls'' was added so as to inform pyexcel to use it instead + of other libraries, in the situation where there are more than one plugin for a + file type, e.g. xlsm' + - action: Updated + details: + - support the auto-import feature of pyexcel-io 0.2.0 + - xlwt is now used for python 2 implementation while xlwt-future is used for python + 3 + date: 01.06.2016 + version: 0.2.0 +- changes: + - action: Added + details: + - Passing "streaming=True" to get_data, you will get the two dimensional array as + a generator + - Passing "data=your_generator" to save_data is acceptable too. + date: 17.01.2016 + version: 0.1.0 diff --git a/docs/source/conf.py b/docs/source/conf.py index 6b93174..dc7ae98 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,39 +1,87 @@ # -*- 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 xls format. It' + + ' reads xlsx and xlsm 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-xls' +copyright = '2015-2021 Onni Software Ltd.' +author = 'C.W.' +# The short X.Y version +version = '0.7.0' +# The full version, including alpha/beta/rc tags +release = '0.7.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-xls' -copyright = u'2015-2016 Onni Software Ltd.' -version = '0.2.1' -release = '0.2.2' +# 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-xlsdoc' -latex_elements = {} -latex_documents = [ - ('index', 'pyexcel-xls.tex', u'pyexcel-xls Documentation', - 'Onni Software Ltd.', 'manual'), -] -man_pages = [ - ('index', 'pyexcel-xls', u'pyexcel-xls 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-xls', u'pyexcel-xls Documentation', - 'Onni Software Ltd.', 'pyexcel-xls', 'One line description of project.', + ('index', 'pyexcel-xls', + 'pyexcel-xls Documentation', + 'Onni Software Ltd.', 'pyexcel-xls', + 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..a8ae3f1 --- /dev/null +++ b/format.sh @@ -0,0 +1,3 @@ +isort $(find pyexcel_xls -name "*.py"|xargs echo) $(find tests -name "*.py"|xargs echo) +black -l 79 pyexcel_xls +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_xls.yaml b/pyexcel-xls.yml similarity index 50% rename from pyexcel_xls.yaml rename to pyexcel-xls.yml index 8b53f43..23bc91c 100644 --- a/pyexcel_xls.yaml +++ b/pyexcel-xls.yml @@ -1,12 +1,18 @@ overrides: "pyexcel.yaml" name: "pyexcel-xls" nick_name: xls -version: 0.2.2 -release: 0.2.1 +version: 0.7.0 +current_version: 0.7.0 +release: 0.7.0 file_type: xls +is_on_conda: true dependencies: - - pyexcel-io>=0.2.2 + - pyexcel-io>=0.6.2 - xlrd - - xlwt;python_version<"3" - - xlwt-future;python_version>="3" + - xlwt +test_dependencies: + - pyexcel description: A wrapper library to read, manipulate and write data in xls format. It reads xlsx and xlsm format +moban_command: false +python_requires: ">=3.6" +min_python_version: "3.6" diff --git a/pyexcel_xls/__init__.py b/pyexcel_xls/__init__.py index 75d7a76..35c2ed7 100644 --- a/pyexcel_xls/__init__.py +++ b/pyexcel_xls/__init__.py @@ -4,28 +4,71 @@ The lower level xls/xlsx/xlsm file format handler using xlrd/xlwt - :copyright: (c) 2015-2016 by Onni Software Ltd + :copyright: (c) 2016-2021 by Onni Software Ltd :license: New BSD License """ +import xlrd + # flake8: noqa +from pyexcel_io.io import get_data as read_data +from pyexcel_io.io import isstream +from pyexcel_io.io import save_data as write_data + # this line has to be place above all else # because of dynamic import -_FILE_TYPE = 'xls' -__pyexcel_io_plugins__ = [_FILE_TYPE] +from pyexcel_io.plugins import IOPluginInfoChainV2 + +__FILE_TYPE__ = "xls" -from pyexcel_io.io import get_data as read_data, isstream, store_data as write_data +def xlrd_version_2_or_greater(): + xlrd_version = getattr(xlrd, "__version__") + + if xlrd_version: + major = int(xlrd_version.split(".")[0]) + if major >= 2: + return True + return False + + +XLRD_VERSION_2_OR_ABOVE = xlrd_version_2_or_greater() +supported_file_formats = [__FILE_TYPE__, "xlsx", "xlsm"] +if XLRD_VERSION_2_OR_ABOVE: + supported_file_formats.remove("xlsx") + + +IOPluginInfoChainV2(__name__).add_a_reader( + relative_plugin_class_path="xlsr.XLSInFile", + locations=["file"], + file_types=supported_file_formats, + stream_type="binary", +).add_a_reader( + relative_plugin_class_path="xlsr.XLSInMemory", + locations=["memory"], + file_types=supported_file_formats, + stream_type="binary", +).add_a_reader( + relative_plugin_class_path="xlsr.XLSInContent", + locations=["content"], + file_types=supported_file_formats, + stream_type="binary", +).add_a_writer( + relative_plugin_class_path="xlsw.XLSWriter", + locations=["file", "memory"], + file_types=[__FILE_TYPE__], + stream_type="binary", +) 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) 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) diff --git a/pyexcel_xls/xls.py b/pyexcel_xls/xls.py deleted file mode 100644 index 3cbebfc..0000000 --- a/pyexcel_xls/xls.py +++ /dev/null @@ -1,277 +0,0 @@ -""" - pyexcel_xls - ~~~~~~~~~~~~~~~~~~~ - - The lower level xls/xlsm file format handler using xlrd/xlwt - - :copyright: (c) 2015-2016 by Onni Software Ltd - :license: New BSD License -""" -import sys -import math -import datetime -import xlrd -from xlwt import Workbook, XFStyle - -from pyexcel_io.book import BookReader, BookWriter -from pyexcel_io.sheet import SheetReader, SheetWriter - -PY2 = sys.version_info[0] == 2 -if PY2 and sys.version_info[1] < 7: - from ordereddict import OrderedDict -else: - from collections import OrderedDict - - -DEFAULT_DATE_FORMAT = "DD/MM/YY" -DEFAULT_TIME_FORMAT = "HH:MM:SS" -DEFAULT_DATETIME_FORMAT = "%s %s" % (DEFAULT_DATE_FORMAT, DEFAULT_TIME_FORMAT) - - -def is_integer_ok_for_xl_float(value): - """check if a float value had zero value in digits""" - return value == math.floor(value) - - -def xldate_to_python_date(value): - """ - convert xl date to python date - """ - date_tuple = xlrd.xldate_as_tuple(value, 0) - ret = None - if date_tuple == (0, 0, 0, 0, 0, 0): - ret = datetime.datetime(1900, 1, 1, 0, 0, 0) - elif date_tuple[0:3] == (0, 0, 0): - ret = datetime.time(date_tuple[3], - date_tuple[4], - date_tuple[5]) - elif date_tuple[3:6] == (0, 0, 0): - ret = datetime.date(date_tuple[0], - date_tuple[1], - date_tuple[2]) - else: - ret = datetime.datetime( - date_tuple[0], - date_tuple[1], - date_tuple[2], - date_tuple[3], - date_tuple[4], - date_tuple[5] - ) - return ret - - -class XLSheet(SheetReader): - """ - xls, xlsx, xlsm sheet reader - - Currently only support first sheet in the file - """ - def __init__(self, sheet, auto_detect_int=True, **keywords): - SheetReader.__init__(self, sheet, **keywords) - self.auto_detect_int = auto_detect_int - - @property - def name(self): - return self.native_sheet.name - - def number_of_rows(self): - """ - Number of rows in the xls sheet - """ - return self.native_sheet.nrows - - def number_of_columns(self): - """ - Number of columns in the xls sheet - """ - return self.native_sheet.ncols - - def _cell_value(self, row, column): - """ - Random access to the xls cells - """ - cell_type = self.native_sheet.cell_type(row, column) - value = self.native_sheet.cell_value(row, column) - if cell_type == xlrd.XL_CELL_DATE: - value = xldate_to_python_date(value) - elif cell_type == xlrd.XL_CELL_NUMBER and self.auto_detect_int: - if is_integer_ok_for_xl_float(value): - value = int(value) - return value - - -class XLSBook(BookReader): - """ - XLSBook reader - - It reads xls, xlsm, xlsx work book - """ - def __init__(self): - BookReader.__init__(self) - self.file_content = None - - def open(self, file_name, **keywords): - BookReader.open(self, file_name, **keywords) - self._get_params() - - def open_stream(self, file_stream, **keywords): - BookReader.open_stream(self, file_stream, **keywords) - self._get_params() - - def open_content(self, file_content, **keywords): - self.keywords = keywords - self.file_content = file_content - self._get_params() - - def close(self): - if self.native_book: - self.native_book.release_resources() - - def read_sheet_by_index(self, sheet_index): - self.native_book = self._get_book(on_demand=True) - sheet = self.native_book.sheet_by_index(sheet_index) - return self.read_sheet(sheet) - - def read_sheet_by_name(self, sheet_name): - self.native_book = self._get_book(on_demand=True) - try: - sheet = self.native_book.sheet_by_name(sheet_name) - except xlrd.XLRDError: - raise ValueError("%s cannot be found" % sheet_name) - return self.read_sheet(sheet) - - def read_all(self): - result = OrderedDict() - self.native_book = self._get_book() - for sheet in self.native_book.sheets(): - if self.skip_hidden_sheets and sheet.visibility != 0: - continue - data_dict = self.read_sheet(sheet) - result.update(data_dict) - return result - - def read_sheet(self, native_sheet): - sheet = XLSheet(native_sheet, **self.keywords) - return {sheet.name: sheet.to_array()} - - def _get_book(self, on_demand=False): - if self.file_name: - xls_book = xlrd.open_workbook(self.file_name, on_demand=on_demand) - elif self.file_stream: - xls_book = xlrd.open_workbook( - None, - file_contents=self.file_stream.getvalue(), - on_demand=on_demand - ) - elif self.file_content: - xls_book = xlrd.open_workbook( - None, - file_contents=self.file_content, - on_demand=on_demand - ) - return xls_book - - def _get_params(self): - self.skip_hidden_sheets = self.keywords.get('skip_hidden_sheets', True) - - -class XLSheetWriter(SheetWriter): - """ - xls sheet writer - """ - def set_sheet_name(self, name): - """Create a sheet - """ - self.native_sheet = self.native_book.add_sheet(name) - self.current_row = 0 - - def write_row(self, array): - """ - write a row into the file - """ - for i in range(len(array)): - value = array[i] - style = None - tmp_array = [] - if isinstance(value, datetime.datetime): - tmp_array = [ - value.year, value.month, value.day, - value.hour, value.minute, value.second - ] - value = xlrd.xldate.xldate_from_datetime_tuple(tmp_array, 0) - style = XFStyle() - style.num_format_str = DEFAULT_DATETIME_FORMAT - elif isinstance(value, datetime.date): - tmp_array = [value.year, value.month, value.day] - value = xlrd.xldate.xldate_from_date_tuple(tmp_array, 0) - style = XFStyle() - style.num_format_str = DEFAULT_DATE_FORMAT - elif isinstance(value, datetime.time): - tmp_array = [value.hour, value.minute, value.second] - value = xlrd.xldate.xldate_from_time_tuple(tmp_array) - style = XFStyle() - style.num_format_str = DEFAULT_TIME_FORMAT - if style: - self.native_sheet.write(self.current_row, i, value, style) - else: - self.native_sheet.write(self.current_row, i, value) - self.current_row += 1 - - -class XLSWriter(BookWriter): - """ - xls writer - """ - def __init__(self): - BookWriter.__init__(self) - self.work_book = None - - def open(self, file_name, - encoding='ascii', style_compression=2, **keywords): - BookWriter.open(self, file_name, **keywords) - self.work_book = Workbook(style_compression=style_compression, - encoding=encoding) - - def create_sheet(self, name): - return XLSheetWriter(self.work_book, None, name) - - def close(self): - """ - This call actually save the file - """ - self.work_book.save(self.file_alike_object) - - -_xls_reader_registry = { - "file_type": "xls", - "reader": XLSBook, - "writer": XLSWriter, - "stream_type": "binary", - "mime_type": "application/vnd.ms-excel", - "library": "pyexcel-xls" -} - -_XLSM_MIME = ( - "application/" + - "vnd.openxmlformats-officedocument.spreadsheetml.sheet") - -_xlsm_registry = { - "file_type": "xlsm", - "reader": XLSBook, - "stream_type": "binary", - "mime_type": _XLSM_MIME, - "library": "pyexcel-xls" -} - -_xlsx_registry = { - "file_type": "xlsx", - "reader": XLSBook, - "stream_type": "binary", - "mime_type": "application/vnd.ms-excel.sheet.macroenabled.12", - "library": "pyexcel-xls" -} - -exports = (_xls_reader_registry, - _xlsm_registry, - _xlsx_registry) diff --git a/pyexcel_xls/xlsr.py b/pyexcel_xls/xlsr.py new file mode 100644 index 0000000..c9f15e8 --- /dev/null +++ b/pyexcel_xls/xlsr.py @@ -0,0 +1,217 @@ +""" + pyexcel_xlsr + ~~~~~~~~~~~~~~~~~~~ + + The lower level xls/xlsm file format handler using xlrd + + :copyright: (c) 2016-2021 by Onni Software Ltd + :license: New BSD License +""" +import datetime + +import xlrd +from pyexcel_io.service import has_no_digits_in_float +from pyexcel_io.plugin_api import ISheet, IReader + +XLS_KEYWORDS = [ + "filename", + "logfile", + "verbosity", + "use_mmap", + "file_contents", + "encoding_override", + "formatting_info", + "on_demand", + "ragged_rows", +] +DEFAULT_ERROR_VALUE = "#N/A" + + +class MergedCell(object): + def __init__(self, row_low, row_high, column_low, column_high): + self.__rl = row_low + self.__rh = row_high + self.__cl = column_low + self.__ch = column_high + self.value = None + + def register_cells(self, registry): + for rowx in range(self.__rl, self.__rh): + for colx in range(self.__cl, self.__ch): + key = "%s-%s" % (rowx, colx) + registry[key] = self + + +class XLSheet(ISheet): + """ + xls, xlsx, xlsm sheet reader + + Currently only support first sheet in the file + """ + + def __init__(self, sheet, auto_detect_int=True, date_mode=0, **keywords): + self.__auto_detect_int = auto_detect_int + self.__hidden_cols = [] + self.__hidden_rows = [] + self.__merged_cells = {} + self._book_date_mode = date_mode + self.xls_sheet = sheet + self._keywords = keywords + if keywords.get("detect_merged_cells") is True: + for merged_cell_ranges in sheet.merged_cells: + merged_cells = MergedCell(*merged_cell_ranges) + merged_cells.register_cells(self.__merged_cells) + if keywords.get("skip_hidden_row_and_column") is True: + for col_index, info in self.xls_sheet.colinfo_map.items(): + if info.hidden == 1: + self.__hidden_cols.append(col_index) + for row_index, info in self.xls_sheet.rowinfo_map.items(): + if info.hidden == 1: + self.__hidden_rows.append(row_index) + + @property + def name(self): + return self.xls_sheet.name + + def row_iterator(self): + number_of_rows = self.xls_sheet.nrows - len(self.__hidden_rows) + return range(number_of_rows) + + def column_iterator(self, row): + number_of_columns = self.xls_sheet.ncols - len(self.__hidden_cols) + for column in range(number_of_columns): + yield self.cell_value(row, column) + + def cell_value(self, row, column): + """ + Random access to the xls cells + """ + if self._keywords.get("skip_hidden_row_and_column") is True: + row, column = self._offset_hidden_indices(row, column) + cell_type = self.xls_sheet.cell_type(row, column) + value = self.xls_sheet.cell_value(row, column) + + if cell_type == xlrd.XL_CELL_DATE: + value = xldate_to_python_date(value, self._book_date_mode) + elif cell_type == xlrd.XL_CELL_NUMBER and self.__auto_detect_int: + if has_no_digits_in_float(value): + value = int(value) + elif cell_type == xlrd.XL_CELL_ERROR: + value = DEFAULT_ERROR_VALUE + + if self.__merged_cells: + merged_cell = self.__merged_cells.get("%s-%s" % (row, column)) + if merged_cell: + if merged_cell.value: + value = merged_cell.value + else: + merged_cell.value = value + return value + + def _offset_hidden_indices(self, row, column): + row = calculate_offsets(row, self.__hidden_rows) + column = calculate_offsets(column, self.__hidden_cols) + return row, column + + +def calculate_offsets(incoming_index, hidden_indices): + offset = 0 + for index in hidden_indices: + if index <= (incoming_index + offset): + offset += 1 + return incoming_index + offset + + +class XLSReader(IReader): + """ + XLSBook reader + + It reads xls, xlsm, xlsx work book + """ + + def __init__(self, file_type, **keywords): + self.__skip_hidden_sheets = keywords.get("skip_hidden_sheets", True) + self.__skip_hidden_row_column = keywords.get( + "skip_hidden_row_and_column", True + ) + self.__detect_merged_cells = keywords.get("detect_merged_cells", False) + self._keywords = keywords + xlrd_params = self._extract_xlrd_params() + if self.__skip_hidden_row_column and file_type == "xls": + xlrd_params["formatting_info"] = True + if self.__detect_merged_cells: + xlrd_params["formatting_info"] = True + + self.content_array = [] + self.xls_book = self.get_xls_book(**xlrd_params) + for sheet in self.xls_book.sheets(): + if self.__skip_hidden_sheets and sheet.visibility != 0: + continue + self.content_array.append(sheet) + + def read_sheet(self, index): + native_sheet = self.content_array[index] + sheet = XLSheet( + native_sheet, date_mode=self.xls_book.datemode, **self._keywords + ) + return sheet + + def close(self): + if self.xls_book: + self.xls_book.release_resources() + self.xls_book = None + + def get_xls_book(self, **xlrd_params): + xls_book = xlrd.open_workbook(**xlrd_params) + return xls_book + + def _extract_xlrd_params(self): + params = {} + if self._keywords is not None: + for key in list(self._keywords.keys()): + if key in XLS_KEYWORDS: + params[key] = self._keywords.pop(key) + return params + + +class XLSInFile(XLSReader): + def __init__(self, file_name, file_type, **keywords): + super().__init__(file_type, filename=file_name, **keywords) + + +class XLSInContent(XLSReader): + def __init__(self, file_content, file_type, **keywords): + super().__init__(file_type, file_contents=file_content, **keywords) + + +class XLSInMemory(XLSReader): + def __init__(self, file_stream, file_type, **keywords): + file_stream.seek(0) + super().__init__( + file_type, file_contents=file_stream.read(), **keywords + ) + + +def xldate_to_python_date(value, date_mode): + """ + convert xl date to python date + """ + date_tuple = xlrd.xldate_as_tuple(value, date_mode) + + ret = None + if date_tuple == (0, 0, 0, 0, 0, 0): + ret = datetime.datetime(1900, 1, 1, 0, 0, 0) + elif date_tuple[0:3] == (0, 0, 0): + ret = datetime.time(date_tuple[3], date_tuple[4], date_tuple[5]) + elif date_tuple[3:6] == (0, 0, 0): + ret = datetime.date(date_tuple[0], date_tuple[1], date_tuple[2]) + else: + ret = datetime.datetime( + date_tuple[0], + date_tuple[1], + date_tuple[2], + date_tuple[3], + date_tuple[4], + date_tuple[5], + ) + return ret diff --git a/pyexcel_xls/xlsw.py b/pyexcel_xls/xlsw.py new file mode 100644 index 0000000..f70ec64 --- /dev/null +++ b/pyexcel_xls/xlsw.py @@ -0,0 +1,111 @@ +""" + pyexcel_xlsw + ~~~~~~~~~~~~~~~~~~~ + + The lower level xls file format handler using xlwt + + :copyright: (c) 2016-2021 by Onni Software Ltd + :license: New BSD License +""" +import datetime + +import xlrd +from xlwt import XFStyle, Workbook +from pyexcel_io import constants +from pyexcel_io.plugin_api import IWriter, ISheetWriter + +DEFAULT_DATE_FORMAT = "DD/MM/YY" +DEFAULT_TIME_FORMAT = "HH:MM:SS" +DEFAULT_LONGTIME_FORMAT = "[HH]:MM:SS" +DEFAULT_DATETIME_FORMAT = "%s %s" % (DEFAULT_DATE_FORMAT, DEFAULT_TIME_FORMAT) +EMPTY_SHEET_NOT_ALLOWED = "xlwt does not support a book without any sheets" + + +class XLSheetWriter(ISheetWriter): + """ + xls sheet writer + """ + + def __init__(self, xls_book, xls_sheet, sheet_name): + if sheet_name is None: + sheet_name = constants.DEFAULT_SHEET_NAME + self._xls_book = xls_book + self._xls_sheet = xls_sheet + self._xls_sheet = self._xls_book.add_sheet(sheet_name) + self.current_row = 0 + + def write_row(self, array): + """ + write a row into the file + """ + for i, value in enumerate(array): + style = None + tmp_array = [] + if isinstance(value, datetime.datetime): + tmp_array = [ + value.year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + ] + value = xlrd.xldate.xldate_from_datetime_tuple(tmp_array, 0) + style = XFStyle() + style.num_format_str = DEFAULT_DATETIME_FORMAT + elif isinstance(value, datetime.timedelta): + value = value.days + value.seconds / 86_400 + style = XFStyle() + style.num_format_str = DEFAULT_LONGTIME_FORMAT + elif isinstance(value, datetime.date): + tmp_array = [value.year, value.month, value.day] + value = xlrd.xldate.xldate_from_date_tuple(tmp_array, 0) + style = XFStyle() + style.num_format_str = DEFAULT_DATE_FORMAT + elif isinstance(value, datetime.time): + tmp_array = [value.hour, value.minute, value.second] + value = xlrd.xldate.xldate_from_time_tuple(tmp_array) + style = XFStyle() + style.num_format_str = DEFAULT_TIME_FORMAT + if style: + self._xls_sheet.write(self.current_row, i, value, style) + else: + self._xls_sheet.write(self.current_row, i, value) + self.current_row += 1 + + def close(self): + pass + + +class XLSWriter(IWriter): + """ + xls writer + """ + + def __init__( + self, + file_alike_object, + _, # file_type not used + encoding="ascii", + style_compression=2, + **keywords, + ): + self.file_alike_object = file_alike_object + self.work_book = Workbook( + style_compression=style_compression, encoding=encoding + ) + + def create_sheet(self, name): + return XLSheetWriter(self.work_book, None, name) + + def write(self, incoming_dict): + if incoming_dict: + IWriter.write(self, incoming_dict) + else: + raise NotImplementedError(EMPTY_SHEET_NOT_ALLOWED) + + def close(self): + """ + This call actually save the file + """ + self.work_book.save(self.file_alike_object) diff --git a/requirements.txt b/requirements.txt index efb555b..4c06b39 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -pyexcel-io>=0.2.2 +pyexcel-io>=0.6.2 xlrd -xlwt;python_version<"3" -xlwt-future;python_version>="3" +xlwt diff --git a/rnd_requirements.txt b/rnd_requirements.txt index 30a50e9..dffaa2d 100644 --- a/rnd_requirements.txt +++ b/rnd_requirements.txt @@ -1 +1,2 @@ -https://github.com/pyexcel/pyexcel-io/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 4a8f428..0763a62 100644 --- a/setup.py +++ b/setup.py @@ -1,92 +1,212 @@ -try: - from setuptools import setup, find_packages -except ImportError: - from ez_setup import use_setuptools - use_setuptools() - from setuptools import setup, find_packages +#!/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 + PY2 = sys.version_info[0] == 2 PY26 = PY2 and sys.version_info[1] < 7 +PY33 = sys.version_info < (3, 4) -NAME = 'pyexcel-xls' -AUTHOR = 'C.W.' -VERSION = '0.2.2' -EMAIL = 'wangc_2011 (at) hotmail.com' -LICENSE = 'New BSD' -PACKAGES = find_packages(exclude=['ez_setup', 'examples', 'tests']) +# 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: + 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-xls" +AUTHOR = "C.W." +VERSION = "0.7.0" +EMAIL = "info@pyexcel.org" +LICENSE = "New BSD" DESCRIPTION = ( - 'A wrapper library to read, manipulate and write data in xls format. It' + - ' reads xlsx and xlsm format' + - '' + "A wrapper library to read, manipulate and write data in xls format. It" + + "reads xlsx and xlsm format" ) +URL = "https://github.com/pyexcel/pyexcel-xls" +DOWNLOAD_URL = "%s/archive/0.7.0.tar.gz" % URL +FILES = ["README.rst","CONTRIBUTORS.rst", "CHANGELOG.rst"] KEYWORDS = [ - 'excel', - 'python', - 'pyexcel', + "python", 'xls', 'xlsx', 'xlsm' ] -INSTALL_REQUIRES = [ - 'pyexcel-io>=0.2.2', - 'xlrd', -] - -if PY2: - INSTALL_REQUIRES.append('xlwt') -if not PY2: - INSTALL_REQUIRES.append('xlwt-future') - -EXTRAS_REQUIRE = { -} - CLASSIFIERS = [ - 'Topic :: Office/Business', - 'Topic :: Utilities', - 'Topic :: Software Development :: Libraries', - 'Programming Language :: Python', - 'License :: OSI Approved :: BSD License', - 'Intended Audience :: Developers', - 'Development Status :: 3 - Alpha', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', + "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", + 'Programming Language :: Python :: Implementation :: PyPy' ] +PYTHON_REQUIRES = ">=3.6" + +INSTALL_REQUIRES = [ + "pyexcel-io>=0.6.2", + "xlrd", + "xlwt", +] +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-xls v0.7.0 " + + "Find 0.7.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_xls.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 00c3550..823adb3 100644 --- a/test.bat +++ b/test.bat @@ -1,3 +1,2 @@ - pip freeze -nosetests --with-cov --cover-package pyexcel_xls --cover-package tests --with-doctest --doctest-extension=.rst tests README.rst pyexcel_xls && flake8 . --exclude=.moban.d --builtins=unicode,xrange,long +nosetests --with-coverage --cover-package pyexcel_xls --cover-package tests tests --with-doctest --doctest-extension=.rst README.rst docs/source pyexcel_xls diff --git a/tests/_compact.py b/tests/_compact.py index 08694b3..e47d675 100644 --- a/tests/_compact.py +++ b/tests/_compact.py @@ -1,7 +1,6 @@ # flake8: noqa import sys - if sys.version_info[0] == 2 and sys.version_info[1] < 7: from ordereddict import OrderedDict else: diff --git a/tests/base.py b/tests/base.py index b12e825..71474a3 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,31 +1,35 @@ -import os +import os # noqa +import datetime # noqa + import pyexcel +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]) table.append(data[8:12]) - pyexcel.save_as(dest_file_name=file, array=table) + pyexcel.save_as(array=table, dest_file_name=file) 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: @@ -35,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): @@ -48,29 +53,13 @@ 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()) - assert actual == self.content - - def test_write_reader(self): - """ - Use reader as data container - - this test case shows the file written by pyexcel - can be read back by itself - """ - self._create_a_file(self.testfile) - r = pyexcel.Reader(self.testfile) - r.save_as(self.testfile2) - r2 = pyexcel.Reader(self.testfile2) - r2.format(int) - actual = pyexcel.utils.to_array(r2.rows()) + actual = list(r.rows()) assert actual == self.content class PyexcelMultipleSheetBase: - def _write_test_file(self, filename): - pyexcel.save_book_as(dest_file_name=filename, bookdict=self.content) + pyexcel.save_book_as(bookdict=self.content, dest_file_name=filename) def _clean_up(self): if os.path.exists(self.testfile2): @@ -87,36 +76,17 @@ 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.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 diff --git a/tests/fixtures/complex-merged-cells-sheet.xls b/tests/fixtures/complex-merged-cells-sheet.xls new file mode 100755 index 0000000..d576c46 Binary files /dev/null and b/tests/fixtures/complex-merged-cells-sheet.xls differ diff --git a/tests/fixtures/complex_hidden_sheets.xls b/tests/fixtures/complex_hidden_sheets.xls new file mode 100644 index 0000000..5ad2ca0 Binary files /dev/null and b/tests/fixtures/complex_hidden_sheets.xls differ diff --git a/tests/fixtures/hidden.xls b/tests/fixtures/hidden.xls new file mode 100755 index 0000000..e2d03ff Binary files /dev/null and b/tests/fixtures/hidden.xls differ diff --git a/tests/fixtures/merged-cell-sheet.xls b/tests/fixtures/merged-cell-sheet.xls new file mode 100755 index 0000000..c5754b8 Binary files /dev/null and b/tests/fixtures/merged-cell-sheet.xls differ diff --git a/tests/fixtures/merged-sheet-exploration.xls b/tests/fixtures/merged-sheet-exploration.xls new file mode 100755 index 0000000..ef359ab Binary files /dev/null and b/tests/fixtures/merged-sheet-exploration.xls differ diff --git a/tests/fixtures/pyexcel_issue_151.xlsx b/tests/fixtures/pyexcel_issue_151.xlsx new file mode 100644 index 0000000..fd2b54a Binary files /dev/null and b/tests/fixtures/pyexcel_issue_151.xlsx differ diff --git a/tests/test-fixtures/test-date-format.xls b/tests/fixtures/test-date-format.xls similarity index 100% rename from tests/test-fixtures/test-date-format.xls rename to tests/fixtures/test-date-format.xls diff --git a/tests/requirements.txt b/tests/requirements.txt index 74a0108..3e19c2a 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,5 +1,12 @@ nose +mock;python_version<"3" codecov coverage flake8 +black +isort +collective.checkdocs +pygments +moban +moban_jinja2_github pyexcel diff --git a/tests/test_bug_fixes.py b/tests/test_bug_fixes.py index cca61f2..85a97dd 100644 --- a/tests/test_bug_fixes.py +++ b/tests/test_bug_fixes.py @@ -5,43 +5,122 @@ """ import os -import pyexcel as pe -from pyexcel_xls import save_data -from _compact import OrderedDict -from nose.tools import eq_ import datetime +from unittest.mock import MagicMock, patch + +import pyexcel as pe +from _compact import OrderedDict +from pyexcel_xls import XLRD_VERSION_2_OR_ABOVE, save_data +from pyexcel_xls.xlsr import xldate_to_python_date +from pyexcel_xls.xlsw import XLSWriter as Writer + +from nose import SkipTest +from nose.tools import eq_, raises + +IN_TRAVIS = "TRAVIS" in os.environ -class TestBugFix: - def test_pyexcel_issue_5(self): - """pyexcel issue #5 +def test_pyexcel_issue_5(): + """pyexcel issue #5 - datetime is not properly parsed - """ - s = pe.load(os.path.join("tests", - "test-fixtures", - "test-date-format.xls")) - assert s[0, 0] == datetime.datetime(2015, 11, 11, 11, 12, 0) + datetime is not properly parsed + """ + s = pe.load(get_fixture("test-date-format.xls")) + assert s[0, 0] == datetime.datetime(2015, 11, 11, 11, 12, 0) - def test_pyexcel_xls_issue_2(self): - data = OrderedDict() - array = [] - for i in range(4100): - array.append([datetime.datetime.now()]) - data.update({"test": array}) - save_data("test.xls", data) - os.unlink("test.xls") - def test_issue_9_hidden_sheet(self): - test_file = os.path.join("tests", "fixtures", "hidden_sheets.xls") - book_dict = pe.get_book_dict(file_name=test_file) - assert "hidden" not in book_dict - eq_(book_dict['shown'], [['A', 'B']]) +def test_pyexcel_xls_issue_2(): + data = OrderedDict() + array = [] + for i in range(4100): + array.append([datetime.datetime.now()]) + data.update({"test": array}) + save_data("test.xls", data) + os.unlink("test.xls") - def test_issue_9_hidden_sheet_2(self): - test_file = os.path.join("tests", "fixtures", "hidden_sheets.xls") - book_dict = pe.get_book_dict(file_name=test_file, - skip_hidden_sheets=False) - assert "hidden" in book_dict - eq_(book_dict['shown'], [['A', 'B']]) - eq_(book_dict['hidden'], [['a', 'b']]) + +def test_issue_9_hidden_sheet(): + test_file = get_fixture("hidden_sheets.xls") + book_dict = pe.get_book_dict(file_name=test_file) + assert "hidden" not in book_dict + eq_(book_dict["shown"], [["A", "B"]]) + + +def test_issue_9_hidden_sheet_2(): + test_file = get_fixture("hidden_sheets.xls") + book_dict = pe.get_book_dict(file_name=test_file, skip_hidden_sheets=False) + assert "hidden" in book_dict + eq_(book_dict["shown"], [["A", "B"]]) + eq_(book_dict["hidden"], [["a", "b"]]) + + +def test_issue_10_generator_as_content(): + def data_gen(): + def custom_row_renderer(row): + for e in row: + yield e + + for i in range(2): + yield custom_row_renderer([1, 2]) + + save_data("test.xls", {"sheet": data_gen()}) + + +@raises(IOError) +def test_issue_13_empty_file_content(): + pe.get_sheet(file_content="", file_type="xls") + + +def test_issue_16_file_stream_has_no_getvalue(): + test_file = get_fixture("hidden_sheets.xls") + with open(test_file, "rb") as f: + pe.get_sheet(file_stream=f, file_type="xls") + + +@patch("xlrd.open_workbook") +def test_issue_18_encoding_override_isnt_passed(fake_open): + fake_open.return_value = MagicMock(sheets=MagicMock(return_value=[])) + test_encoding = "utf-32" + from pyexcel_xls.xlsr import XLSInFile + + XLSInFile("fake_file.xls", "xls", encoding_override=test_encoding) + keywords = fake_open.call_args[1] + assert keywords["encoding_override"] == test_encoding + + +def test_issue_20(): + if not IN_TRAVIS: + raise SkipTest() + pe.get_book( + url="https://github.com/pyexcel/pyexcel-xls/raw/master/tests/fixtures/file_with_an_empty_sheet.xls" # noqa: E501 + ) + + +def test_issue_151(): + if XLRD_VERSION_2_OR_ABOVE: + raise SkipTest() + s = pe.get_sheet( + file_name=get_fixture("pyexcel_issue_151.xlsx"), + skip_hidden_row_and_column=False, + library="pyexcel-xls", + ) + eq_("#N/A", s[0, 0]) + + +@raises(NotImplementedError) +def test_empty_book_pyexcel_issue_120(): + """ + https://github.com/pyexcel/pyexcel/issues/120 + """ + writer = Writer("fake.xls", "xls") + writer.write({}) + + +def test_pyexcel_issue_54(): + xlvalue = 41071.0 + date = xldate_to_python_date(xlvalue, 1) + eq_(date, datetime.date(2016, 6, 12)) + + +def get_fixture(file_name): + return os.path.join("tests", "fixtures", file_name) diff --git a/tests/test_filter.py b/tests/test_filter.py index 2d760b2..cd07d3c 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-xls") + filtered_data = get_data( + self.test_file, start_row=3, library="pyexcel-xls" + ) 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-xls") + filtered_data = get_data( + self.test_file, start_row=3, row_limit=1, library="pyexcel-xls" + ) 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-xls") - 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-xls" + ) + 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-xls") + filtered_data = get_data( + self.test_file, + start_column=1, + column_limit=1, + library="pyexcel-xls", + ) 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-xls") + filtered_data = get_data( + self.test_file, start_column=1, start_row=3, library="pyexcel-xls" + ) 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-xls") + filtered_data = get_data( + self.test_file, + start_column=1, + column_limit=1, + start_row=3, + row_limit=1, + library="pyexcel-xls", + ) expected = [[24]] eq_(filtered_data[self.sheet_name], expected) diff --git a/tests/test_formatters.py b/tests/test_formatters.py index f18fd3a..750a60f 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -1,86 +1,123 @@ import os -import datetime -from unittest import TestCase from textwrap import dedent import pyexcel as pe +from nose.tools import eq_ + class TestDateFormat: def test_reading_date_format(self): """ date time 25/12/14 11:11:11 - 25/12/14 12:11:11 + 25/12/14 12:12:12 01/01/15 13:13:13 0.0 0.0 """ - r = pe.get_sheet(file_name=os.path.join("tests", "fixtures", - "date_field.xls")) - assert isinstance(r[1, 0], datetime.date) is True - assert r[1, 0].strftime("%d/%m/%y") == "25/12/14" + import datetime + + r = pe.get_sheet( + file_name=os.path.join("tests", "fixtures", "date_field.xls"), + library="pyexcel-xls", + ) + assert isinstance(r[1, 0], datetime.date) + eq_(r[1, 0].strftime("%d/%m/%y"), "25/12/14") assert isinstance(r[1, 1], datetime.time) is True assert r[1, 1].strftime("%H:%M:%S") == "11:11:11" assert r[4, 0].strftime("%d/%m/%Y") == "01/01/1900" assert r[4, 1].strftime("%H:%M:%S") == "00:00:00" def test_writing_date_format(self): + import datetime + excel_filename = "testdateformat.xls" - data = [[datetime.date(2014, 12, 25), + data = [ + [ + datetime.date(2014, 12, 25), datetime.time(11, 11, 11), - datetime.datetime(2014, 12, 25, 11, 11, 11)]] + datetime.datetime(2014, 12, 25, 11, 11, 11), + datetime.timedelta( + days=50, + seconds=27, + microseconds=10, + milliseconds=29000, + minutes=5, + hours=8, + weeks=2, + ), + ] + ] pe.save_as(dest_file_name=excel_filename, array=data) - r = pe.get_sheet(file_name=excel_filename) + r = pe.get_sheet(file_name=excel_filename, library="pyexcel-xls") assert isinstance(r[0, 0], datetime.date) is True assert r[0, 0].strftime("%d/%m/%y") == "25/12/14" assert isinstance(r[0, 1], datetime.time) is True assert r[0, 1].strftime("%H:%M:%S") == "11:11:11" assert isinstance(r[0, 2], datetime.date) is True assert r[0, 2].strftime("%d/%m/%y %H:%M:%S") == "25/12/14 11:11:11" + assert isinstance(r[0, 3], datetime.datetime) + assert r[0, 3].strftime("%D-%H:%M:%S") == "03/04/00-08:05:56" os.unlink(excel_filename) -class TestAutoDetectInt(TestCase): +class TestAutoDetectInt: def setUp(self): self.content = [[1, 2, 3.1]] self.test_file = "test_auto_detect_init.xls" 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-xls") + 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-xls") + 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-xls", + ) + 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-xls", + ) + 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_hidden.py b/tests/test_hidden.py new file mode 100644 index 0000000..62d6c29 --- /dev/null +++ b/tests/test_hidden.py @@ -0,0 +1,23 @@ +import os + +from pyexcel_xls import get_data + +from nose.tools import eq_ + + +def test_simple_hidden_sheets(): + data = get_data( + os.path.join("tests", "fixtures", "hidden.xls"), + skip_hidden_row_and_column=True, + ) + expected = [[1, 3], [7, 9]] + eq_(data["Sheet1"], expected) + + +def test_complex_hidden_sheets(): + data = get_data( + os.path.join("tests", "fixtures", "complex_hidden_sheets.xls"), + skip_hidden_row_and_column=True, + ) + expected = [[1, 3, 5, 7, 9], [31, 33, 35, 37, 39], [61, 63, 65, 67]] + eq_(data["Sheet1"], expected) diff --git a/tests/test_merged_cells.py b/tests/test_merged_cells.py new file mode 100644 index 0000000..6f243ac --- /dev/null +++ b/tests/test_merged_cells.py @@ -0,0 +1,84 @@ +import os + +from pyexcel_xls import get_data +from pyexcel_xls.xlsr import MergedCell + +from nose.tools import eq_ + + +def test_merged_cells(): + data = get_data( + get_fixture("merged-cell-sheet.xls"), + detect_merged_cells=True, + library="pyexcel-xls", + ) + expected = [[1, 2, 3], [1, 5, 6], [1, 8, 9], [10, 11, 11]] + eq_(data["Sheet1"], expected) + + +def test_complex_merged_cells(): + data = get_data( + get_fixture("complex-merged-cells-sheet.xls"), + detect_merged_cells=True, + library="pyexcel-xls", + ) + expected = [ + [1, 1, 2, 3, 15, 16, 22, 22, 24, 24], + [1, 1, 4, 5, 15, 17, 22, 22, 24, 24], + [6, 7, 8, 9, 15, 18, 22, 22, 24, 24], + [10, 11, 11, 12, 19, 19, 23, 23, 24, 24], + [13, 11, 11, 14, 20, 20, 23, 23, 24, 24], + [21, 21, 21, 21, 21, 21, 23, 23, 24, 24], + [25, 25, 25, 25, 25, 25, 25, 25, 25, 25], + [25, 25, 25, 25, 25, 25, 25, 25, 25, 25], + ] + eq_(data["Sheet1"], expected) + + +def test_exploration(): + data = get_data( + get_fixture("merged-sheet-exploration.xls"), + detect_merged_cells=True, + library="pyexcel-xls", + ) + expected_sheet1 = [ + [1, 1, 1, 1, 1, 1], + [2], + [2], + [2], + [2], + [2], + [2], + [2], + [2], + [2], + ] + eq_(data["Sheet1"], expected_sheet1) + expected_sheet2 = [[3], [3], [3], [3, 4, 4, 4, 4, 4, 4], [3], [3], [3]] + eq_(data["Sheet2"], expected_sheet2) + expected_sheet3 = [ + ["", "", "", "", "", 2, 2, 2], + [], + [], + [], + ["", "", "", 5], + ["", "", "", 5], + ["", "", "", 5], + ["", "", "", 5], + ["", "", "", 5], + ] + eq_(data["Sheet3"], expected_sheet3) + + +def test_merged_cell_class(): + test_dict = {} + merged_cell = MergedCell(1, 4, 1, 4) + merged_cell.register_cells(test_dict) + keys = sorted(list(test_dict.keys())) + expected = ["1-1", "1-2", "1-3", "2-1", "2-2", "2-3", "3-1", "3-2", "3-3"] + eq_(keys, expected) + eq_(merged_cell, test_dict["3-1"]) + + +def get_fixture(file_name): + return os.path.join("tests", "fixtures", file_name) diff --git a/tests/test_mutliple_sheets.py b/tests/test_multiple_sheets.py similarity index 83% rename from tests/test_mutliple_sheets.py rename to tests/test_multiple_sheets.py index 7a5abce..3f526f7 100644 --- a/tests/test_mutliple_sheets.py +++ b/tests/test_multiple_sheets.py @@ -1,9 +1,11 @@ import os import sys + import pyexcel -from nose.tools import raises 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: @@ -31,8 +33,7 @@ class TestAddBooks: 3,3,3,3 """ self.rows = 3 - pyexcel.save_book_as(bookdict=self.content, - dest_file_name=file) + pyexcel.save_book_as(bookdict=self.content, dest_file_name=file) def setUp(self): self.testfile = "multiple1.xls" @@ -45,12 +46,12 @@ 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): @@ -92,10 +93,10 @@ class TestAddBooks: """ test this scenario: book3 = book1 + book2 """ - b1 = pyexcel.BookReader(self.testfile) - b2 = pyexcel.BookReader(self.testfile2) + 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: @@ -108,12 +109,12 @@ class TestAddBooks: def test_add_book1_in_place(self): """ - test this scenario book1 += book2 + test this scenario: book1 += book2 """ 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: @@ -126,12 +127,12 @@ class TestAddBooks: def test_add_book2(self): """ - test this scenario book3 = book1 + sheet3 + test this scenario: book3 = book1 + sheet3 """ 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: @@ -144,12 +145,12 @@ class TestAddBooks: def test_add_book2_in_place(self): """ - test this scenario book3 = book1 + sheet3 + test this scenario: book3 = book1 + sheet3 """ 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: @@ -162,12 +163,12 @@ class TestAddBooks: def test_add_book3(self): """ - test this scenario book3 = sheet1 + sheet2 + test this scenario: book3 = sheet1 + sheet2 """ 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"] @@ -175,12 +176,12 @@ class TestAddBooks: def test_add_book4(self): """ - test this scenario book3 = sheet1 + book + test this scenario: book3 = sheet1 + book """ 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: @@ -219,18 +220,17 @@ class TestMultiSheetReader: self.testfile = "file_with_an_empty_sheet.xls" 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_stringio.py b/tests/test_stringio.py index 0d8c4fe..e1aa055 100644 --- a/tests/test_stringio.py +++ b/tests/test_stringio.py @@ -1,30 +1,32 @@ import os + import pyexcel from base import create_sample_file1 +from nose.tools import eq_ + class TestStringIO: - def test_xls_stringio(self): - xlsfile = "cute.xls" - create_sample_file1(xlsfile) - with open(xlsfile, "rb") as f: + testfile = "cute.xls" + create_sample_file1(testfile) + with open(testfile, "rb") as f: content = f.read() - r = pyexcel.get_sheet(file_type="xls", file_content=content) - result = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 1.1, 1] - actual = pyexcel.utils.to_array(r.enumerate()) - assert result == actual - if os.path.exists(xlsfile): - os.unlink(xlsfile) + r = pyexcel.get_sheet( + file_type="xls", file_content=content, library="pyexcel-xls" + ) + 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="xls", - array=data) - r = pyexcel.get_sheet(file_type="xls", file_content=io.getvalue()) + data = [[1, 2, 3], [4, 5, 6]] + io = pyexcel.save_as(dest_file_type="xls", array=data) + r = pyexcel.get_sheet( + file_type="xls", file_content=io.getvalue(), library="pyexcel-xls" + ) result = [1, 2, 3, 4, 5, 6] - actual = pyexcel.utils.to_array(r.enumerate()) - assert result == actual + actual = list(r.enumerate()) + eq_(result, actual) diff --git a/tests/test_writer.py b/tests/test_writer.py index 561d420..12004e8 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -1,24 +1,22 @@ import os -from pyexcel_xls.xls import XLSWriter, XLSBook + from base import PyexcelWriterBase, PyexcelHatWriterBase +from pyexcel_xls import get_data +from pyexcel_xls.xlsw import XLSWriter as Writer -class TestNativeXLWriter: +class TestNativeXLSWriter: def test_write_book(self): 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 = "xlwriter.xls" - writer = XLSWriter() - writer.open(self.testfile) + self.testfile = "writer.xls" + writer = Writer(self.testfile, "xls") writer.write(self.content) writer.close() - reader = XLSBook() - reader.open(self.testfile) - content = reader.read_all() - reader.close() + content = get_data(self.testfile) for key in content.keys(): content[key] = list(content[key]) assert content == self.content @@ -28,7 +26,7 @@ class TestNativeXLWriter: os.unlink(self.testfile) -class TestXLSnCSVWriter(PyexcelWriterBase): +class TestxlsnCSVWriter(PyexcelWriterBase): def setUp(self): self.testfile = "test.xls" self.testfile2 = "test.csv" @@ -40,7 +38,7 @@ class TestXLSnCSVWriter(PyexcelWriterBase): os.unlink(self.testfile2) -class TestXLSHatWriter(PyexcelHatWriterBase): +class TestxlsHatWriter(PyexcelHatWriterBase): def setUp(self): self.testfile = "test.xls"