├── .github ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── lint.yml │ ├── moban-update.yml │ ├── pythonpublish.yml │ └── tests.yml ├── .gitignore ├── .isort.cfg ├── .moban.d ├── LICENSE ├── custom_README.rst.jj2 ├── custom_setup.py.jj2 ├── requirements.txt └── tests │ ├── base.py │ ├── requirements.txt │ └── test_formatters.py ├── .moban.yml ├── .readthedocs.yml ├── CHANGELOG.rst ├── CONTRIBUTORS.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── Pipfile ├── README.rst ├── changelog.yml ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ └── index.rst ├── format.sh ├── lint.sh ├── pyexcel-xlsx.yml ├── pyexcel_xlsx ├── __init__.py ├── book.py ├── xlsxr.py └── xlsxw.py ├── requirements.txt ├── rnd_requirements.txt ├── setup.cfg ├── setup.py ├── test.bat ├── test.sh └── tests ├── base.py ├── fixtures ├── complex-merged-cells-sheet.xlsx ├── complex_hidden_sheets.xlsx ├── date_field.xlsx ├── file_with_an_empty_sheet.xlsx ├── hidden.xlsx ├── hidden_sheets.xlsx ├── merged-cell-sheet.xlsx ├── merged-sheet-exploration.xlsx ├── test-date-format.xls └── test8.xlsx ├── requirements.txt ├── test_book.py ├── test_bug_fixes.py ├── test_filter.py ├── test_formatters.py ├── test_hidden.py ├── test_merged_cells.py ├── test_multiple_sheets.py ├── test_reader.py ├── test_stringio.py └── test_writer.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: chfw 4 | patreon: chfw 5 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | With your PR, here is a check list: 2 | 3 | - [ ] Has test cases written? 4 | - [ ] Has all code lines tested? 5 | - [ ] Has `make format` been run? 6 | - [ ] Please update CHANGELOG.yml(not CHANGELOG.rst) 7 | - [ ] Has fair amount of documentation if your change is complex 8 | - [ ] Agree on NEW BSD License for your contribution 9 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | name: lint code 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Set up Python 12 | uses: actions/setup-python@v1 13 | with: 14 | python-version: 3.11 15 | - name: lint 16 | run: | 17 | pip --use-deprecated=legacy-resolver install flake8 18 | pip --use-deprecated=legacy-resolver install -r tests/requirements.txt 19 | flake8 --exclude=.moban.d,docs,setup.py --builtins=unicode,xrange,long . 20 | python setup.py checkdocs 21 | -------------------------------------------------------------------------------- /.github/workflows/moban-update.yml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | 3 | jobs: 4 | run_moban: 5 | runs-on: ubuntu-latest 6 | name: synchronize templates via moban 7 | steps: 8 | - uses: actions/checkout@v2 9 | with: 10 | ref: ${{ github.head_ref }} 11 | token: ${{ secrets.PAT }} 12 | - name: Set up Python 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: '3.11' 16 | - name: check changes 17 | run: | 18 | pip install markupsafe==2.0.1 19 | pip install ruamel.yaml moban gitfs2 pypifs moban-jinja2-github moban-ansible 20 | moban 21 | git status 22 | git diff --exit-code 23 | - name: Auto-commit 24 | if: failure() 25 | uses: stefanzweifel/git-auto-commit-action@v4 26 | with: 27 | commit_message: >- 28 | This is an auto-commit, updating project meta data, 29 | such as changelog.rst, contributors.rst 30 | -------------------------------------------------------------------------------- /.github/workflows/pythonpublish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | pypi-publish: 9 | name: upload release to PyPI 10 | runs-on: ubuntu-latest 11 | # Specifying a GitHub environment is optional, but strongly encouraged 12 | environment: pypi 13 | permissions: 14 | # IMPORTANT: this permission is mandatory for trusted publishing 15 | id-token: write 16 | steps: 17 | # retrieve your distributions here 18 | - uses: actions/checkout@v1 19 | - name: Set up Python 20 | uses: actions/setup-python@v1 21 | with: 22 | python-version: '3.x' 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install setuptools wheel 27 | - name: Build 28 | run: | 29 | python setup.py sdist bdist_wheel 30 | - name: Publish package distributions to PyPI 31 | uses: pypa/gh-action-pypi-publish@release/v1 32 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run unit tests on Windows, Ubuntu and Mac 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | test: 8 | name: ${{ matrix.os }} / ${{ matrix.python_version }} 9 | runs-on: ${{ matrix.os }}-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | os: [Ubuntu] 14 | python_version: ["3.9.16"] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python_version }} 22 | architecture: x64 23 | 24 | - name: install 25 | run: | 26 | pip --use-deprecated=legacy-resolver install -r requirements.txt 27 | pip --use-deprecated=legacy-resolver install -r tests/requirements.txt 28 | pip --use-deprecated=legacy-resolver install -r rnd_requirements.txt 29 | - name: test 30 | run: | 31 | pip freeze 32 | nosetests --verbosity=3 --with-coverage --cover-package pyexcel_xlsx --cover-package tests tests --with-doctest --doctest-extension=.rst README.rst docs/source pyexcel_xlsx 33 | - name: Upload coverage 34 | uses: codecov/codecov-action@v1 35 | with: 36 | name: ${{ matrix.os }} Python ${{ matrix.python-version }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # moban hashes 2 | .moban.hashes 3 | 4 | # Extra rules from https://github.com/github/gitignore/ 5 | # Python rules 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | .pybuilder/ 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | # For a library or package, you might want to ignore these files since the code is 92 | # intended to run in multiple environments; otherwise, check them in: 93 | # .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 103 | __pypackages__/ 104 | 105 | # Celery stuff 106 | celerybeat-schedule 107 | celerybeat.pid 108 | 109 | # SageMath parsed files 110 | *.sage.py 111 | 112 | # Environments 113 | .env 114 | .venv 115 | env/ 116 | venv/ 117 | ENV/ 118 | env.bak/ 119 | venv.bak/ 120 | 121 | # Spyder project settings 122 | .spyderproject 123 | .spyproject 124 | 125 | # Rope project settings 126 | .ropeproject 127 | 128 | # mkdocs documentation 129 | /site 130 | 131 | # mypy 132 | .mypy_cache/ 133 | .dmypy.json 134 | dmypy.json 135 | 136 | # Pyre type checker 137 | .pyre/ 138 | 139 | # pytype static type analyzer 140 | .pytype/ 141 | 142 | # Cython debug symbols 143 | cython_debug/ 144 | 145 | # VirtualEnv rules 146 | # Virtualenv 147 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 148 | .Python 149 | [Bb]in 150 | [Ii]nclude 151 | [Ll]ib 152 | [Ll]ib64 153 | [Ll]ocal 154 | [Ss]cripts 155 | pyvenv.cfg 156 | .venv 157 | pip-selfcheck.json 158 | 159 | # Linux rules 160 | *~ 161 | 162 | # temporary files which can be created if a process still has a handle open of a deleted file 163 | .fuse_hidden* 164 | 165 | # KDE directory preferences 166 | .directory 167 | 168 | # Linux trash folder which might appear on any partition or disk 169 | .Trash-* 170 | 171 | # .nfs files are created when an open file is removed but is still being accessed 172 | .nfs* 173 | 174 | # Windows rules 175 | # Windows thumbnail cache files 176 | Thumbs.db 177 | Thumbs.db:encryptable 178 | ehthumbs.db 179 | ehthumbs_vista.db 180 | 181 | # Dump file 182 | *.stackdump 183 | 184 | # Folder config file 185 | [Dd]esktop.ini 186 | 187 | # Recycle Bin used on file shares 188 | $RECYCLE.BIN/ 189 | 190 | # Windows Installer files 191 | *.cab 192 | *.msi 193 | *.msix 194 | *.msm 195 | *.msp 196 | 197 | # Windows shortcuts 198 | *.lnk 199 | 200 | # macOS rules 201 | # General 202 | .DS_Store 203 | .AppleDouble 204 | .LSOverride 205 | 206 | # Icon must end with two \r 207 | Icon 208 | 209 | 210 | # Thumbnails 211 | ._* 212 | 213 | # Files that might appear in the root of a volume 214 | .DocumentRevisions-V100 215 | .fseventsd 216 | .Spotlight-V100 217 | .TemporaryItems 218 | .Trashes 219 | .VolumeIcon.icns 220 | .com.apple.timemachine.donotpresent 221 | 222 | # Directories potentially created on remote AFP share 223 | .AppleDB 224 | .AppleDesktop 225 | Network Trash Folder 226 | Temporary Items 227 | .apdisk 228 | 229 | # Emacs rules 230 | # -*- mode: gitignore; -*- 231 | *~ 232 | \#*\# 233 | /.emacs.desktop 234 | /.emacs.desktop.lock 235 | *.elc 236 | auto-save-list 237 | tramp 238 | .\#* 239 | 240 | # Org-mode 241 | .org-id-locations 242 | *_archive 243 | 244 | # flymake-mode 245 | *_flymake.* 246 | 247 | # eshell files 248 | /eshell/history 249 | /eshell/lastdir 250 | 251 | # elpa packages 252 | /elpa/ 253 | 254 | # reftex files 255 | *.rel 256 | 257 | # AUCTeX auto folder 258 | /auto/ 259 | 260 | # cask packages 261 | .cask/ 262 | dist/ 263 | 264 | # Flycheck 265 | flycheck_*.el 266 | 267 | # server auth directory 268 | /server/ 269 | 270 | # projectiles files 271 | .projectile 272 | 273 | # directory configuration 274 | .dir-locals.el 275 | 276 | # network security 277 | /network-security.data 278 | 279 | 280 | # Vim rules 281 | # Swap 282 | [._]*.s[a-v][a-z] 283 | !*.svg # comment out if you don't need vector files 284 | [._]*.sw[a-p] 285 | [._]s[a-rt-v][a-z] 286 | [._]ss[a-gi-z] 287 | [._]sw[a-p] 288 | 289 | # Session 290 | Session.vim 291 | Sessionx.vim 292 | 293 | # Temporary 294 | .netrwhist 295 | *~ 296 | # Auto-generated tag files 297 | tags 298 | # Persistent undo 299 | [._]*.un~ 300 | 301 | # JetBrains rules 302 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 303 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 304 | 305 | # User-specific stuff 306 | .idea/**/workspace.xml 307 | .idea/**/tasks.xml 308 | .idea/**/usage.statistics.xml 309 | .idea/**/dictionaries 310 | .idea/**/shelf 311 | 312 | # Generated files 313 | .idea/**/contentModel.xml 314 | 315 | # Sensitive or high-churn files 316 | .idea/**/dataSources/ 317 | .idea/**/dataSources.ids 318 | .idea/**/dataSources.local.xml 319 | .idea/**/sqlDataSources.xml 320 | .idea/**/dynamic.xml 321 | .idea/**/uiDesigner.xml 322 | .idea/**/dbnavigator.xml 323 | 324 | # Gradle 325 | .idea/**/gradle.xml 326 | .idea/**/libraries 327 | 328 | # Gradle and Maven with auto-import 329 | # When using Gradle or Maven with auto-import, you should exclude module files, 330 | # since they will be recreated, and may cause churn. Uncomment if using 331 | # auto-import. 332 | # .idea/artifacts 333 | # .idea/compiler.xml 334 | # .idea/jarRepositories.xml 335 | # .idea/modules.xml 336 | # .idea/*.iml 337 | # .idea/modules 338 | # *.iml 339 | # *.ipr 340 | 341 | # CMake 342 | cmake-build-*/ 343 | 344 | # Mongo Explorer plugin 345 | .idea/**/mongoSettings.xml 346 | 347 | # File-based project format 348 | *.iws 349 | 350 | # IntelliJ 351 | out/ 352 | 353 | # mpeltonen/sbt-idea plugin 354 | .idea_modules/ 355 | 356 | # JIRA plugin 357 | atlassian-ide-plugin.xml 358 | 359 | # Cursive Clojure plugin 360 | .idea/replstate.xml 361 | 362 | # Crashlytics plugin (for Android Studio and IntelliJ) 363 | com_crashlytics_export_strings.xml 364 | crashlytics.properties 365 | crashlytics-build.properties 366 | fabric.properties 367 | 368 | # Editor-based Rest Client 369 | .idea/httpRequests 370 | 371 | # Android studio 3.1+ serialized cache file 372 | .idea/caches/build_file_checksums.ser 373 | 374 | # SublimeText rules 375 | # Cache files for Sublime Text 376 | *.tmlanguage.cache 377 | *.tmPreferences.cache 378 | *.stTheme.cache 379 | 380 | # Workspace files are user-specific 381 | *.sublime-workspace 382 | 383 | # Project files should be checked into the repository, unless a significant 384 | # proportion of contributors will probably not be using Sublime Text 385 | # *.sublime-project 386 | 387 | # SFTP configuration file 388 | sftp-config.json 389 | sftp-config-alt*.json 390 | 391 | # Package control specific files 392 | Package Control.last-run 393 | Package Control.ca-list 394 | Package Control.ca-bundle 395 | Package Control.system-ca-bundle 396 | Package Control.cache/ 397 | Package Control.ca-certs/ 398 | Package Control.merged-ca-bundle 399 | Package Control.user-ca-bundle 400 | oscrypto-ca-bundle.crt 401 | bh_unicode_properties.cache 402 | 403 | # Sublime-github package stores a github token in this file 404 | # https://packagecontrol.io/packages/sublime-github 405 | GitHub.sublime-settings 406 | 407 | # KDevelop4 rules 408 | *.kdev4 409 | .kdev4/ 410 | 411 | # Kate rules 412 | # Swap Files # 413 | .*.kate-swp 414 | .swp.* 415 | 416 | # TextMate rules 417 | *.tmproj 418 | *.tmproject 419 | tmtags 420 | 421 | # VisualStudioCode rules 422 | .vscode/* 423 | !.vscode/settings.json 424 | !.vscode/tasks.json 425 | !.vscode/launch.json 426 | !.vscode/extensions.json 427 | *.code-workspace 428 | 429 | # Local History for Visual Studio Code 430 | .history/ 431 | 432 | # Xcode rules 433 | # Xcode 434 | # 435 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 436 | 437 | ## User settings 438 | xcuserdata/ 439 | 440 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 441 | *.xcscmblueprint 442 | *.xccheckout 443 | 444 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 445 | build/ 446 | DerivedData/ 447 | *.moved-aside 448 | *.pbxuser 449 | !default.pbxuser 450 | *.mode1v3 451 | !default.mode1v3 452 | *.mode2v3 453 | !default.mode2v3 454 | *.perspectivev3 455 | !default.perspectivev3 456 | 457 | ## Gcc Patch 458 | /*.gcno 459 | 460 | # Eclipse rules 461 | .metadata 462 | bin/ 463 | tmp/ 464 | *.tmp 465 | *.bak 466 | *.swp 467 | *~.nib 468 | local.properties 469 | .settings/ 470 | .loadpath 471 | .recommenders 472 | 473 | # External tool builders 474 | .externalToolBuilders/ 475 | 476 | # Locally stored "Eclipse launch configurations" 477 | *.launch 478 | 479 | # PyDev specific (Python IDE for Eclipse) 480 | *.pydevproject 481 | 482 | # CDT-specific (C/C++ Development Tooling) 483 | .cproject 484 | 485 | # CDT- autotools 486 | .autotools 487 | 488 | # Java annotation processor (APT) 489 | .factorypath 490 | 491 | # PDT-specific (PHP Development Tools) 492 | .buildpath 493 | 494 | # sbteclipse plugin 495 | .target 496 | 497 | # Tern plugin 498 | .tern-project 499 | 500 | # TeXlipse plugin 501 | .texlipse 502 | 503 | # STS (Spring Tool Suite) 504 | .springBeans 505 | 506 | # Code Recommenders 507 | .recommenders/ 508 | 509 | # Annotation Processing 510 | .apt_generated/ 511 | .apt_generated_test/ 512 | 513 | # Scala IDE specific (Scala & Java development for Eclipse) 514 | .cache-main 515 | .scala_dependencies 516 | .worksheet 517 | 518 | # Uncomment this line if you wish to ignore the project description file. 519 | # Typically, this file would be tracked if it contains build/dependency configurations: 520 | #.project 521 | 522 | # TortoiseGit rules 523 | # Project-level settings 524 | /.tgitconfig 525 | 526 | # Tags rules 527 | # Ignore tags created by etags, ctags, gtags (GNU global) and cscope 528 | TAGS 529 | .TAGS 530 | !TAGS/ 531 | tags 532 | .tags 533 | !tags/ 534 | gtags.files 535 | GTAGS 536 | GRTAGS 537 | GPATH 538 | GSYMS 539 | cscope.files 540 | cscope.out 541 | cscope.in.out 542 | cscope.po.out 543 | 544 | 545 | # remove moban hash dictionary 546 | .moban.hashes 547 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | line_length=79 3 | known_first_party=lml, pyexcel, openpyxl 4 | known_third_party=nose 5 | indent=' ' 6 | multi_line_output=3 7 | length_sort=1 8 | default_section=FIRSTPARTY 9 | no_lines_before=LOCALFOLDER 10 | sections=FUTURE,STDLIB,FIRSTPARTY,THIRDPARTY,LOCALFOLDER 11 | -------------------------------------------------------------------------------- /.moban.d/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) {{copyright_year}} by {{company}} and its contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms of the software as well 5 | as documentation, with or without modification, are permitted provided 6 | that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * The names of the contributors may not be used to endorse or 17 | promote products derived from this software without specific 18 | prior written permission. 19 | 20 | THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND 21 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 22 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 24 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 25 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 26 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 27 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 28 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 29 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 31 | DAMAGE. -------------------------------------------------------------------------------- /.moban.d/custom_README.rst.jj2: -------------------------------------------------------------------------------- 1 | {%extends 'README.rst.jj2' %} 2 | 3 | {%block documentation_link%} 4 | {%endblock%} 5 | 6 | {%block description%} 7 | **{{name}}** is a tiny wrapper library to read, manipulate and write data in xlsx and xlsm format using `read_only` mode reader, `write_only` mode writer from openpyxl. You are likely to use it with `pyexcel `__. 8 | 9 | Please note: 10 | 11 | 1. `auto_detect_int` flag will not take effect because openpyxl detect integer in python 3 by default. 12 | 2. `skip_hidden_row_and_column` will get a penalty where `read_only` mode cannot be used. 13 | 14 | 15 | {%endblock%} 16 | -------------------------------------------------------------------------------- /.moban.d/custom_setup.py.jj2: -------------------------------------------------------------------------------- 1 | {% extends 'setup.py.jj2' %} 2 | 3 | {%block platform_block%} 4 | {%endblock%} 5 | 6 | {%block compat_block%} 7 | {%endblock%} 8 | 9 | {%block additional_keywords%} 10 | 'xlsx' 11 | {%endblock%} 12 | 13 | {%block additional_classifiers %} 14 | 'Programming Language :: Python :: Implementation :: PyPy' 15 | {%endblock %} 16 | -------------------------------------------------------------------------------- /.moban.d/requirements.txt: -------------------------------------------------------------------------------- 1 | {% for dependency in dependencies: %} 2 | {{dependency}} 3 | {% endfor %} 4 | -------------------------------------------------------------------------------- /.moban.d/tests/base.py: -------------------------------------------------------------------------------- 1 | {% extends 'tests/base.py.jj2' %} 2 | 3 | {%block ods_types%} 4 | {%endblock%} 5 | -------------------------------------------------------------------------------- /.moban.d/tests/requirements.txt: -------------------------------------------------------------------------------- 1 | {% extends 'tests/requirements.txt.jj2' %} 2 | {%block extras %} 3 | pyexcel 4 | pyexcel-xls 5 | moban 6 | black;python_version>="3.6" 7 | isort;python_version>="3.6" 8 | {%endblock%} 9 | -------------------------------------------------------------------------------- /.moban.d/tests/test_formatters.py: -------------------------------------------------------------------------------- 1 | {% extends 'tests/test_formatters.py.jj2' %} 2 | 3 | {%block xlsx_exception%} 4 | {%endblock%} 5 | -------------------------------------------------------------------------------- /.moban.yml: -------------------------------------------------------------------------------- 1 | overrides: "git://github.com/pyexcel/pyexcel-mobans!/mobanfile.yaml" 2 | configuration: 3 | configuration: pyexcel-xlsx.yml 4 | targets: 5 | - Pipfile: Pipfile.jj2 6 | - README.rst: custom_README.rst.jj2 7 | - setup.py: custom_setup.py.jj2 8 | - "docs/source/conf.py": "docs/source/conf.py.jj2" 9 | - MANIFEST.in: MANIFEST.in.jj2 10 | - .gitignore: gitignore.jj2 11 | - "tests/requirements.txt": "tests/requirements.txt" 12 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | 13 | # Build documentation in the docs/ directory with Sphinx 14 | sphinx: 15 | configuration: docs/source/conf.py 16 | 17 | # Optionally build your docs in additional formats such as PDF 18 | formats: 19 | - pdf 20 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Change log 2 | ================================================================================ 3 | 4 | 0.7.0 - 10.05.2025 5 | -------------------------------------------------------------------------------- 6 | 7 | **Updated** 8 | 9 | #. `#56 `_: to add a csv file 10 | as sheet to a xlsx file 11 | 12 | 0.6.1 - 10.11.2024 13 | -------------------------------------------------------------------------------- 14 | 15 | **Updated** 16 | 17 | #. Compatability with openpyxl 3.1.0 and later 18 | 19 | 0.6.0 - 10.10.2020 20 | -------------------------------------------------------------------------------- 21 | 22 | **Updated** 23 | 24 | #. New style xlsx plugins, promoted by pyexcel-io v0.6.2. 25 | 26 | 0.5.8 - 28.12.2019 27 | -------------------------------------------------------------------------------- 28 | 29 | **Updated** 30 | 31 | #. `#34 `_: pin 32 | openpyxl>=2.6.1 33 | 34 | 0.5.7 - 15.02.2019 35 | -------------------------------------------------------------------------------- 36 | 37 | **Added** 38 | 39 | #. `pyexcel-io#66 `_ pin 40 | openpyxl < 2.6.0 41 | 42 | 0.5.6 - 26.03.2018 43 | -------------------------------------------------------------------------------- 44 | 45 | **Added** 46 | 47 | #. `#24 `_, remove deprecated 48 | warning from merged_cell_ranges and get_sheet_by_name 49 | 50 | 0.5.5 - 18.12.2017 51 | -------------------------------------------------------------------------------- 52 | 53 | **Added** 54 | 55 | #. `#22 `_, to detect merged 56 | cell in xlsx - fast tracked patreon request. 57 | 58 | 0.5.4 - 2.11.2017 59 | -------------------------------------------------------------------------------- 60 | 61 | **Updated** 62 | 63 | #. Align the behavior of skip_hidden_row_and_column. Default it to True. 64 | 65 | 0.5.3 - 2.11.2017 66 | -------------------------------------------------------------------------------- 67 | 68 | **Added** 69 | 70 | #. `#20 `_, skip hidden rows 71 | and columns under 'skip_hidden_row_and_column' flag. 72 | 73 | 0.5.2 - 23.10.2017 74 | -------------------------------------------------------------------------------- 75 | 76 | **updated** 77 | 78 | #. pyexcel `pyexcel#105 `_, 79 | remove gease from setup_requires, introduced by 0.5.1. 80 | #. remove python2.6 test support 81 | #. update its dependecy on pyexcel-io to 0.5.3 82 | 83 | 0.5.1 - 20.10.2017 84 | -------------------------------------------------------------------------------- 85 | 86 | **added** 87 | 88 | #. `pyexcel#103 `_, include 89 | LICENSE file in MANIFEST.in, meaning LICENSE file will appear in the released 90 | tar ball. 91 | 92 | 0.5.0 - 30.08.2017 93 | -------------------------------------------------------------------------------- 94 | 95 | **Updated** 96 | 97 | #. put dependency on pyexcel-io 0.5.0, which uses cStringIO instead of StringIO. 98 | Hence, there will be performance boost in handling files in memory. 99 | 100 | **Removed** 101 | 102 | #. `#18 `_, is handled in 103 | pyexcel-io 104 | 105 | 0.4.2 - 25.08.2017 106 | -------------------------------------------------------------------------------- 107 | 108 | **Updated** 109 | 110 | #. `#18 `_, handle unseekable 111 | stream given by http response 112 | 113 | 0.4.1 - 16.07.2017 114 | -------------------------------------------------------------------------------- 115 | 116 | **Removed** 117 | 118 | #. Removed useless code 119 | 120 | 0.4.0 - 19.06.2017 121 | -------------------------------------------------------------------------------- 122 | 123 | **Updated** 124 | 125 | #. `#14 `_, close file handle 126 | #. pyexcel-io plugin interface now updated to use `lml 127 | `_. 128 | 129 | 0.3.0 - 22.12.2016 130 | -------------------------------------------------------------------------------- 131 | 132 | **Updated** 133 | 134 | #. Code refactoring with pyexcel-io v 0.3.0 135 | #. `#13 `_, turn read_only 136 | flag on openpyxl. 137 | 138 | 0.2.3 - 05.11.2016 139 | -------------------------------------------------------------------------------- 140 | 141 | **Updated** 142 | 143 | #. `#12 `_, remove 144 | UserWarning: Using a coordinate with ws.cell is deprecated. Use 145 | ws[coordinate] 146 | 147 | 0.2.2 - 31.08.2016 148 | -------------------------------------------------------------------------------- 149 | 150 | **Added** 151 | 152 | #. support pagination. two pairs: start_row, row_limit and start_column, 153 | column_limit help you deal with large files. 154 | 155 | 0.2.1 - 12.07.2016 156 | -------------------------------------------------------------------------------- 157 | 158 | **Added** 159 | 160 | #. `#8 `__, 161 | `skip_hidden_sheets` is added. By default, hidden sheets are skipped when 162 | reading all sheets. Reading sheet by name or by index are not affected. 163 | 164 | 0.2.0 - 01.06.2016 165 | -------------------------------------------------------------------------------- 166 | 167 | **Added** 168 | 169 | #. 'library=pyexcel-xlsx' was added to inform pyexcel to use it instead of other 170 | libraries, in the situation where there are more than one plugin for a file 171 | type, e.g. xlsm 172 | 173 | **Updated** 174 | 175 | #. support the auto-import feature of pyexcel-io 0.2.0 176 | 177 | 0.1.0 - 17.01.2016 178 | -------------------------------------------------------------------------------- 179 | 180 | **Added** 181 | 182 | #. Passing "streaming=True" to get_data, you will get the two dimensional array 183 | as a generator 184 | #. Passing "data=your_generator" to save_data is acceptable too. 185 | 186 | **Updated** 187 | 188 | #. compatibility with pyexcel-io 0.1.0 189 | -------------------------------------------------------------------------------- /CONTRIBUTORS.rst: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 contributors 4 | ================================================================================ 5 | 6 | In alphabetical order: 7 | 8 | * `Benoit Pierre `_ 9 | * `Chun-Sheng, Li `_ 10 | * `Craig Anderson `_ 11 | * `John Vandenberg `_ 12 | * `Stephen J. Fuhry `_ 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2025 by Onni Software Ltd. and its contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms of the software as well 5 | as documentation, with or without modification, are permitted provided 6 | that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of 'pyexcel-xlsx' nor the names of the contributors 16 | may be used to endorse or promote products derived from this software 17 | without specific prior written permission. 18 | 19 | THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND 20 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 21 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 23 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 24 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 25 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 26 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 28 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 30 | DAMAGE. 31 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | include CHANGELOG.rst 4 | include CONTRIBUTORS.rst 5 | recursive-include tests * 6 | recursive-include docs * 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test 2 | 3 | test: lint 4 | bash test.sh 5 | 6 | install_test: 7 | pip install -r tests/requirements.txt 8 | 9 | lint: 10 | bash lint.sh 11 | 12 | format: 13 | bash format.sh 14 | 15 | git-diff-check: 16 | git diff --exit-code 17 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = 'https://pypi.python.org/simple' 3 | verify_ssl = true 4 | name = 'pypi' 5 | 6 | [requires] 7 | python_version= '3.6' 8 | 9 | [packages] 10 | openpyxl = '>=2.6.1' 11 | pyexcel-io = '>=0.6.2' 12 | 13 | [dev-packages] 14 | nose = "*" 15 | mock = "*" 16 | codecov = "*" 17 | coverage = "*" 18 | flake8 = "*" 19 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================================================================================ 2 | pyexcel-xlsx - Let you focus on data, instead of xlsx format 3 | ================================================================================ 4 | 5 | .. image:: https://raw.githubusercontent.com/pyexcel/pyexcel.github.io/master/images/patreon.png 6 | :target: https://www.patreon.com/chfw 7 | 8 | .. image:: https://raw.githubusercontent.com/pyexcel/pyexcel-mobans/master/images/awesome-badge.svg 9 | :target: https://awesome-python.com/#specific-formats-processing 10 | 11 | .. image:: https://codecov.io/gh/pyexcel/pyexcel-xlsx/branch/master/graph/badge.svg 12 | :target: https://codecov.io/gh/pyexcel/pyexcel-xlsx 13 | 14 | .. image:: https://badge.fury.io/py/pyexcel-xlsx.svg 15 | :target: https://pypi.org/project/pyexcel-xlsx 16 | 17 | .. image:: https://anaconda.org/conda-forge/pyexcel-xlsx/badges/version.svg 18 | :target: https://anaconda.org/conda-forge/pyexcel-xlsx 19 | 20 | 21 | .. image:: https://pepy.tech/badge/pyexcel-xlsx/month 22 | :target: https://pepy.tech/project/pyexcel-xlsx 23 | 24 | .. image:: https://anaconda.org/conda-forge/pyexcel-xlsx/badges/downloads.svg 25 | :target: https://anaconda.org/conda-forge/pyexcel-xlsx 26 | 27 | .. image:: https://img.shields.io/gitter/room/gitterHQ/gitter.svg 28 | :target: https://gitter.im/pyexcel/Lobby 29 | 30 | .. 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 31 | :target: https://moban.readthedocs.io/en/latest/#at-scale-continous-templating-for-open-source-projects 32 | 33 | .. image:: https://img.shields.io/static/v1?label=coding%20style&message=black&color=black&style=flat-square 34 | :target: https://github.com/psf/black 35 | 36 | **pyexcel-xlsx** is a tiny wrapper library to read, manipulate and write data in xlsx and xlsm format using `read_only` mode reader, `write_only` mode writer from openpyxl. You are likely to use it with `pyexcel `__. 37 | 38 | Please note: 39 | 40 | 1. `auto_detect_int` flag will not take effect because openpyxl detect integer in python 3 by default. 41 | 2. `skip_hidden_row_and_column` will get a penalty where `read_only` mode cannot be used. 42 | 43 | 44 | 45 | Support the project 46 | ================================================================================ 47 | 48 | If your company uses pyexcel and its components in a revenue-generating product, 49 | please consider supporting the project on GitHub or 50 | `Patreon `_. Your financial 51 | support will enable me to dedicate more time to coding, improving documentation, 52 | and creating engaging content. 53 | 54 | 55 | Known constraints 56 | ================== 57 | 58 | Fonts, colors and charts are not supported. 59 | 60 | Nor to read password protected xls, xlsx and ods files. 61 | 62 | Installation 63 | ================================================================================ 64 | 65 | 66 | You can install pyexcel-xlsx via pip: 67 | 68 | .. code-block:: bash 69 | 70 | $ pip install pyexcel-xlsx 71 | 72 | 73 | or clone it and install it: 74 | 75 | .. code-block:: bash 76 | 77 | $ git clone https://github.com/pyexcel/pyexcel-xlsx.git 78 | $ cd pyexcel-xlsx 79 | $ python setup.py install 80 | 81 | Usage 82 | ================================================================================ 83 | 84 | As a standalone library 85 | -------------------------------------------------------------------------------- 86 | 87 | .. testcode:: 88 | :hide: 89 | 90 | >>> import os 91 | >>> import sys 92 | >>> from io import BytesIO 93 | >>> from collections import OrderedDict 94 | 95 | 96 | Write to an xlsx file 97 | ******************************************************************************** 98 | 99 | 100 | 101 | Here's the sample code to write a dictionary to an xlsx file: 102 | 103 | .. code-block:: python 104 | 105 | >>> from pyexcel_xlsx import save_data 106 | >>> data = OrderedDict() # from collections import OrderedDict 107 | >>> data.update({"Sheet 1": [[1, 2, 3], [4, 5, 6]]}) 108 | >>> data.update({"Sheet 2": [["row 1", "row 2", "row 3"]]}) 109 | >>> save_data("your_file.xlsx", data) 110 | 111 | 112 | Read from an xlsx file 113 | ******************************************************************************** 114 | 115 | Here's the sample code: 116 | 117 | .. code-block:: python 118 | 119 | >>> from pyexcel_xlsx import get_data 120 | >>> data = get_data("your_file.xlsx") 121 | >>> import json 122 | >>> print(json.dumps(data)) 123 | {"Sheet 1": [[1, 2, 3], [4, 5, 6]], "Sheet 2": [["row 1", "row 2", "row 3"]]} 124 | 125 | 126 | Write an xlsx to memory 127 | ******************************************************************************** 128 | 129 | Here's the sample code to write a dictionary to an xlsx file: 130 | 131 | .. code-block:: python 132 | 133 | >>> from pyexcel_xlsx import save_data 134 | >>> data = OrderedDict() 135 | >>> data.update({"Sheet 1": [[1, 2, 3], [4, 5, 6]]}) 136 | >>> data.update({"Sheet 2": [[7, 8, 9], [10, 11, 12]]}) 137 | >>> io = BytesIO() 138 | >>> save_data(io, data) 139 | >>> # do something with the io 140 | >>> # In reality, you might give it to your http response 141 | >>> # object for downloading 142 | 143 | 144 | 145 | 146 | Read from an xlsx from memory 147 | ******************************************************************************** 148 | 149 | Continue from previous example: 150 | 151 | .. code-block:: python 152 | 153 | >>> # This is just an illustration 154 | >>> # In reality, you might deal with xlsx file upload 155 | >>> # where you will read from requests.FILES['YOUR_XLSX_FILE'] 156 | >>> data = get_data(io) 157 | >>> print(json.dumps(data)) 158 | {"Sheet 1": [[1, 2, 3], [4, 5, 6]], "Sheet 2": [[7, 8, 9], [10, 11, 12]]} 159 | 160 | 161 | Pagination feature 162 | ******************************************************************************** 163 | 164 | 165 | 166 | Let's assume the following file is a huge xlsx file: 167 | 168 | .. code-block:: python 169 | 170 | >>> huge_data = [ 171 | ... [1, 21, 31], 172 | ... [2, 22, 32], 173 | ... [3, 23, 33], 174 | ... [4, 24, 34], 175 | ... [5, 25, 35], 176 | ... [6, 26, 36] 177 | ... ] 178 | >>> sheetx = { 179 | ... "huge": huge_data 180 | ... } 181 | >>> save_data("huge_file.xlsx", sheetx) 182 | 183 | And let's pretend to read partial data: 184 | 185 | .. code-block:: python 186 | 187 | >>> partial_data = get_data("huge_file.xlsx", start_row=2, row_limit=3) 188 | >>> print(json.dumps(partial_data)) 189 | {"huge": [[3, 23, 33], [4, 24, 34], [5, 25, 35]]} 190 | 191 | And you could as well do the same for columns: 192 | 193 | .. code-block:: python 194 | 195 | >>> partial_data = get_data("huge_file.xlsx", start_column=1, column_limit=2) 196 | >>> print(json.dumps(partial_data)) 197 | {"huge": [[21, 31], [22, 32], [23, 33], [24, 34], [25, 35], [26, 36]]} 198 | 199 | Obvious, you could do both at the same time: 200 | 201 | .. code-block:: python 202 | 203 | >>> partial_data = get_data("huge_file.xlsx", 204 | ... start_row=2, row_limit=3, 205 | ... start_column=1, column_limit=2) 206 | >>> print(json.dumps(partial_data)) 207 | {"huge": [[23, 33], [24, 34], [25, 35]]} 208 | 209 | .. testcode:: 210 | :hide: 211 | 212 | >>> os.unlink("huge_file.xlsx") 213 | 214 | 215 | As a pyexcel plugin 216 | -------------------------------------------------------------------------------- 217 | 218 | No longer, explicit import is needed since pyexcel version 0.2.2. Instead, 219 | this library is auto-loaded. So if you want to read data in xlsx format, 220 | installing it is enough. 221 | 222 | 223 | Reading from an xlsx file 224 | ******************************************************************************** 225 | 226 | Here is the sample code: 227 | 228 | .. code-block:: python 229 | 230 | >>> import pyexcel as pe 231 | >>> sheet = pe.get_book(file_name="your_file.xlsx") 232 | >>> sheet 233 | Sheet 1: 234 | +---+---+---+ 235 | | 1 | 2 | 3 | 236 | +---+---+---+ 237 | | 4 | 5 | 6 | 238 | +---+---+---+ 239 | Sheet 2: 240 | +-------+-------+-------+ 241 | | row 1 | row 2 | row 3 | 242 | +-------+-------+-------+ 243 | 244 | 245 | Writing to an xlsx file 246 | ******************************************************************************** 247 | 248 | Here is the sample code: 249 | 250 | .. code-block:: python 251 | 252 | >>> sheet.save_as("another_file.xlsx") 253 | 254 | 255 | Reading from a IO instance 256 | ******************************************************************************** 257 | 258 | You got to wrap the binary content with stream to get xlsx working: 259 | 260 | .. code-block:: python 261 | 262 | >>> # This is just an illustration 263 | >>> # In reality, you might deal with xlsx file upload 264 | >>> # where you will read from requests.FILES['YOUR_XLSX_FILE'] 265 | >>> xlsxfile = "another_file.xlsx" 266 | >>> with open(xlsxfile, "rb") as f: 267 | ... content = f.read() 268 | ... r = pe.get_book(file_type="xlsx", file_content=content) 269 | ... print(r) 270 | ... 271 | Sheet 1: 272 | +---+---+---+ 273 | | 1 | 2 | 3 | 274 | +---+---+---+ 275 | | 4 | 5 | 6 | 276 | +---+---+---+ 277 | Sheet 2: 278 | +-------+-------+-------+ 279 | | row 1 | row 2 | row 3 | 280 | +-------+-------+-------+ 281 | 282 | 283 | Writing to a BytesIO instance 284 | ******************************************************************************** 285 | 286 | You need to pass a BytesIO instance to Writer: 287 | 288 | .. code-block:: python 289 | 290 | >>> data = [ 291 | ... [1, 2, 3], 292 | ... [4, 5, 6] 293 | ... ] 294 | >>> io = BytesIO() 295 | >>> sheet = pe.Sheet(data) 296 | >>> io = sheet.save_to_memory("xlsx", io) 297 | >>> # then do something with io 298 | >>> # In reality, you might give it to your http response 299 | >>> # object for downloading 300 | 301 | 302 | License 303 | ================================================================================ 304 | 305 | New BSD License 306 | 307 | Developer guide 308 | ================== 309 | 310 | Development steps for code changes 311 | 312 | #. git clone https://github.com/pyexcel/pyexcel-xlsx.git 313 | #. cd pyexcel-xlsx 314 | 315 | Upgrade your setup tools and pip. They are needed for development and testing only: 316 | 317 | #. pip install --upgrade setuptools pip 318 | 319 | Then install relevant development requirements: 320 | 321 | #. pip install -r rnd_requirements.txt # if such a file exists 322 | #. pip install -r requirements.txt 323 | #. pip install -r tests/requirements.txt 324 | 325 | Once you have finished your changes, please provide test case(s), relevant documentation 326 | and update changelog.yml 327 | 328 | .. note:: 329 | 330 | As to rnd_requirements.txt, usually, it is created when a dependent 331 | library is not released. Once the dependency is installed 332 | (will be released), the future 333 | version of the dependency in the requirements.txt will be valid. 334 | 335 | 336 | How to test your contribution 337 | -------------------------------------------------------------------------------- 338 | 339 | Although `nose` and `doctest` are both used in code testing, it is advisable 340 | that unit tests are put in tests. `doctest` is incorporated only to make sure 341 | the code examples in documentation remain valid across different development 342 | releases. 343 | 344 | On Linux/Unix systems, please launch your tests like this:: 345 | 346 | $ make 347 | 348 | On Windows, please issue this command:: 349 | 350 | > test.bat 351 | 352 | 353 | Before you commit 354 | ------------------------------ 355 | 356 | Please run:: 357 | 358 | $ make format 359 | 360 | so as to beautify your code otherwise your build may fail your unit test. 361 | 362 | 363 | 364 | .. testcode:: 365 | :hide: 366 | 367 | >>> import os 368 | >>> os.unlink("your_file.xlsx") 369 | >>> os.unlink("another_file.xlsx") 370 | -------------------------------------------------------------------------------- /changelog.yml: -------------------------------------------------------------------------------- 1 | name: pyexcel-xlsx 2 | organisation: pyexcel 3 | releases: 4 | - changes: 5 | - action: Updated 6 | details: 7 | - '`#56`: to add a csv file as sheet to a xlsx file' 8 | date: 10.05.2025 9 | version: 0.7.0 10 | - changes: 11 | - action: Updated 12 | details: 13 | - 'Compatability with openpyxl 3.1.0 and later' 14 | date: 10.11.2024 15 | version: 0.6.1 16 | - changes: 17 | - action: Updated 18 | details: 19 | - 'New style xlsx plugins, promoted by pyexcel-io v0.6.2.' 20 | date: 10.10.2020 21 | version: 0.6.0 22 | - changes: 23 | - action: Updated 24 | details: 25 | - '`#34`: pin openpyxl>=2.6.1' 26 | date: 28.12.2019 27 | version: 0.5.8 28 | - changes: 29 | - action: Added 30 | details: 31 | - '`pyexcel-io#66` pin openpyxl < 2.6.0' 32 | date: 15.02.2019 33 | version: 0.5.7 34 | - changes: 35 | - action: Added 36 | details: 37 | - '`#24`, remove deprecated warning from merged_cell_ranges and get_sheet_by_name' 38 | date: 26.03.2018 39 | version: 0.5.6 40 | - changes: 41 | - action: Added 42 | details: 43 | - '`#22`, to detect merged cell in xlsx - fast tracked patreon request.' 44 | date: 18.12.2017 45 | version: 0.5.5 46 | - changes: 47 | - action: Updated 48 | details: 49 | - Align the behavior of skip_hidden_row_and_column. Default it to True. 50 | date: 2.11.2017 51 | version: 0.5.4 52 | - changes: 53 | - action: Added 54 | details: 55 | - '`#20`, skip hidden rows and columns under ''skip_hidden_row_and_column'' flag.' 56 | date: 2.11.2017 57 | version: 0.5.3 58 | - changes: 59 | - action: updated 60 | details: 61 | - pyexcel `pyexcel#105`, remove gease from setup_requires, introduced by 0.5.1. 62 | - remove python2.6 test support 63 | - update its dependecy on pyexcel-io to 0.5.3 64 | date: 23.10.2017 65 | version: 0.5.2 66 | - changes: 67 | - action: added 68 | details: 69 | - '`pyexcel#103`, include LICENSE file in MANIFEST.in, meaning LICENSE file will 70 | appear in the released tar ball.' 71 | date: 20.10.2017 72 | version: 0.5.1 73 | - changes: 74 | - action: Updated 75 | details: 76 | - put dependency on pyexcel-io 0.5.0, which uses cStringIO instead of StringIO. Hence, 77 | there will be performance boost in handling files in memory. 78 | - action: Removed 79 | details: 80 | - '`#18`, is handled in pyexcel-io' 81 | date: 30.08.2017 82 | version: 0.5.0 83 | - changes: 84 | - action: Updated 85 | details: 86 | - '`#18`, handle unseekable stream given by http response' 87 | date: 25.08.2017 88 | version: 0.4.2 89 | - changes: 90 | - action: Removed 91 | details: 92 | - Removed useless code 93 | date: 16.07.2017 94 | version: 0.4.1 95 | - changes: 96 | - action: Updated 97 | details: 98 | - '`#14`, close file handle' 99 | - pyexcel-io plugin interface now updated to use `lml `_. 100 | date: 19.06.2017 101 | version: 0.4.0 102 | - changes: 103 | - action: Updated 104 | details: 105 | - Code refactoring with pyexcel-io v 0.3.0 106 | - '`#13`, turn read_only flag on openpyxl.' 107 | date: 22.12.2016 108 | version: 0.3.0 109 | - changes: 110 | - action: Updated 111 | details: 112 | - '`#12`, remove UserWarning: Using a coordinate with ws.cell is deprecated. Use 113 | ws[coordinate]' 114 | date: 05.11.2016 115 | version: 0.2.3 116 | - changes: 117 | - action: Added 118 | details: 119 | - 'support pagination. two pairs: start_row, row_limit and start_column, column_limit help 120 | you deal with large files.' 121 | date: 31.08.2016 122 | version: 0.2.2 123 | - changes: 124 | - action: Added 125 | details: 126 | - '`#8`_, `skip_hidden_sheets` is added. By default, hidden sheets are skipped 127 | when reading all sheets. Reading sheet by name or by index are not affected.' 128 | date: 12.07.2016 129 | version: 0.2.1 130 | - changes: 131 | - action: Added 132 | details: 133 | - '''library=pyexcel-xlsx'' was added to inform pyexcel to use it instead of other 134 | libraries, in the situation where there are more than one plugin for a file 135 | type, e.g. xlsm' 136 | - action: Updated 137 | details: 138 | - support the auto-import feature of pyexcel-io 0.2.0 139 | date: 01.06.2016 140 | version: 0.2.0 141 | - changes: 142 | - action: Added 143 | details: 144 | - Passing "streaming=True" to get_data, you will get the two dimensional array 145 | as a generator 146 | - Passing "data=your_generator" to save_data is acceptable too. 147 | - action: Updated 148 | details: 149 | - compatibility with pyexcel-io 0.1.0 150 | date: 17.01.2016 151 | version: 0.1.0 152 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyexcel-xlsx.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyexcel-xlsx.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pyexcel-xlsx" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyexcel-xlsx" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pyexcel-xlsx.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pyexcel-xlsx.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | DESCRIPTION = ( 3 | 'A wrapper library to read, manipulate and write data in xlsx and xlsm ' + 4 | 'format' + 5 | '' 6 | ) 7 | # Configuration file for the Sphinx documentation builder. 8 | # 9 | # This file only contains a selection of the most common options. For a full 10 | # list see the documentation: 11 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 12 | 13 | # -- Path setup -------------------------------------------------------------- 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | # import os 20 | # import sys 21 | # sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- Project information ----------------------------------------------------- 24 | 25 | project = 'pyexcel-xlsx' 26 | copyright = '2015-2025 Onni Software Ltd.' 27 | author = 'C.W.' 28 | # The short X.Y version 29 | version = '0.7.0' 30 | # The full version, including alpha/beta/rc tags 31 | release = '0.6.1' 32 | 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # Add any Sphinx extension module names here, as strings. They can be 36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 37 | # ones. 38 | extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode',] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # The language for content autogenerated by Sphinx. Refer to documentation 44 | # for a list of supported languages. 45 | # 46 | # This is also used if you do content translation via gettext catalogs. 47 | # Usually you set "language" from the command line for these cases. 48 | language = 'en' 49 | 50 | # List of patterns, relative to source directory, that match files and 51 | # directories to ignore when looking for source files. 52 | # This pattern also affects html_static_path and html_extra_path. 53 | exclude_patterns = [] 54 | 55 | 56 | # -- Options for HTML output ------------------------------------------------- 57 | 58 | # The theme to use for HTML and HTML Help pages. See the documentation for 59 | # a list of builtin themes. 60 | # 61 | html_theme = 'sphinx_rtd_theme' 62 | 63 | # Add any paths that contain custom static files (such as style sheets) here, 64 | # relative to this directory. They are copied after the builtin static files, 65 | # so a file named "default.css" will overwrite the builtin "default.css". 66 | html_static_path = ['_static'] 67 | 68 | # -- Extension configuration ------------------------------------------------- 69 | # -- Options for intersphinx extension --------------------------------------- 70 | 71 | # Example configuration for intersphinx: refer to the Python standard library. 72 | intersphinx_mapping = {'python': ('https://docs.python.org/3', 73 | 'python-inv.txt')} 74 | # TODO: html_theme not configurable upstream 75 | html_theme = 'default' 76 | 77 | # TODO: DESCRIPTION not configurable upstream 78 | texinfo_documents = [ 79 | ('index', 'pyexcel-xlsx', 80 | 'pyexcel-xlsx Documentation', 81 | 'Onni Software Ltd.', 'pyexcel-xlsx', 82 | DESCRIPTION, 83 | 'Miscellaneous'), 84 | ] 85 | intersphinx_mapping.update({ 86 | 'pyexcel': ('http://pyexcel.readthedocs.io/en/latest/', None), 87 | }) 88 | master_doc = "index" 89 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. pyexcel-xlsx documentation master file, created by 2 | sphinx-quickstart on Fri Oct 9 23:23:01 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. include :: ../../README.rst 7 | 8 | Indices and tables 9 | ================== 10 | 11 | * :ref:`genindex` 12 | * :ref:`modindex` 13 | * :ref:`search` 14 | 15 | -------------------------------------------------------------------------------- /format.sh: -------------------------------------------------------------------------------- 1 | isort $(find pyexcel_xlsx -name "*.py"|xargs echo) $(find tests -name "*.py"|xargs echo) 2 | black -l 79 pyexcel_xlsx 3 | black -l 79 tests 4 | -------------------------------------------------------------------------------- /lint.sh: -------------------------------------------------------------------------------- 1 | pip install flake8 2 | flake8 --exclude=.moban.d,docs,setup.py --builtins=unicode,xrange,long . && python setup.py checkdocs -------------------------------------------------------------------------------- /pyexcel-xlsx.yml: -------------------------------------------------------------------------------- 1 | overrides: "pyexcel.yaml" 2 | name: "pyexcel-xlsx" 3 | project: "pyexcel-xlsx" 4 | nick_name: xlsx 5 | version: 0.7.0 6 | current_version: 0.7.0 7 | release: 0.6.1 8 | file_type: xlsx 9 | gitignore_language: Python 10 | is_on_conda: true 11 | dependencies: 12 | - openpyxl>=2.6.1 13 | - pyexcel-io>=0.6.2 14 | test_dependencies: 15 | - xlrd==1.2.0 16 | - pyexcel-xls 17 | - pyexcel 18 | description: A wrapper library to read, manipulate and write data in xlsx and xlsm format 19 | moban_command: false 20 | python_requires: ">=3.6" 21 | min_python_version: "3.6" 22 | -------------------------------------------------------------------------------- /pyexcel_xlsx/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyexcel_xlsx 3 | ~~~~~~~~~~~~~~~~~~~ 4 | 5 | The lower level xlsx file format handler using openpyxl 6 | 7 | :copyright: (c) 2015-2019 by Onni Software Ltd & its contributors 8 | :license: New BSD License 9 | """ 10 | 11 | from pyexcel_io.io import get_data as read_data 12 | from pyexcel_io.io import isstream 13 | from pyexcel_io.io import save_data as write_data 14 | from pyexcel_io.plugins import IOPluginInfoChainV2 15 | 16 | __FILE_TYPE__ = "xlsx" 17 | 18 | IOPluginInfoChainV2(__name__).add_a_reader( 19 | relative_plugin_class_path="xlsxr.XLSXBook", 20 | locations=["file", "memory"], 21 | file_types=[__FILE_TYPE__, "xlsm"], 22 | stream_type="binary", 23 | ).add_a_reader( 24 | relative_plugin_class_path="xlsxr.XLSXBookInContent", 25 | locations=["content"], 26 | file_types=[__FILE_TYPE__, "xlsm"], 27 | stream_type="binary", 28 | ).add_a_writer( 29 | relative_plugin_class_path="xlsxw.XLSXWriter", 30 | locations=["file", "memory"], 31 | file_types=[__FILE_TYPE__, "xlsm"], 32 | stream_type="binary", 33 | ) 34 | 35 | 36 | def save_data(afile, data, file_type=None, **keywords): 37 | """standalone module function for writing module supported file type""" 38 | if isstream(afile) and file_type is None: 39 | file_type = __FILE_TYPE__ 40 | write_data(afile, data, file_type=file_type, **keywords) 41 | 42 | 43 | def get_data(afile, file_type=None, **keywords): 44 | """standalone module function for reading module supported file type""" 45 | if isstream(afile) and file_type is None: 46 | file_type = __FILE_TYPE__ 47 | return read_data(afile, file_type=file_type, **keywords) 48 | -------------------------------------------------------------------------------- /pyexcel_xlsx/book.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyexcel_xlsx.xlsxr 3 | ~~~~~~~~~~~~~~~~~~~ 4 | 5 | Read xlsx file format using openpyxl 6 | 7 | :copyright: (c) 2015-2025 by Onni Software Ltd & its contributors 8 | :license: New BSD License 9 | """ 10 | 11 | import openpyxl 12 | 13 | 14 | class Sheet(object): 15 | def __init__(self, xlsx_sheet): 16 | self.xlsx_sheet = xlsx_sheet 17 | 18 | def __getitem__(self, cell): 19 | return self.xlsx_sheet[cell] 20 | 21 | def __setitem__(self, cell, value): 22 | self.xlsx_sheet[cell] = value 23 | 24 | def append(self, data): 25 | self.xlsx_sheet.append(data) 26 | 27 | 28 | class Book(object): 29 | def __init__(self, file_name): 30 | self.xlsx_book = openpyxl.load_workbook( 31 | filename=file_name, 32 | ) 33 | 34 | def save(self, file_name): 35 | self.xlsx_book.save(file_name) 36 | 37 | def close(self): 38 | self.xlsx_book.close() 39 | 40 | def __getitem__(self, key): 41 | if key not in self.xlsx_book.sheetnames: 42 | self.xlsx_book.create_sheet(key) 43 | return Sheet(self.xlsx_book[key]) 44 | 45 | def __delitem__(self, other): 46 | self.xlsx_book.remove(self.xlsx_book[other]) 47 | -------------------------------------------------------------------------------- /pyexcel_xlsx/xlsxr.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyexcel_xlsx.xlsxr 3 | ~~~~~~~~~~~~~~~~~~~ 4 | 5 | Read xlsx file format using openpyxl 6 | 7 | :copyright: (c) 2015-2020 by Onni Software Ltd & its contributors 8 | :license: New BSD License 9 | """ 10 | 11 | from io import BytesIO 12 | 13 | import openpyxl 14 | from pyexcel_io.plugin_api import ISheet, IReader, NamedContent 15 | 16 | 17 | class FastSheet(ISheet): 18 | """ 19 | Iterate through rows 20 | """ 21 | 22 | def __init__(self, sheet, **_): 23 | self.xlsx_sheet = sheet 24 | 25 | def row_iterator(self): 26 | """ 27 | openpyxl row iterator 28 | 29 | http://openpyxl.readthedocs.io/en/default/optimized.html 30 | """ 31 | for row in self.xlsx_sheet.rows: 32 | yield row 33 | 34 | def column_iterator(self, row): 35 | """ 36 | a generator for the values in a row 37 | """ 38 | for cell in row: 39 | yield cell.value 40 | 41 | 42 | class MergedCell(object): 43 | def __init__(self, cell_ranges): 44 | self.__cl, self.__rl, self.__ch, self.__rh = cell_ranges.bounds 45 | self.value = None 46 | 47 | def register_cells(self, registry): 48 | for rowx in range(self.__rl, self.__rh + 1): 49 | for colx in range(self.__cl, self.__ch + 1): 50 | key = "%s-%s" % (rowx, colx) 51 | registry[key] = self 52 | 53 | def bottom_row(self): 54 | return self.__rh 55 | 56 | def right_column(self): 57 | return self.__ch 58 | 59 | 60 | class SlowSheet(FastSheet): 61 | """ 62 | This sheet will be slower because it does not use readonly sheet 63 | """ 64 | 65 | def __init__(self, sheet, **keywords): 66 | self.xlsx_sheet = sheet 67 | self._keywords = keywords 68 | self.__merged_cells = {} 69 | self.max_row = 0 70 | self.max_column = 0 71 | self.__sheet_max_row = sheet.max_row 72 | self.__sheet_max_column = sheet.max_column 73 | for ranges in list(sheet.merged_cells.ranges)[:]: 74 | merged_cells = MergedCell(ranges) 75 | merged_cells.register_cells(self.__merged_cells) 76 | if self.max_row < merged_cells.bottom_row(): 77 | self.max_row = merged_cells.bottom_row() 78 | if self.max_column < merged_cells.right_column(): 79 | self.max_column = merged_cells.right_column() 80 | 81 | def row_iterator(self): 82 | """ 83 | skip hidden rows 84 | """ 85 | for row_index, row in enumerate(self.xlsx_sheet.rows, 1): 86 | if self.xlsx_sheet.row_dimensions[row_index].hidden is False: 87 | yield (row, row_index) 88 | if self.max_row > self.__sheet_max_row: 89 | for i in range(self.__sheet_max_row, self.max_row): 90 | data = [None] * self.__sheet_max_column 91 | yield (data, i + 1) 92 | 93 | def column_iterator(self, row_struct): 94 | """ 95 | skip hidden columns 96 | """ 97 | row, row_index = row_struct 98 | for column_index, cell in enumerate(row, 1): 99 | letter = openpyxl.utils.get_column_letter(column_index) 100 | if self.xlsx_sheet.column_dimensions[letter].hidden is False: 101 | if cell: 102 | value = cell.value 103 | else: 104 | value = "" 105 | if value is None: 106 | value = "" 107 | value = self._merged_cells(row_index, column_index, value) 108 | yield value 109 | if self.max_column > self.__sheet_max_column: 110 | for i in range(self.__sheet_max_column, self.max_column): 111 | value = self._merged_cells(row_index, i + 1, "") 112 | yield value 113 | 114 | def _merged_cells(self, row, column, value): 115 | ret = value 116 | if self.__merged_cells: 117 | merged_cell = self.__merged_cells.get("%s-%s" % (row, column)) 118 | if merged_cell: 119 | if merged_cell.value: 120 | ret = merged_cell.value 121 | else: 122 | merged_cell.value = value 123 | return ret 124 | 125 | 126 | class XLSXBook(IReader): 127 | """ 128 | Open xlsx as read only mode 129 | """ 130 | 131 | def __init__( 132 | self, 133 | file_alike_object, 134 | file_type, 135 | skip_hidden_sheets=True, 136 | detect_merged_cells=False, 137 | skip_hidden_row_and_column=True, 138 | **keywords 139 | ): 140 | self.skip_hidden_sheets = skip_hidden_sheets 141 | self.skip_hidden_row_and_column = skip_hidden_row_and_column 142 | self.detect_merged_cells = detect_merged_cells 143 | self.keywords = keywords 144 | self._load_the_excel_file(file_alike_object) 145 | 146 | def read_sheet(self, sheet_index): 147 | native_sheet = self.content_array[sheet_index].payload 148 | if self.skip_hidden_row_and_column or self.detect_merged_cells: 149 | sheet = SlowSheet(native_sheet, **self.keywords) 150 | else: 151 | sheet = FastSheet(native_sheet, **self.keywords) 152 | return sheet 153 | 154 | def close(self): 155 | self.xlsx_book.close() 156 | self.xlsx_book = None 157 | 158 | def _load_the_excel_file(self, file_alike_object): 159 | read_only_flag = True 160 | if self.skip_hidden_row_and_column: 161 | read_only_flag = False 162 | data_only_flag = True 163 | if self.detect_merged_cells: 164 | data_only_flag = False 165 | self.xlsx_book = openpyxl.load_workbook( 166 | filename=file_alike_object, 167 | data_only=data_only_flag, 168 | read_only=read_only_flag, 169 | ) 170 | self.content_array = [] 171 | for sheet_name, sheet in zip( 172 | self.xlsx_book.sheetnames, self.xlsx_book 173 | ): 174 | if self.skip_hidden_sheets and sheet.sheet_state == "hidden": 175 | continue 176 | self.content_array.append(NamedContent(sheet_name, sheet)) 177 | 178 | 179 | class XLSXBookInContent(XLSXBook): 180 | """ 181 | Open xlsx as read only mode 182 | """ 183 | 184 | def __init__(self, file_content, file_type, **keywords): 185 | io = BytesIO(file_content) 186 | super().__init__(io, file_type, **keywords) 187 | -------------------------------------------------------------------------------- /pyexcel_xlsx/xlsxw.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyexcel_xlsx.xlsxw 3 | ~~~~~~~~~~~~~~~~~~~ 4 | 5 | Write xlsx file format using openpyxl 6 | 7 | :copyright: (c) 2015-2020 by Onni Software Ltd & its contributors 8 | :license: New BSD License 9 | """ 10 | 11 | import openpyxl 12 | from pyexcel_io import constants 13 | from pyexcel_io.plugin_api import IWriter, ISheetWriter 14 | 15 | 16 | class XLSXSheetWriter(ISheetWriter): 17 | """ 18 | Write data into xlsx sheet 19 | """ 20 | 21 | def __init__(self, xlsx_sheet, sheet_name=constants.DEFAULT_SHEET_NAME): 22 | self._xlsx_sheet = xlsx_sheet 23 | self._xlsx_sheet.title = sheet_name 24 | 25 | def write_row(self, array): 26 | """ 27 | write a row into the file 28 | """ 29 | self._xlsx_sheet.append(array) 30 | 31 | def close(self): 32 | pass 33 | 34 | 35 | class XLSXWriter(IWriter): 36 | """ 37 | Write data in write only mode 38 | """ 39 | 40 | def __init__(self, file_alike_object, _, **keywords): 41 | self._file_alike_object = file_alike_object 42 | self._native_book = openpyxl.Workbook(write_only=True) 43 | 44 | def create_sheet(self, name): 45 | return XLSXSheetWriter(self._native_book.create_sheet(), name) 46 | 47 | def close(self): 48 | """ 49 | This call actually save the file 50 | """ 51 | self._native_book.save(filename=self._file_alike_object) 52 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | openpyxl>=2.6.1 2 | pyexcel-io>=0.6.2 3 | -------------------------------------------------------------------------------- /rnd_requirements.txt: -------------------------------------------------------------------------------- 1 | https://github.com/pyexcel/pyexcel-io/archive/dev.zip 2 | https://github.com/pyexcel/pyexcel-xls/archive/dev.zip 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.rst 3 | [bdist_wheel] 4 | universal = 1 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Template by pypi-mobans 5 | """ 6 | 7 | import os 8 | import sys 9 | import codecs 10 | import locale 11 | import platform 12 | from shutil import rmtree 13 | 14 | from setuptools import Command, setup, find_packages 15 | 16 | 17 | # Work around mbcs bug in distutils. 18 | # http://bugs.python.org/issue10945 19 | # This work around is only if a project supports Python < 3.4 20 | 21 | # Work around for locale not being set 22 | try: 23 | lc = locale.getlocale() 24 | pf = platform.system() 25 | if pf != "Windows" and lc == (None, None): 26 | locale.setlocale(locale.LC_ALL, "C.UTF-8") 27 | except (ValueError, UnicodeError, locale.Error): 28 | locale.setlocale(locale.LC_ALL, "en_US.UTF-8") 29 | 30 | NAME = "pyexcel-xlsx" 31 | AUTHOR = "C.W." 32 | VERSION = "0.7.0" 33 | EMAIL = "info@pyexcel.org" 34 | LICENSE = "New BSD" 35 | DESCRIPTION = ( 36 | "A wrapper library to read, manipulate and write data in xlsx and xlsm" + 37 | "format" 38 | ) 39 | URL = "https://github.com/pyexcel/pyexcel-xlsx" 40 | DOWNLOAD_URL = "%s/archive/0.6.1.tar.gz" % URL 41 | FILES = ["README.rst", "CHANGELOG.rst"] 42 | KEYWORDS = [ 43 | "python", 44 | 'xlsx' 45 | ] 46 | 47 | CLASSIFIERS = [ 48 | "Topic :: Software Development :: Libraries", 49 | "Programming Language :: Python", 50 | "Intended Audience :: Developers", 51 | 52 | "Programming Language :: Python :: 3 :: Only", 53 | 54 | 55 | 56 | "Programming Language :: Python :: 3.6", 57 | "Programming Language :: Python :: 3.7", 58 | "Programming Language :: Python :: 3.8", 59 | 60 | 'Programming Language :: Python :: Implementation :: PyPy' 61 | ] 62 | 63 | PYTHON_REQUIRES = ">=3.6" 64 | 65 | INSTALL_REQUIRES = [ 66 | "openpyxl>=2.6.1", 67 | "pyexcel-io>=0.6.2", 68 | ] 69 | SETUP_COMMANDS = {} 70 | 71 | PACKAGES = find_packages(exclude=["ez_setup", "examples", "tests", "tests.*"]) 72 | EXTRAS_REQUIRE = { 73 | } 74 | # You do not need to read beyond this line 75 | PUBLISH_COMMAND = "{0} setup.py sdist bdist_wheel upload -r pypi".format(sys.executable) 76 | HERE = os.path.abspath(os.path.dirname(__file__)) 77 | 78 | GS_COMMAND = ("gease pyexcel-xlsx v0.6.1 " + 79 | "Find 0.6.1 in changelog for more details") 80 | NO_GS_MESSAGE = ("Automatic github release is disabled. " + 81 | "Please install gease to enable it.") 82 | UPLOAD_FAILED_MSG = ( 83 | 'Upload failed. please run "%s" yourself.' % PUBLISH_COMMAND) 84 | 85 | 86 | class PublishCommand(Command): 87 | """Support setup.py upload.""" 88 | 89 | description = "Build and publish the package on github and pypi" 90 | user_options = [] 91 | 92 | @staticmethod 93 | def status(s): 94 | """Prints things in bold.""" 95 | print("\033[1m{0}\033[0m".format(s)) 96 | 97 | def initialize_options(self): 98 | pass 99 | 100 | def finalize_options(self): 101 | pass 102 | 103 | def run(self): 104 | try: 105 | self.status("Removing previous builds...") 106 | rmtree(os.path.join(HERE, "dist")) 107 | rmtree(os.path.join(HERE, "build")) 108 | rmtree(os.path.join(HERE, "pyexcel_xlsx.egg-info")) 109 | except OSError: 110 | pass 111 | 112 | self.status("Building Source and Wheel (universal) distribution...") 113 | run_status = True 114 | if has_gease(): 115 | run_status = os.system(GS_COMMAND) == 0 116 | else: 117 | self.status(NO_GS_MESSAGE) 118 | if run_status: 119 | if os.system(PUBLISH_COMMAND) != 0: 120 | self.status(UPLOAD_FAILED_MSG) 121 | 122 | sys.exit() 123 | 124 | 125 | SETUP_COMMANDS.update({ 126 | "publish": PublishCommand 127 | }) 128 | 129 | def has_gease(): 130 | """ 131 | test if github release command is installed 132 | 133 | visit http://github.com/moremoban/gease for more info 134 | """ 135 | try: 136 | import gease # noqa 137 | return True 138 | except ImportError: 139 | return False 140 | 141 | 142 | def read_files(*files): 143 | """Read files into setup""" 144 | text = "" 145 | for single_file in files: 146 | content = read(single_file) 147 | text = text + content + "\n" 148 | return text 149 | 150 | 151 | def read(afile): 152 | """Read a file into setup""" 153 | the_relative_file = os.path.join(HERE, afile) 154 | with codecs.open(the_relative_file, "r", "utf-8") as opened_file: 155 | content = filter_out_test_code(opened_file) 156 | content = "".join(list(content)) 157 | return content 158 | 159 | 160 | def filter_out_test_code(file_handle): 161 | found_test_code = False 162 | for line in file_handle.readlines(): 163 | if line.startswith(".. testcode:"): 164 | found_test_code = True 165 | continue 166 | if found_test_code is True: 167 | if line.startswith(" "): 168 | continue 169 | else: 170 | empty_line = line.strip() 171 | if len(empty_line) == 0: 172 | continue 173 | else: 174 | found_test_code = False 175 | yield line 176 | else: 177 | for keyword in ["|version|", "|today|"]: 178 | if keyword in line: 179 | break 180 | else: 181 | yield line 182 | 183 | 184 | if __name__ == "__main__": 185 | setup( 186 | test_suite="tests", 187 | name=NAME, 188 | author=AUTHOR, 189 | version=VERSION, 190 | author_email=EMAIL, 191 | description=DESCRIPTION, 192 | url=URL, 193 | download_url=DOWNLOAD_URL, 194 | long_description=read_files(*FILES), 195 | license=LICENSE, 196 | keywords=KEYWORDS, 197 | python_requires=PYTHON_REQUIRES, 198 | extras_require=EXTRAS_REQUIRE, 199 | tests_require=["nose"], 200 | install_requires=INSTALL_REQUIRES, 201 | packages=PACKAGES, 202 | include_package_data=True, 203 | zip_safe=False, 204 | classifiers=CLASSIFIERS, 205 | cmdclass=SETUP_COMMANDS 206 | ) 207 | -------------------------------------------------------------------------------- /test.bat: -------------------------------------------------------------------------------- 1 | pip freeze 2 | nosetests --with-coverage --cover-package pyexcel_xlsx --cover-package tests tests --with-doctest --doctest-extension=.rst README.rst docs/source pyexcel_xlsx 3 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | pip freeze 3 | nosetests --with-coverage --cover-package pyexcel_xlsx --cover-package tests tests --with-doctest --doctest-extension=.rst README.rst docs/source pyexcel_xlsx 4 | -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | import os # noqa 2 | import datetime # noqa 3 | 4 | import pyexcel 5 | 6 | from nose.tools import eq_, raises # noqa 7 | 8 | 9 | def create_sample_file1(file): 10 | data = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", 1.1, 1] 11 | table = [] 12 | table.append(data[:4]) 13 | table.append(data[4:8]) 14 | table.append(data[8:12]) 15 | pyexcel.save_as(array=table, dest_file_name=file) 16 | 17 | 18 | class PyexcelHatWriterBase: 19 | """ 20 | Abstract functional test for hat writers 21 | """ 22 | 23 | content = { 24 | "X": [1, 2, 3, 4, 5], 25 | "Y": [6, 7, 8, 9, 10], 26 | "Z": [11, 12, 13, 14, 15], 27 | } 28 | 29 | def test_series_table(self): 30 | pyexcel.save_as(adict=self.content, dest_file_name=self.testfile) 31 | r = pyexcel.get_sheet(file_name=self.testfile, name_columns_by_row=0) 32 | eq_(r.dict, self.content) 33 | 34 | 35 | class PyexcelWriterBase: 36 | """ 37 | Abstract functional test for writers 38 | 39 | testfile and testfile2 have to be initialized before 40 | it is used for testing 41 | """ 42 | 43 | content = [ 44 | [1, 2, 3, 4, 5], 45 | [1, 2, 3, 4, 5], 46 | [1, 2, 3, 4, 5], 47 | [1, 2, 3, 4, 5], 48 | ] 49 | 50 | def _create_a_file(self, file): 51 | pyexcel.save_as(dest_file_name=file, array=self.content) 52 | 53 | def test_write_array(self): 54 | self._create_a_file(self.testfile) 55 | r = pyexcel.get_sheet(file_name=self.testfile) 56 | actual = list(r.rows()) 57 | assert actual == self.content 58 | 59 | 60 | class PyexcelMultipleSheetBase: 61 | def _write_test_file(self, filename): 62 | pyexcel.save_book_as(bookdict=self.content, dest_file_name=filename) 63 | 64 | def _clean_up(self): 65 | if os.path.exists(self.testfile2): 66 | os.unlink(self.testfile2) 67 | if os.path.exists(self.testfile): 68 | os.unlink(self.testfile) 69 | 70 | def test_sheet_names(self): 71 | r = pyexcel.BookReader(self.testfile) 72 | expected = ["Sheet1", "Sheet2", "Sheet3"] 73 | sheet_names = r.sheet_names() 74 | for name in sheet_names: 75 | assert name in expected 76 | 77 | def test_reading_through_sheets(self): 78 | b = pyexcel.BookReader(self.testfile) 79 | data = list(b["Sheet1"].rows()) 80 | expected = [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]] 81 | assert data == expected 82 | data = list(b["Sheet2"].rows()) 83 | expected = [[4, 4, 4, 4], [5, 5, 5, 5], [6, 6, 6, 6]] 84 | assert data == expected 85 | data = list(b["Sheet3"].rows()) 86 | expected = [["X", "Y", "Z"], [1, 4, 7], [2, 5, 8], [3, 6, 9]] 87 | assert data == expected 88 | sheet3 = b["Sheet3"] 89 | sheet3.name_columns_by_row(0) 90 | data = list(b["Sheet3"].rows()) 91 | expected = [[1, 4, 7], [2, 5, 8], [3, 6, 9]] 92 | assert data == expected 93 | -------------------------------------------------------------------------------- /tests/fixtures/complex-merged-cells-sheet.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-xlsx/0524e27b264ff606e82aeb5244939a52c1cd2668/tests/fixtures/complex-merged-cells-sheet.xlsx -------------------------------------------------------------------------------- /tests/fixtures/complex_hidden_sheets.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-xlsx/0524e27b264ff606e82aeb5244939a52c1cd2668/tests/fixtures/complex_hidden_sheets.xlsx -------------------------------------------------------------------------------- /tests/fixtures/date_field.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-xlsx/0524e27b264ff606e82aeb5244939a52c1cd2668/tests/fixtures/date_field.xlsx -------------------------------------------------------------------------------- /tests/fixtures/file_with_an_empty_sheet.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-xlsx/0524e27b264ff606e82aeb5244939a52c1cd2668/tests/fixtures/file_with_an_empty_sheet.xlsx -------------------------------------------------------------------------------- /tests/fixtures/hidden.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-xlsx/0524e27b264ff606e82aeb5244939a52c1cd2668/tests/fixtures/hidden.xlsx -------------------------------------------------------------------------------- /tests/fixtures/hidden_sheets.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-xlsx/0524e27b264ff606e82aeb5244939a52c1cd2668/tests/fixtures/hidden_sheets.xlsx -------------------------------------------------------------------------------- /tests/fixtures/merged-cell-sheet.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-xlsx/0524e27b264ff606e82aeb5244939a52c1cd2668/tests/fixtures/merged-cell-sheet.xlsx -------------------------------------------------------------------------------- /tests/fixtures/merged-sheet-exploration.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-xlsx/0524e27b264ff606e82aeb5244939a52c1cd2668/tests/fixtures/merged-sheet-exploration.xlsx -------------------------------------------------------------------------------- /tests/fixtures/test-date-format.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-xlsx/0524e27b264ff606e82aeb5244939a52c1cd2668/tests/fixtures/test-date-format.xls -------------------------------------------------------------------------------- /tests/fixtures/test8.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-xlsx/0524e27b264ff606e82aeb5244939a52c1cd2668/tests/fixtures/test8.xlsx -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | nose 2 | mock;python_version<"3" 3 | codecov 4 | coverage 5 | flake8 6 | black 7 | isort 8 | collective.checkdocs 9 | pygments 10 | moban 11 | moban_jinja2_github 12 | xlrd==1.2.0 13 | pyexcel-xls 14 | pyexcel 15 | -------------------------------------------------------------------------------- /tests/test_book.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import time, datetime 3 | 4 | from pyexcel_xlsx import get_data 5 | from pyexcel_xlsx.book import Book 6 | from pyexcel_io._compact import OrderedDict 7 | 8 | from nose.tools import eq_ 9 | 10 | 11 | def test_book(): 12 | test_file = "book_test.xlsx" 13 | book = Book(os.path.join("tests", "fixtures", "date_field.xlsx")) 14 | sheet = book["Sheet2"] 15 | sheet["A1"] = "Test" 16 | book.save(test_file) 17 | book.close() 18 | 19 | data = get_data(test_file) 20 | eq_(data["Sheet2"], [["Test"]]) 21 | os.unlink(test_file) 22 | 23 | 24 | def test_create_new_sheet(): 25 | test_file = "book_test.xlsx" 26 | book = Book(os.path.join("tests", "fixtures", "date_field.xlsx")) 27 | sheet = book["alien"] 28 | sheet["A1"] = "Test" 29 | book.save(test_file) 30 | book.close() 31 | 32 | data = get_data(test_file) 33 | eq_(data["alien"], [["Test"]]) 34 | os.unlink(test_file) 35 | 36 | 37 | def test_create_append_new_data(): 38 | test_file = "book_test.xlsx" 39 | book = Book(os.path.join("tests", "fixtures", "date_field.xlsx")) 40 | sheet = book["alien"] 41 | sheet.append([1, 2, 3]) 42 | sheet.append([3, 4, 5]) 43 | book.save(test_file) 44 | book.close() 45 | 46 | data = get_data(test_file) 47 | eq_(data["alien"], [[1, 2, 3], [3, 4, 5]]) 48 | os.unlink(test_file) 49 | -------------------------------------------------------------------------------- /tests/test_bug_fixes.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This file keeps all fixes for issues found 4 | 5 | """ 6 | 7 | import os 8 | import sys 9 | import datetime 10 | from textwrap import dedent 11 | 12 | import pyexcel as pe 13 | 14 | from nose.tools import eq_ 15 | 16 | IN_TRAVIS = "TRAVIS" in os.environ 17 | 18 | 19 | PY36_ABOVE = sys.version_info[0] == 3 and sys.version_info[1] >= 6 20 | 21 | 22 | def test_pyexcel_issue_5(): 23 | """pyexcel issue #5 24 | 25 | datetime is not properly parsed 26 | """ 27 | s = pe.get_sheet(file_name=get_fixtures("test-date-format.xls")) 28 | s.save_as("issue5.xlsx") 29 | s2 = pe.load("issue5.xlsx") 30 | assert s[0, 0] == datetime.datetime(2015, 11, 11, 11, 12, 0) 31 | assert s2[0, 0] == datetime.datetime(2015, 11, 11, 11, 12, 0) 32 | 33 | 34 | def test_pyexcel_issue_8_with_physical_file(): 35 | """pyexcel issue #8 36 | 37 | formular got lost 38 | """ 39 | tmp_file = "issue_8_save_as.xlsx" 40 | s = pe.get_sheet(file_name=get_fixtures("test8.xlsx")) 41 | s.save_as(tmp_file) 42 | s2 = pe.load(tmp_file) 43 | eq_(str(s), str(s2)) 44 | content = dedent( 45 | """ 46 | CNY: 47 | +----------+----------+------+---+-------+ 48 | | 01/09/13 | 02/09/13 | 1000 | 5 | 13.89 | 49 | +----------+----------+------+---+-------+ 50 | | 02/09/13 | 03/09/13 | 2000 | 6 | 33.33 | 51 | +----------+----------+------+---+-------+ 52 | | 03/09/13 | 04/09/13 | 3000 | 7 | 58.33 | 53 | +----------+----------+------+---+-------+""" 54 | ).strip("\n") 55 | eq_(str(s2), content) 56 | os.unlink(tmp_file) 57 | 58 | 59 | def test_pyexcel_issue_8_with_memory_file(): 60 | """pyexcel issue #8 61 | 62 | formular got lost 63 | """ 64 | tmp_file = "issue_8_save_as.xlsx" 65 | f = open(get_fixtures("test8.xlsx"), "rb") 66 | s = pe.load_from_memory("xlsx", f.read()) 67 | s.save_as(tmp_file) 68 | s2 = pe.load(tmp_file) 69 | eq_(str(s), str(s2)) 70 | content = dedent( 71 | """ 72 | CNY: 73 | +----------+----------+------+---+-------+ 74 | | 01/09/13 | 02/09/13 | 1000 | 5 | 13.89 | 75 | +----------+----------+------+---+-------+ 76 | | 02/09/13 | 03/09/13 | 2000 | 6 | 33.33 | 77 | +----------+----------+------+---+-------+ 78 | | 03/09/13 | 04/09/13 | 3000 | 7 | 58.33 | 79 | +----------+----------+------+---+-------+""" 80 | ).strip("\n") 81 | eq_(str(s2), content) 82 | os.unlink(tmp_file) 83 | 84 | 85 | def test_excessive_columns(): 86 | tmp_file = "date_field.xlsx" 87 | s = pe.get_sheet(file_name=get_fixtures(tmp_file)) 88 | assert s.number_of_columns() == 2 89 | 90 | 91 | def test_issue_8_hidden_sheet(): 92 | test_file = get_fixtures("hidden_sheets.xlsx") 93 | book_dict = pe.get_book_dict(file_name=test_file, library="pyexcel-xlsx") 94 | assert "hidden" not in book_dict 95 | eq_(book_dict["shown"], [["A", "B"]]) 96 | 97 | 98 | def test_issue_8_hidden_sheet_2(): 99 | test_file = get_fixtures("hidden_sheets.xlsx") 100 | book_dict = pe.get_book_dict( 101 | file_name=test_file, skip_hidden_sheets=False, library="pyexcel-xlsx" 102 | ) 103 | assert "hidden" in book_dict 104 | eq_(book_dict["shown"], [["A", "B"]]) 105 | eq_(book_dict["hidden"], [["a", "b"]]) 106 | 107 | 108 | def test_issue_20(): 109 | pe.get_book( 110 | url="https://github.com/pyexcel/pyexcel-xlsx/raw/master/tests/fixtures/file_with_an_empty_sheet.xlsx" # noqa: E501 111 | ) 112 | 113 | 114 | def get_fixtures(file_name): 115 | return os.path.join("tests", "fixtures", file_name) 116 | -------------------------------------------------------------------------------- /tests/test_filter.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pyexcel_io import get_data, save_data 4 | 5 | from nose.tools import eq_ 6 | 7 | 8 | class TestFilter: 9 | def setUp(self): 10 | self.test_file = "test_filter.xlsx" 11 | sample = [ 12 | [1, 21, 31], 13 | [2, 22, 32], 14 | [3, 23, 33], 15 | [4, 24, 34], 16 | [5, 25, 35], 17 | [6, 26, 36], 18 | ] 19 | save_data(self.test_file, sample) 20 | self.sheet_name = "pyexcel_sheet1" 21 | 22 | def test_filter_row(self): 23 | filtered_data = get_data( 24 | self.test_file, start_row=3, library="pyexcel-xlsx" 25 | ) 26 | expected = [[4, 24, 34], [5, 25, 35], [6, 26, 36]] 27 | eq_(filtered_data[self.sheet_name], expected) 28 | 29 | def test_filter_row_2(self): 30 | filtered_data = get_data( 31 | self.test_file, start_row=3, row_limit=1, library="pyexcel-xlsx" 32 | ) 33 | expected = [[4, 24, 34]] 34 | eq_(filtered_data[self.sheet_name], expected) 35 | 36 | def test_filter_column(self): 37 | filtered_data = get_data( 38 | self.test_file, start_column=1, library="pyexcel-xlsx" 39 | ) 40 | expected = [[21, 31], [22, 32], [23, 33], [24, 34], [25, 35], [26, 36]] 41 | eq_(filtered_data[self.sheet_name], expected) 42 | 43 | def test_filter_column_2(self): 44 | filtered_data = get_data( 45 | self.test_file, 46 | start_column=1, 47 | column_limit=1, 48 | library="pyexcel-xlsx", 49 | ) 50 | expected = [[21], [22], [23], [24], [25], [26]] 51 | eq_(filtered_data[self.sheet_name], expected) 52 | 53 | def test_filter_both_ways(self): 54 | filtered_data = get_data( 55 | self.test_file, start_column=1, start_row=3, library="pyexcel-xlsx" 56 | ) 57 | expected = [[24, 34], [25, 35], [26, 36]] 58 | eq_(filtered_data[self.sheet_name], expected) 59 | 60 | def test_filter_both_ways_2(self): 61 | filtered_data = get_data( 62 | self.test_file, 63 | start_column=1, 64 | column_limit=1, 65 | start_row=3, 66 | row_limit=1, 67 | library="pyexcel-xlsx", 68 | ) 69 | expected = [[24]] 70 | eq_(filtered_data[self.sheet_name], expected) 71 | 72 | def tearDown(self): 73 | os.unlink(self.test_file) 74 | -------------------------------------------------------------------------------- /tests/test_formatters.py: -------------------------------------------------------------------------------- 1 | import os 2 | from textwrap import dedent 3 | 4 | import pyexcel as pe 5 | 6 | from nose.tools import eq_ 7 | 8 | 9 | class TestDateFormat: 10 | def test_reading_date_format(self): 11 | """ 12 | date time 13 | 25/12/14 11:11:11 14 | 25/12/14 12:12:12 15 | 01/01/15 13:13:13 16 | 0.0 0.0 17 | """ 18 | import datetime 19 | 20 | r = pe.get_sheet( 21 | file_name=os.path.join("tests", "fixtures", "date_field.xlsx"), 22 | library="pyexcel-xlsx", 23 | ) 24 | assert isinstance(r[1, 0], datetime.date) 25 | eq_(r[1, 0].strftime("%d/%m/%y"), "25/12/14") 26 | assert isinstance(r[1, 1], datetime.time) is True 27 | assert r[1, 1].strftime("%H:%M:%S") == "11:11:11" 28 | value = r[4, 0].isoformat() 29 | eq_(value, "00:00:00") 30 | eq_(r[4, 1].isoformat(), "00:00:00") 31 | 32 | def test_writing_date_format(self): 33 | import datetime 34 | 35 | excel_filename = "testdateformat.xlsx" 36 | data = [ 37 | [ 38 | datetime.date(2014, 12, 25), 39 | datetime.time(11, 11, 11), 40 | datetime.datetime(2014, 12, 25, 11, 11, 11), 41 | ] 42 | ] 43 | pe.save_as(dest_file_name=excel_filename, array=data) 44 | r = pe.get_sheet(file_name=excel_filename, library="pyexcel-xlsx") 45 | assert isinstance(r[0, 0], datetime.date) is True 46 | assert r[0, 0].strftime("%d/%m/%y") == "25/12/14" 47 | assert isinstance(r[0, 1], datetime.time) is True 48 | assert r[0, 1].strftime("%H:%M:%S") == "11:11:11" 49 | assert isinstance(r[0, 2], datetime.date) is True 50 | assert r[0, 2].strftime("%d/%m/%y %H:%M:%S") == "25/12/14 11:11:11" 51 | os.unlink(excel_filename) 52 | 53 | 54 | class TestAutoDetectInt: 55 | def setUp(self): 56 | self.content = [[1, 2, 3.1]] 57 | self.test_file = "test_auto_detect_init.xlsx" 58 | pe.save_as(array=self.content, dest_file_name=self.test_file) 59 | 60 | def test_auto_detect_int(self): 61 | sheet = pe.get_sheet(file_name=self.test_file, library="pyexcel-xlsx") 62 | expected = dedent( 63 | """ 64 | pyexcel_sheet1: 65 | +---+---+-----+ 66 | | 1 | 2 | 3.1 | 67 | +---+---+-----+""" 68 | ).strip() 69 | eq_(str(sheet), expected) 70 | 71 | def test_get_book_auto_detect_int(self): 72 | book = pe.get_book(file_name=self.test_file, library="pyexcel-xlsx") 73 | expected = dedent( 74 | """ 75 | pyexcel_sheet1: 76 | +---+---+-----+ 77 | | 1 | 2 | 3.1 | 78 | +---+---+-----+""" 79 | ).strip() 80 | eq_(str(book), expected) 81 | 82 | def tearDown(self): 83 | os.unlink(self.test_file) 84 | -------------------------------------------------------------------------------- /tests/test_hidden.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pyexcel_xlsx import get_data 4 | 5 | from nose.tools import eq_ 6 | 7 | 8 | def test_hidden_row(): 9 | data = get_data( 10 | os.path.join("tests", "fixtures", "hidden.xlsx"), 11 | skip_hidden_row_and_column=True, 12 | library="pyexcel-xlsx", 13 | ) 14 | expected = [[1, 2], [7, 9]] 15 | eq_(data["Sheet1"], expected) 16 | 17 | 18 | def test_complex_hidden_sheets(): 19 | data = get_data( 20 | os.path.join("tests", "fixtures", "complex_hidden_sheets.xlsx"), 21 | skip_hidden_row_and_column=True, 22 | library="pyexcel-xlsx", 23 | ) 24 | expected = [[1, 3, 5, 7, 9], [31, 33, 35, 37, 39], [61, 63, 65, 67]] 25 | eq_(data["Sheet1"], expected) 26 | -------------------------------------------------------------------------------- /tests/test_merged_cells.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pyexcel_xlsx import get_data 4 | from pyexcel_xlsx.xlsxr import MergedCell 5 | from openpyxl.worksheet.cell_range import CellRange 6 | 7 | from nose.tools import eq_ 8 | 9 | 10 | def test_merged_cells(): 11 | data = get_data( 12 | get_fixture("merged-cell-sheet.xlsx"), 13 | detect_merged_cells=True, 14 | library="pyexcel-xlsx", 15 | ) 16 | expected = [[1, 2, 3], [1, 5, 6], [1, 8, 9], [10, 11, 11]] 17 | eq_(data["Sheet1"], expected) 18 | 19 | 20 | def test_complex_merged_cells(): 21 | data = get_data( 22 | get_fixture("complex-merged-cells-sheet.xlsx"), 23 | detect_merged_cells=True, 24 | library="pyexcel-xlsx", 25 | ) 26 | expected = [ 27 | [1, 1, 2, 3, 15, 16, 22, 22, 24, 24], 28 | [1, 1, 4, 5, 15, 17, 22, 22, 24, 24], 29 | [6, 7, 8, 9, 15, 18, 22, 22, 24, 24], 30 | [10, 11, 11, 12, 19, 19, 23, 23, 24, 24], 31 | [13, 11, 11, 14, 20, 20, 23, 23, 24, 24], 32 | [21, 21, 21, 21, 21, 21, 23, 23, 24, 24], 33 | [25, 25, 25, 25, 25, 25, 25, 25, 25, 25], 34 | [25, 25, 25, 25, 25, 25, 25, 25, 25, 25], 35 | ] 36 | eq_(data["Sheet1"], expected) 37 | 38 | 39 | def test_exploration(): 40 | data = get_data( 41 | get_fixture("merged-sheet-exploration.xlsx"), 42 | detect_merged_cells=True, 43 | library="pyexcel-xlsx", 44 | ) 45 | expected_sheet1 = [ 46 | [1, 1, 1, 1, 1, 1], 47 | [2], 48 | [2], 49 | [2], 50 | [2], 51 | [2], 52 | [2], 53 | [2], 54 | [2], 55 | [2], 56 | ] 57 | eq_(data["Sheet1"], expected_sheet1) 58 | expected_sheet2 = [[3], [3], [3], [3, 4, 4, 4, 4, 4, 4], [3], [3], [3]] 59 | eq_(data["Sheet2"], expected_sheet2) 60 | expected_sheet3 = [ 61 | ["", "", "", "", "", 2, 2, 2], 62 | [], 63 | [], 64 | [], 65 | ["", "", "", 5], 66 | ["", "", "", 5], 67 | ["", "", "", 5], 68 | ["", "", "", 5], 69 | ["", "", "", 5], 70 | ] 71 | eq_(data["Sheet3"], expected_sheet3) 72 | 73 | 74 | def test_merged_cell_class(): 75 | test_dict = {} 76 | merged_cell = MergedCell(CellRange(range_string="A7:J8")) 77 | merged_cell.register_cells(test_dict) 78 | keys = sorted(list(test_dict.keys())) 79 | expected = [ 80 | "7-1", 81 | "7-10", 82 | "7-2", 83 | "7-3", 84 | "7-4", 85 | "7-5", 86 | "7-6", 87 | "7-7", 88 | "7-8", 89 | "7-9", 90 | "8-1", 91 | "8-10", 92 | "8-2", 93 | "8-3", 94 | "8-4", 95 | "8-5", 96 | "8-6", 97 | "8-7", 98 | "8-8", 99 | "8-9", 100 | ] 101 | eq_(keys, expected) 102 | eq_(merged_cell, test_dict["7-1"]) 103 | eq_(merged_cell.bottom_row(), 8) 104 | eq_(merged_cell.right_column(), 10) 105 | 106 | 107 | def get_fixture(file_name): 108 | return os.path.join("tests", "fixtures", file_name) 109 | -------------------------------------------------------------------------------- /tests/test_multiple_sheets.py: -------------------------------------------------------------------------------- 1 | import os 2 | from collections import OrderedDict 3 | 4 | import pyexcel 5 | from base import PyexcelMultipleSheetBase 6 | 7 | from nose.tools import raises 8 | 9 | 10 | class TestXlsmNxlsMultipleSheets(PyexcelMultipleSheetBase): 11 | def setUp(self): 12 | self.testfile = "multiple1.xlsm" 13 | self.testfile2 = "multiple1.xlsx" 14 | self.content = _produce_ordered_dict() 15 | self._write_test_file(self.testfile) 16 | 17 | def tearDown(self): 18 | self._clean_up() 19 | 20 | 21 | class TestXlsNXlsxMultipleSheets(PyexcelMultipleSheetBase): 22 | def setUp(self): 23 | self.testfile = "multiple1.xlsm" 24 | self.testfile2 = "multiple1.xlsx" 25 | self.content = _produce_ordered_dict() 26 | self._write_test_file(self.testfile) 27 | 28 | def tearDown(self): 29 | self._clean_up() 30 | 31 | 32 | class TestAddBooks: 33 | def _write_test_file(self, file): 34 | """ 35 | Make a test file as: 36 | 37 | 1,1,1,1 38 | 2,2,2,2 39 | 3,3,3,3 40 | """ 41 | self.rows = 3 42 | pyexcel.save_book_as(bookdict=self.content, dest_file_name=file) 43 | 44 | def setUp(self): 45 | self.testfile = "multiple3.xlsx" 46 | self.testfile2 = "multiple1.xlsx" 47 | self.testfile3 = "multiple2.xlsx" 48 | self.content = _produce_ordered_dict() 49 | self._write_test_file(self.testfile) 50 | self._write_test_file(self.testfile2) 51 | 52 | def test_load_a_single_sheet(self): 53 | b1 = pyexcel.get_book( 54 | file_name=self.testfile, 55 | sheet_name="Sheet1", 56 | library="pyexcel-xlsx", 57 | ) 58 | assert len(b1.sheet_names()) == 1 59 | assert b1["Sheet1"].to_array() == self.content["Sheet1"] 60 | 61 | def test_load_a_single_sheet2(self): 62 | b1 = pyexcel.get_book( 63 | file_name=self.testfile, sheet_index=1, library="pyexcel-xlsx" 64 | ) 65 | assert len(b1.sheet_names()) == 1 66 | assert b1["Sheet2"].to_array() == self.content["Sheet2"] 67 | 68 | @raises(IndexError) 69 | def test_load_a_single_sheet3(self): 70 | pyexcel.get_book(file_name=self.testfile, sheet_index=10000) 71 | 72 | @raises(ValueError) 73 | def test_load_a_single_sheet4(self): 74 | pyexcel.get_book( 75 | file_name=self.testfile, 76 | sheet_name="Not exist", 77 | library="pyexcel-xlsx", 78 | ) 79 | 80 | def test_delete_sheets(self): 81 | b1 = pyexcel.load_book(self.testfile) 82 | assert len(b1.sheet_names()) == 3 83 | del b1["Sheet1"] 84 | assert len(b1.sheet_names()) == 2 85 | try: 86 | del b1["Sheet1"] 87 | assert 1 == 2 88 | except KeyError: 89 | assert 1 == 1 90 | del b1[1] 91 | assert len(b1.sheet_names()) == 1 92 | try: 93 | del b1[1] 94 | assert 1 == 2 95 | except IndexError: 96 | assert 1 == 1 97 | 98 | def test_delete_sheets2(self): 99 | """repetitively delete first sheet""" 100 | b1 = pyexcel.load_book(self.testfile) 101 | del b1[0] 102 | assert len(b1.sheet_names()) == 2 103 | del b1[0] 104 | assert len(b1.sheet_names()) == 1 105 | del b1[0] 106 | assert len(b1.sheet_names()) == 0 107 | 108 | def test_add_book1(self): 109 | """ 110 | test this scenario: book3 = book1 + book2 111 | """ 112 | b1 = pyexcel.get_book(file_name=self.testfile) 113 | b2 = pyexcel.get_book(file_name=self.testfile2) 114 | b3 = b1 + b2 115 | content = b3.dict 116 | sheet_names = content.keys() 117 | assert len(sheet_names) == 6 118 | for name in sheet_names: 119 | if "Sheet3" in name: 120 | assert content[name] == self.content["Sheet3"] 121 | elif "Sheet2" in name: 122 | assert content[name] == self.content["Sheet2"] 123 | elif "Sheet1" in name: 124 | assert content[name] == self.content["Sheet1"] 125 | 126 | def test_add_book1_in_place(self): 127 | """ 128 | test this scenario: book1 += book2 129 | """ 130 | b1 = pyexcel.BookReader(self.testfile) 131 | b2 = pyexcel.BookReader(self.testfile2) 132 | b1 += b2 133 | content = b1.dict 134 | sheet_names = content.keys() 135 | assert len(sheet_names) == 6 136 | for name in sheet_names: 137 | if "Sheet3" in name: 138 | assert content[name] == self.content["Sheet3"] 139 | elif "Sheet2" in name: 140 | assert content[name] == self.content["Sheet2"] 141 | elif "Sheet1" in name: 142 | assert content[name] == self.content["Sheet1"] 143 | 144 | def test_add_book2(self): 145 | """ 146 | test this scenario: book3 = book1 + sheet3 147 | """ 148 | b1 = pyexcel.BookReader(self.testfile) 149 | b2 = pyexcel.BookReader(self.testfile2) 150 | b3 = b1 + b2["Sheet3"] 151 | content = b3.dict 152 | sheet_names = content.keys() 153 | assert len(sheet_names) == 4 154 | for name in sheet_names: 155 | if "Sheet3" in name: 156 | assert content[name] == self.content["Sheet3"] 157 | elif "Sheet2" in name: 158 | assert content[name] == self.content["Sheet2"] 159 | elif "Sheet1" in name: 160 | assert content[name] == self.content["Sheet1"] 161 | 162 | def test_add_book2_in_place(self): 163 | """ 164 | test this scenario: book3 = book1 + sheet3 165 | """ 166 | b1 = pyexcel.BookReader(self.testfile) 167 | b2 = pyexcel.BookReader(self.testfile2) 168 | b1 += b2["Sheet3"] 169 | content = b1.dict 170 | sheet_names = content.keys() 171 | assert len(sheet_names) == 4 172 | for name in sheet_names: 173 | if "Sheet3" in name: 174 | assert content[name] == self.content["Sheet3"] 175 | elif "Sheet2" in name: 176 | assert content[name] == self.content["Sheet2"] 177 | elif "Sheet1" in name: 178 | assert content[name] == self.content["Sheet1"] 179 | 180 | def test_add_book3(self): 181 | """ 182 | test this scenario: book3 = sheet1 + sheet2 183 | """ 184 | b1 = pyexcel.BookReader(self.testfile) 185 | b2 = pyexcel.BookReader(self.testfile2) 186 | b3 = b1["Sheet1"] + b2["Sheet3"] 187 | content = b3.dict 188 | sheet_names = content.keys() 189 | assert len(sheet_names) == 2 190 | assert content["Sheet3"] == self.content["Sheet3"] 191 | assert content["Sheet1"] == self.content["Sheet1"] 192 | 193 | def test_add_book4(self): 194 | """ 195 | test this scenario: book3 = sheet1 + book 196 | """ 197 | b1 = pyexcel.BookReader(self.testfile) 198 | b2 = pyexcel.BookReader(self.testfile2) 199 | b3 = b1["Sheet1"] + b2 200 | content = b3.dict 201 | sheet_names = content.keys() 202 | assert len(sheet_names) == 4 203 | for name in sheet_names: 204 | if "Sheet3" in name: 205 | assert content[name] == self.content["Sheet3"] 206 | elif "Sheet2" in name: 207 | assert content[name] == self.content["Sheet2"] 208 | elif "Sheet1" in name: 209 | assert content[name] == self.content["Sheet1"] 210 | 211 | def test_add_book_error(self): 212 | """ 213 | test this scenario: book3 = sheet1 + book 214 | """ 215 | b1 = pyexcel.BookReader(self.testfile) 216 | try: 217 | b1 + 12 218 | assert 1 == 2 219 | except TypeError: 220 | assert 1 == 1 221 | try: 222 | b1 += 12 223 | assert 1 == 2 224 | except TypeError: 225 | assert 1 == 1 226 | 227 | def tearDown(self): 228 | if os.path.exists(self.testfile): 229 | os.unlink(self.testfile) 230 | if os.path.exists(self.testfile2): 231 | os.unlink(self.testfile2) 232 | 233 | 234 | class TestMultiSheetReader: 235 | def setUp(self): 236 | self.testfile = "file_with_an_empty_sheet.xlsx" 237 | 238 | def test_reader_with_correct_sheets(self): 239 | r = pyexcel.BookReader( 240 | os.path.join("tests", "fixtures", self.testfile) 241 | ) 242 | assert r.number_of_sheets() == 3 243 | 244 | 245 | def _produce_ordered_dict(): 246 | data_dict = OrderedDict() 247 | data_dict.update({"Sheet1": [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]]}) 248 | data_dict.update({"Sheet2": [[4, 4, 4, 4], [5, 5, 5, 5], [6, 6, 6, 6]]}) 249 | data_dict.update( 250 | {"Sheet3": [["X", "Y", "Z"], [1, 4, 7], [2, 5, 8], [3, 6, 9]]} 251 | ) 252 | return data_dict 253 | -------------------------------------------------------------------------------- /tests/test_reader.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import time, datetime 3 | 4 | from pyexcel_xlsx import get_data 5 | from pyexcel_io._compact import OrderedDict 6 | 7 | from nose.tools import eq_ 8 | 9 | 10 | def test_reading(): 11 | data = get_data( 12 | os.path.join("tests", "fixtures", "date_field.xlsx"), 13 | library="pyexcel-xlsx", 14 | skip_hidden_row_and_column=False, 15 | ) 16 | expected = OrderedDict() 17 | expected.update( 18 | { 19 | "Sheet1": [ 20 | ["Date", "Time"], 21 | [ 22 | datetime(year=2014, month=12, day=25), 23 | time(hour=11, minute=11, second=11), 24 | ], 25 | [ 26 | datetime(2014, 12, 26, 0, 0), 27 | time(hour=12, minute=12, second=12), 28 | ], 29 | [ 30 | datetime(2015, 1, 1, 0, 0), 31 | time(hour=13, minute=13, second=13), 32 | ], 33 | [time(0, 0), time(0, 0)], 34 | ] 35 | } 36 | ) 37 | expected.update({"Sheet2": []}) 38 | expected.update({"Sheet3": []}) 39 | eq_(data, expected) 40 | -------------------------------------------------------------------------------- /tests/test_stringio.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pyexcel 4 | from base import create_sample_file1 5 | 6 | from nose.tools import eq_ 7 | 8 | 9 | class TestStringIO: 10 | def test_xlsx_stringio(self): 11 | testfile = "cute.xlsx" 12 | create_sample_file1(testfile) 13 | with open(testfile, "rb") as f: 14 | content = f.read() 15 | r = pyexcel.get_sheet( 16 | file_type="xlsx", file_content=content, library="pyexcel-xlsx" 17 | ) 18 | result = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", 1.1, 1] 19 | actual = list(r.enumerate()) 20 | eq_(result, actual) 21 | if os.path.exists(testfile): 22 | os.unlink(testfile) 23 | 24 | def test_xlsx_output_stringio(self): 25 | data = [[1, 2, 3], [4, 5, 6]] 26 | io = pyexcel.save_as(dest_file_type="xlsx", array=data) 27 | r = pyexcel.get_sheet( 28 | file_type="xlsx", 29 | file_content=io.getvalue(), 30 | library="pyexcel-xlsx", 31 | ) 32 | result = [1, 2, 3, 4, 5, 6] 33 | actual = list(r.enumerate()) 34 | eq_(result, actual) 35 | -------------------------------------------------------------------------------- /tests/test_writer.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from base import PyexcelWriterBase, PyexcelHatWriterBase 4 | from pyexcel_xlsx import get_data 5 | from pyexcel_xlsx.xlsxw import XLSXWriter as Writer 6 | 7 | 8 | class TestNativeXLSXWriter: 9 | def test_write_book(self): 10 | self.content = { 11 | "Sheet1": [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]], 12 | "Sheet2": [[4, 4, 4, 4], [5, 5, 5, 5], [6, 6, 6, 6]], 13 | "Sheet3": [["X", "Y", "Z"], [1, 4, 7], [2, 5, 8], [3, 6, 9]], 14 | } 15 | self.testfile = "writer.xlsx" 16 | writer = Writer(self.testfile, "xlsx") 17 | writer.write(self.content) 18 | writer.close() 19 | content = get_data(self.testfile) 20 | for key in content.keys(): 21 | content[key] = list(content[key]) 22 | assert content == self.content 23 | 24 | def tearDown(self): 25 | if os.path.exists(self.testfile): 26 | os.unlink(self.testfile) 27 | 28 | 29 | class TestxlsxnCSVWriter(PyexcelWriterBase): 30 | def setUp(self): 31 | self.testfile = "test.xlsx" 32 | self.testfile2 = "test.csv" 33 | 34 | def tearDown(self): 35 | if os.path.exists(self.testfile): 36 | os.unlink(self.testfile) 37 | if os.path.exists(self.testfile2): 38 | os.unlink(self.testfile2) 39 | 40 | 41 | class TestxlsxHatWriter(PyexcelHatWriterBase): 42 | def setUp(self): 43 | self.testfile = "test.xlsx" 44 | 45 | def tearDown(self): 46 | if os.path.exists(self.testfile): 47 | os.unlink(self.testfile) 48 | --------------------------------------------------------------------------------