├── .github ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── lint.yml │ ├── moban-update.yml │ ├── pythonpublish.yml │ └── tests.yml ├── .gitignore ├── .isort.cfg ├── .moban.d ├── custom_README.rst.jj2 ├── custom_setup.py.jj2 ├── requirements.txt └── tests │ ├── custom_base.py.jj2 │ └── custom_requirements.txt.jj2 ├── .moban.yml ├── .readthedocs.yml ├── CHANGELOG.rst ├── CONTRIBUTORS.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── changelog.yml ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ └── index.rst ├── format.sh ├── lint.sh ├── pyexcel-xls.yml ├── pyexcel_xls ├── __init__.py ├── xlsr.py └── xlsw.py ├── pytest.ini ├── requirements.txt ├── rnd_requirements.txt ├── setup.cfg ├── setup.py ├── test.bat ├── test.sh └── tests ├── _compact.py ├── base.py ├── fixtures ├── complex-merged-cells-sheet.xls ├── complex_hidden_sheets.xls ├── date_field.xls ├── file_with_an_empty_sheet.xls ├── hidden.xls ├── hidden_sheets.xls ├── merged-cell-sheet.xls ├── merged-sheet-exploration.xls ├── pyexcel_issue_151.xlsx └── test-date-format.xls ├── requirements.txt ├── requirements3.txt ├── test_bug_fixes.py ├── test_filter.py ├── test_formatters.py ├── test_hidden.py ├── test_merged_cells.py ├── test_multiple_sheets.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 | make test 33 | 34 | - name: Upload coverage 35 | uses: codecov/codecov-action@v1 36 | with: 37 | 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=pyexcel,xlrd,xlwt 4 | known_third_party=mock,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/custom_README.rst.jj2: -------------------------------------------------------------------------------- 1 | {%extends 'README.rst.jj2' %} 2 | 3 | {% block documentation_link %} 4 | {% endblock %} 5 | 6 | {%block description%} 7 | **pyexcel-{{file_type}}** is a tiny wrapper library to read, manipulate and 8 | write data in {{file_type}} format and it can read xlsx and xlsm fromat. 9 | You are likely to use it with `pyexcel `_. 10 | 11 | Oct 2021 - Update: 12 | =================== 13 | 14 | 1. v0.7.0 removed the pin on xlrd < 2. If you have xlrd >= 2, this 15 | library will NOT read 'xlsx' format and you need to install pyexcel-xlsx. Othwise, 16 | this library can use xlrd < 2 to read xlsx format for you. So 'xlsx' support 17 | in this library will vary depending on the installed version of xlrd. 18 | 19 | 2. v0.7.0 can write datetime.timedelta. but when the value is read out, 20 | you will get datetime.datetime. so you as the developer decides what to do with it. 21 | 22 | Past news 23 | =========== 24 | 25 | `detect_merged_cells` allows you to spread the same value among 26 | all merged cells. But be aware that this may slow down its reading 27 | performance. 28 | 29 | `skip_hidden_row_and_column` allows you to skip hidden rows 30 | and columns and is defaulted to **True**. It may slow down its reading 31 | performance. And it is only valid for 'xls' files. For 'xlsx' files, 32 | please use pyexcel-xlsx. 33 | 34 | Warning 35 | ================================================================================ 36 | 37 | **xls file cannot contain more than 65,000 rows**. You are risking the reputation 38 | of yourself/your company/ 39 | `your country `_ if you keep 40 | using xls and are not aware of its row limit. 41 | 42 | {%endblock%} 43 | 44 | {%block extras %} 45 | Known Issues 46 | ============= 47 | 48 | * If a zero was typed in a DATE formatted field in xls, you will get "01/01/1900". 49 | * If a zero was typed in a TIME formatted field in xls, you will get "00:00:00". 50 | {%endblock%} 51 | -------------------------------------------------------------------------------- /.moban.d/custom_setup.py.jj2: -------------------------------------------------------------------------------- 1 | {% extends 'setup.py.jj2' %} 2 | 3 | {%block platform_block%} 4 | {%endblock%} 5 | 6 | {%block additional_keywords %} 7 | 'xls', 8 | 'xlsx', 9 | 'xlsm' 10 | {%endblock%} 11 | 12 | {% block morefiles %}"CONTRIBUTORS.rst",{% endblock %} 13 | 14 | {%block additional_classifiers%} 15 | 'Programming Language :: Python :: Implementation :: PyPy' 16 | {%endblock%} 17 | -------------------------------------------------------------------------------- /.moban.d/requirements.txt: -------------------------------------------------------------------------------- 1 | {% for dependency in dependencies: %} 2 | {{dependency}} 3 | {% endfor %} 4 | -------------------------------------------------------------------------------- /.moban.d/tests/custom_base.py.jj2: -------------------------------------------------------------------------------- 1 | {% extends 'tests/base.py.jj2' %} 2 | 3 | {%block ods_types%} 4 | {%endblock%} 5 | -------------------------------------------------------------------------------- /.moban.d/tests/custom_requirements.txt.jj2: -------------------------------------------------------------------------------- 1 | {% extends 'tests/requirements.txt.jj2' %} 2 | {%block extras %} 3 | pyexcel 4 | xlrd==1.1.0 5 | moban 6 | black;python_version>="3.6" 7 | isort;python_version>="3.6" 8 | {%endblock%} 9 | -------------------------------------------------------------------------------- /.moban.yml: -------------------------------------------------------------------------------- 1 | overrides: "git://github.com/pyexcel/pyexcel-mobans!/mobanfile.yaml" 2 | configuration: 3 | configuration: pyexcel-xls.yml 4 | targets: 5 | - README.rst: custom_README.rst.jj2 6 | - setup.py: custom_setup.py.jj2 7 | - "docs/source/conf.py": "docs/source/conf.py.jj2" 8 | - .gitignore: gitignore.jj2 9 | - MANIFEST.in: MANIFEST.in.jj2 10 | - "tests/requirements.txt": "tests/custom_requirements.txt.jj2" 11 | -------------------------------------------------------------------------------- /.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.1 - 07.03.2024 5 | -------------------------------------------------------------------------------- 6 | 7 | **Removed** 8 | 9 | #. `#52 `_: remove xlsm 10 | support for xlrd > 2.0.0 11 | 12 | **Updated** 13 | 14 | #. `#54 `_: first project in 15 | pyexcel to migrate to pytest 16 | 17 | 0.7.0 - 07.10.2021 18 | -------------------------------------------------------------------------------- 19 | 20 | **Removed** 21 | 22 | #. `#46 `_: remove the hard 23 | pin on xlrd version < 2.0 24 | 25 | **Added** 26 | 27 | #. `#47 `_: limit support to 28 | persist datetime.timedelta. see more details in doc 29 | 30 | 0.6.2 - 12.12.2020 31 | -------------------------------------------------------------------------------- 32 | 33 | **Updated** 34 | 35 | #. lock down xlrd version less than version 2.0, because 2.0+ does not support 36 | xlsx read 37 | 38 | 0.6.1 - 21.10.2020 39 | -------------------------------------------------------------------------------- 40 | 41 | **Updated** 42 | 43 | #. Restrict this library to get installed on python 3.6+, because pyexcel-io 44 | 0.6.0+ supports only python 3.6+. 45 | 46 | 0.6.0 - 8.10.2020 47 | -------------------------------------------------------------------------------- 48 | 49 | **Updated** 50 | 51 | #. New style xlsx plugins, promoted by pyexcel-io v0.6.2. 52 | 53 | 0.5.9 - 29.08.2020 54 | -------------------------------------------------------------------------------- 55 | 56 | **Added** 57 | 58 | #. `#35 `_, include tests 59 | 60 | 0.5.8 - 22.08.2018 61 | -------------------------------------------------------------------------------- 62 | 63 | **Added** 64 | 65 | #. `pyexcel#151 `_, read cell 66 | error as #N/A. 67 | 68 | 0.5.7 - 15.03.2018 69 | -------------------------------------------------------------------------------- 70 | 71 | **Added** 72 | 73 | #. `pyexcel#54 `_, Book.datemode 74 | attribute of that workbook should be passed always. 75 | 76 | 0.5.6 - 15.03.2018 77 | -------------------------------------------------------------------------------- 78 | 79 | **Added** 80 | 81 | #. `pyexcel#120 `_, xlwt cannot 82 | save a book without any sheet. So, let's raise an exception in this case in 83 | order to warn the developers. 84 | 85 | 0.5.5 - 8.11.2017 86 | -------------------------------------------------------------------------------- 87 | 88 | **Added** 89 | 90 | #. `#25 `_, detect merged cell 91 | in .xls 92 | 93 | 0.5.4 - 2.11.2017 94 | -------------------------------------------------------------------------------- 95 | 96 | **Added** 97 | 98 | #. `#24 `_, xlsx format cannot 99 | use skip_hidden_row_and_column. please use pyexcel-xlsx instead. 100 | 101 | 0.5.3 - 2.11.2017 102 | -------------------------------------------------------------------------------- 103 | 104 | **Added** 105 | 106 | #. `#21 `_, skip hidden rows 107 | and columns under 'skip_hidden_row_and_column' flag. 108 | 109 | 0.5.2 - 23.10.2017 110 | -------------------------------------------------------------------------------- 111 | 112 | **updated** 113 | 114 | #. pyexcel `pyexcel#105 `_, 115 | remove gease from setup_requires, introduced by 0.5.1. 116 | #. remove python2.6 test support 117 | #. update its dependecy on pyexcel-io to 0.5.3 118 | 119 | 0.5.1 - 20.10.2017 120 | -------------------------------------------------------------------------------- 121 | 122 | **added** 123 | 124 | #. `pyexcel#103 `_, include 125 | LICENSE file in MANIFEST.in, meaning LICENSE file will appear in the released 126 | tar ball. 127 | 128 | 0.5.0 - 30.08.2017 129 | -------------------------------------------------------------------------------- 130 | 131 | **Updated** 132 | 133 | #. `#20 `_, is handled in 134 | pyexcel-io 135 | #. put dependency on pyexcel-io 0.5.0, which uses cStringIO instead of StringIO. 136 | Hence, there will be performance boost in handling files in memory. 137 | 138 | 0.4.1 - 25.08.2017 139 | -------------------------------------------------------------------------------- 140 | 141 | **Updated** 142 | 143 | #. `#20 `_, handle unseekable 144 | stream given by http response. 145 | 146 | 0.4.0 - 19.06.2017 147 | -------------------------------------------------------------------------------- 148 | 149 | **Updated** 150 | 151 | #. `pyexcel-xlsx#15 `_, close 152 | file handle 153 | #. pyexcel-io plugin interface now updated to use `lml 154 | `_. 155 | 156 | 0.3.3 - 30/05/2017 157 | -------------------------------------------------------------------------------- 158 | 159 | **Updated** 160 | 161 | #. `#18 `_, pass on 162 | encoding_override and others to xlrd. 163 | 164 | 0.3.2 - 18.05.2017 165 | -------------------------------------------------------------------------------- 166 | 167 | **Updated** 168 | 169 | #. `#16 `_, allow mmap to be 170 | passed as file content 171 | 172 | 0.3.1 - 16.01.2017 173 | -------------------------------------------------------------------------------- 174 | 175 | **Updated** 176 | 177 | #. `#14 `_, Python 3.6 - 178 | cannot use LOCALE flag with a str pattern 179 | #. fix its dependency on pyexcel-io 0.3.0 180 | 181 | 0.3.0 - 22.12.2016 182 | -------------------------------------------------------------------------------- 183 | 184 | **Updated** 185 | 186 | #. `#13 `_, alert on empyty 187 | file content 188 | #. Support pyexcel-io v0.3.0 189 | 190 | 0.2.3 - 20.09.2016 191 | -------------------------------------------------------------------------------- 192 | 193 | **Updated** 194 | 195 | #. `#10 `_, To support 196 | generator as member of the incoming two dimensional data 197 | 198 | 0.2.2 - 31.08.2016 199 | -------------------------------------------------------------------------------- 200 | 201 | **Added** 202 | 203 | #. support pagination. two pairs: start_row, row_limit and start_column, 204 | column_limit help you deal with large files. 205 | 206 | 0.2.1 - 13.07.2016 207 | -------------------------------------------------------------------------------- 208 | 209 | **Added** 210 | 211 | #. `#9 `_, `skip_hidden_sheets` 212 | is added. By default, hidden sheets are skipped when reading all sheets. 213 | Reading sheet by name or by index are not affected. 214 | 215 | 0.2.0 - 01.06.2016 216 | -------------------------------------------------------------------------------- 217 | 218 | **Added** 219 | 220 | #. By default, `float` will be converted to `int` where fits. `auto_detect_int`, 221 | a flag to switch off the autoatic conversion from `float` to `int`. 222 | #. 'library=pyexcel-xls' was added so as to inform pyexcel to use it instead of 223 | other libraries, in the situation where there are more than one plugin for a 224 | file type, e.g. xlsm 225 | 226 | **Updated** 227 | 228 | #. support the auto-import feature of pyexcel-io 0.2.0 229 | #. xlwt is now used for python 2 implementation while xlwt-future is used for 230 | python 3 231 | 232 | 0.1.0 - 17.01.2016 233 | -------------------------------------------------------------------------------- 234 | 235 | **Added** 236 | 237 | #. Passing "streaming=True" to get_data, you will get the two dimensional array 238 | as a generator 239 | #. Passing "data=your_generator" to save_data is acceptable too. 240 | -------------------------------------------------------------------------------- /CONTRIBUTORS.rst: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 contributors 4 | ================================================================================ 5 | 6 | In alphabetical order: 7 | 8 | * `Ivan Cvitic `_ 9 | * `John Vandenberg `_ 10 | * `Peter Carnesciali `_ 11 | * `Pyrox `_ 12 | * `Vincent Raspal `_ 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-xls' 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 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================================================================================ 2 | pyexcel-xls - Let you focus on data, instead of xls 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-xls/branch/master/graph/badge.svg 12 | :target: https://codecov.io/gh/pyexcel/pyexcel-xls 13 | 14 | .. image:: https://badge.fury.io/py/pyexcel-xls.svg 15 | :target: https://pypi.org/project/pyexcel-xls 16 | 17 | .. image:: https://anaconda.org/conda-forge/pyexcel-xls/badges/version.svg 18 | :target: https://anaconda.org/conda-forge/pyexcel-xls 19 | 20 | 21 | .. image:: https://pepy.tech/badge/pyexcel-xls/month 22 | :target: https://pepy.tech/project/pyexcel-xls 23 | 24 | .. image:: https://anaconda.org/conda-forge/pyexcel-xls/badges/downloads.svg 25 | :target: https://anaconda.org/conda-forge/pyexcel-xls 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-xls** is a tiny wrapper library to read, manipulate and 37 | write data in xls format and it can read xlsx and xlsm fromat. 38 | You are likely to use it with `pyexcel `_. 39 | 40 | Oct 2021 - Update: 41 | =================== 42 | 43 | 1. v0.7.0 removed the pin on xlrd < 2. If you have xlrd >= 2, this 44 | library will NOT read 'xlsx' format and you need to install pyexcel-xlsx. Othwise, 45 | this library can use xlrd < 2 to read xlsx format for you. So 'xlsx' support 46 | in this library will vary depending on the installed version of xlrd. 47 | 48 | 2. v0.7.0 can write datetime.timedelta. but when the value is read out, 49 | you will get datetime.datetime. so you as the developer decides what to do with it. 50 | 51 | Past news 52 | =========== 53 | 54 | `detect_merged_cells` allows you to spread the same value among 55 | all merged cells. But be aware that this may slow down its reading 56 | performance. 57 | 58 | `skip_hidden_row_and_column` allows you to skip hidden rows 59 | and columns and is defaulted to **True**. It may slow down its reading 60 | performance. And it is only valid for 'xls' files. For 'xlsx' files, 61 | please use pyexcel-xlsx. 62 | 63 | Warning 64 | ================================================================================ 65 | 66 | **xls file cannot contain more than 65,000 rows**. You are risking the reputation 67 | of yourself/your company/ 68 | `your country `_ if you keep 69 | using xls and are not aware of its row limit. 70 | 71 | 72 | Support the project 73 | ================================================================================ 74 | 75 | If your company uses pyexcel and its components in a revenue-generating product, 76 | please consider supporting the project on GitHub or 77 | `Patreon `_. Your financial 78 | support will enable me to dedicate more time to coding, improving documentation, 79 | and creating engaging content. 80 | 81 | 82 | Known constraints 83 | ================== 84 | 85 | Fonts, colors and charts are not supported. 86 | 87 | Nor to read password protected xls, xlsx and ods files. 88 | 89 | Installation 90 | ================================================================================ 91 | 92 | 93 | You can install pyexcel-xls via pip: 94 | 95 | .. code-block:: bash 96 | 97 | $ pip install pyexcel-xls 98 | 99 | 100 | or clone it and install it: 101 | 102 | .. code-block:: bash 103 | 104 | $ git clone https://github.com/pyexcel/pyexcel-xls.git 105 | $ cd pyexcel-xls 106 | $ python setup.py install 107 | 108 | Usage 109 | ================================================================================ 110 | 111 | As a standalone library 112 | -------------------------------------------------------------------------------- 113 | 114 | .. testcode:: 115 | :hide: 116 | 117 | >>> import os 118 | >>> import sys 119 | >>> from io import BytesIO 120 | >>> from collections import OrderedDict 121 | 122 | 123 | Write to an xls file 124 | ******************************************************************************** 125 | 126 | 127 | 128 | Here's the sample code to write a dictionary to an xls file: 129 | 130 | .. code-block:: python 131 | 132 | >>> from pyexcel_xls import save_data 133 | >>> data = OrderedDict() # from collections import OrderedDict 134 | >>> data.update({"Sheet 1": [[1, 2, 3], [4, 5, 6]]}) 135 | >>> data.update({"Sheet 2": [["row 1", "row 2", "row 3"]]}) 136 | >>> save_data("your_file.xls", data) 137 | 138 | 139 | Read from an xls file 140 | ******************************************************************************** 141 | 142 | Here's the sample code: 143 | 144 | .. code-block:: python 145 | 146 | >>> from pyexcel_xls import get_data 147 | >>> data = get_data("your_file.xls") 148 | >>> import json 149 | >>> print(json.dumps(data)) 150 | {"Sheet 1": [[1, 2, 3], [4, 5, 6]], "Sheet 2": [["row 1", "row 2", "row 3"]]} 151 | 152 | 153 | Write an xls to memory 154 | ******************************************************************************** 155 | 156 | Here's the sample code to write a dictionary to an xls file: 157 | 158 | .. code-block:: python 159 | 160 | >>> from pyexcel_xls import save_data 161 | >>> data = OrderedDict() 162 | >>> data.update({"Sheet 1": [[1, 2, 3], [4, 5, 6]]}) 163 | >>> data.update({"Sheet 2": [[7, 8, 9], [10, 11, 12]]}) 164 | >>> io = BytesIO() 165 | >>> save_data(io, data) 166 | >>> # do something with the io 167 | >>> # In reality, you might give it to your http response 168 | >>> # object for downloading 169 | 170 | 171 | 172 | 173 | Read from an xls from memory 174 | ******************************************************************************** 175 | 176 | Continue from previous example: 177 | 178 | .. code-block:: python 179 | 180 | >>> # This is just an illustration 181 | >>> # In reality, you might deal with xls file upload 182 | >>> # where you will read from requests.FILES['YOUR_XLS_FILE'] 183 | >>> data = get_data(io) 184 | >>> print(json.dumps(data)) 185 | {"Sheet 1": [[1, 2, 3], [4, 5, 6]], "Sheet 2": [[7, 8, 9], [10, 11, 12]]} 186 | 187 | 188 | Pagination feature 189 | ******************************************************************************** 190 | 191 | 192 | 193 | Let's assume the following file is a huge xls file: 194 | 195 | .. code-block:: python 196 | 197 | >>> huge_data = [ 198 | ... [1, 21, 31], 199 | ... [2, 22, 32], 200 | ... [3, 23, 33], 201 | ... [4, 24, 34], 202 | ... [5, 25, 35], 203 | ... [6, 26, 36] 204 | ... ] 205 | >>> sheetx = { 206 | ... "huge": huge_data 207 | ... } 208 | >>> save_data("huge_file.xls", sheetx) 209 | 210 | And let's pretend to read partial data: 211 | 212 | .. code-block:: python 213 | 214 | >>> partial_data = get_data("huge_file.xls", start_row=2, row_limit=3) 215 | >>> print(json.dumps(partial_data)) 216 | {"huge": [[3, 23, 33], [4, 24, 34], [5, 25, 35]]} 217 | 218 | And you could as well do the same for columns: 219 | 220 | .. code-block:: python 221 | 222 | >>> partial_data = get_data("huge_file.xls", start_column=1, column_limit=2) 223 | >>> print(json.dumps(partial_data)) 224 | {"huge": [[21, 31], [22, 32], [23, 33], [24, 34], [25, 35], [26, 36]]} 225 | 226 | Obvious, you could do both at the same time: 227 | 228 | .. code-block:: python 229 | 230 | >>> partial_data = get_data("huge_file.xls", 231 | ... start_row=2, row_limit=3, 232 | ... start_column=1, column_limit=2) 233 | >>> print(json.dumps(partial_data)) 234 | {"huge": [[23, 33], [24, 34], [25, 35]]} 235 | 236 | .. testcode:: 237 | :hide: 238 | 239 | >>> os.unlink("huge_file.xls") 240 | 241 | 242 | As a pyexcel plugin 243 | -------------------------------------------------------------------------------- 244 | 245 | No longer, explicit import is needed since pyexcel version 0.2.2. Instead, 246 | this library is auto-loaded. So if you want to read data in xls format, 247 | installing it is enough. 248 | 249 | 250 | Reading from an xls file 251 | ******************************************************************************** 252 | 253 | Here is the sample code: 254 | 255 | .. code-block:: python 256 | 257 | >>> import pyexcel as pe 258 | >>> sheet = pe.get_book(file_name="your_file.xls") 259 | >>> sheet 260 | Sheet 1: 261 | +---+---+---+ 262 | | 1 | 2 | 3 | 263 | +---+---+---+ 264 | | 4 | 5 | 6 | 265 | +---+---+---+ 266 | Sheet 2: 267 | +-------+-------+-------+ 268 | | row 1 | row 2 | row 3 | 269 | +-------+-------+-------+ 270 | 271 | 272 | Writing to an xls file 273 | ******************************************************************************** 274 | 275 | Here is the sample code: 276 | 277 | .. code-block:: python 278 | 279 | >>> sheet.save_as("another_file.xls") 280 | 281 | 282 | Reading from a IO instance 283 | ******************************************************************************** 284 | 285 | You got to wrap the binary content with stream to get xls working: 286 | 287 | .. code-block:: python 288 | 289 | >>> # This is just an illustration 290 | >>> # In reality, you might deal with xls file upload 291 | >>> # where you will read from requests.FILES['YOUR_XLS_FILE'] 292 | >>> xlsfile = "another_file.xls" 293 | >>> with open(xlsfile, "rb") as f: 294 | ... content = f.read() 295 | ... r = pe.get_book(file_type="xls", file_content=content) 296 | ... print(r) 297 | ... 298 | Sheet 1: 299 | +---+---+---+ 300 | | 1 | 2 | 3 | 301 | +---+---+---+ 302 | | 4 | 5 | 6 | 303 | +---+---+---+ 304 | Sheet 2: 305 | +-------+-------+-------+ 306 | | row 1 | row 2 | row 3 | 307 | +-------+-------+-------+ 308 | 309 | 310 | Writing to a BytesIO instance 311 | ******************************************************************************** 312 | 313 | You need to pass a BytesIO instance to Writer: 314 | 315 | .. code-block:: python 316 | 317 | >>> data = [ 318 | ... [1, 2, 3], 319 | ... [4, 5, 6] 320 | ... ] 321 | >>> io = BytesIO() 322 | >>> sheet = pe.Sheet(data) 323 | >>> io = sheet.save_to_memory("xls", io) 324 | >>> # then do something with io 325 | >>> # In reality, you might give it to your http response 326 | >>> # object for downloading 327 | 328 | 329 | License 330 | ================================================================================ 331 | 332 | New BSD License 333 | 334 | Developer guide 335 | ================== 336 | 337 | Development steps for code changes 338 | 339 | #. git clone https://github.com/pyexcel/pyexcel-xls.git 340 | #. cd pyexcel-xls 341 | 342 | Upgrade your setup tools and pip. They are needed for development and testing only: 343 | 344 | #. pip install --upgrade setuptools pip 345 | 346 | Then install relevant development requirements: 347 | 348 | #. pip install -r rnd_requirements.txt # if such a file exists 349 | #. pip install -r requirements.txt 350 | #. pip install -r tests/requirements.txt 351 | 352 | Once you have finished your changes, please provide test case(s), relevant documentation 353 | and update changelog.yml 354 | 355 | .. note:: 356 | 357 | As to rnd_requirements.txt, usually, it is created when a dependent 358 | library is not released. Once the dependency is installed 359 | (will be released), the future 360 | version of the dependency in the requirements.txt will be valid. 361 | 362 | 363 | How to test your contribution 364 | -------------------------------------------------------------------------------- 365 | 366 | Although `nose` and `doctest` are both used in code testing, it is advisable 367 | that unit tests are put in tests. `doctest` is incorporated only to make sure 368 | the code examples in documentation remain valid across different development 369 | releases. 370 | 371 | On Linux/Unix systems, please launch your tests like this:: 372 | 373 | $ make 374 | 375 | On Windows, please issue this command:: 376 | 377 | > test.bat 378 | 379 | 380 | Before you commit 381 | ------------------------------ 382 | 383 | Please run:: 384 | 385 | $ make format 386 | 387 | so as to beautify your code otherwise your build may fail your unit test. 388 | 389 | 390 | Known Issues 391 | ============= 392 | 393 | * If a zero was typed in a DATE formatted field in xls, you will get "01/01/1900". 394 | * If a zero was typed in a TIME formatted field in xls, you will get "00:00:00". 395 | 396 | .. testcode:: 397 | :hide: 398 | 399 | >>> import os 400 | >>> os.unlink("your_file.xls") 401 | >>> os.unlink("another_file.xls") 402 | -------------------------------------------------------------------------------- /changelog.yml: -------------------------------------------------------------------------------- 1 | name: pyexcel-xls 2 | organisation: pyexcel 3 | releases: 4 | - changes: 5 | - action: Removed 6 | details: 7 | - "`#52`: remove xlsm support for xlrd > 2.0.0" 8 | - action: Updated 9 | details: 10 | - "`#54`: first project in pyexcel to migrate to pytest" 11 | date: 07.03.2024 12 | version: 0.7.1 13 | - changes: 14 | - action: Removed 15 | details: 16 | - "`#46`: remove the hard pin on xlrd version < 2.0" 17 | - action: Added 18 | details: 19 | - "`#47`: limit support to persist datetime.timedelta. see more details in doc" 20 | date: 07.10.2021 21 | version: 0.7.0 22 | - changes: 23 | - action: Updated 24 | details: 25 | - "lock down xlrd version less than version 2.0, because 2.0+ does not support xlsx read" 26 | date: 12.12.2020 27 | version: 0.6.2 28 | - changes: 29 | - action: Updated 30 | details: 31 | - "Restrict this library to get installed on python 3.6+, because pyexcel-io 0.6.0+ supports only python 3.6+." 32 | date: 21.10.2020 33 | version: 0.6.1 34 | - changes: 35 | - action: Updated 36 | details: 37 | - 'New style xlsx plugins, promoted by pyexcel-io v0.6.2.' 38 | date: 8.10.2020 39 | version: 0.6.0 40 | - changes: 41 | - action: Added 42 | details: 43 | - "`#35`, include tests" 44 | date: 29.08.2020 45 | version: 0.5.9 46 | - changes: 47 | - action: Added 48 | details: 49 | - "`pyexcel#151`, read cell error as #N/A." 50 | date: 22.08.2018 51 | version: 0.5.8 52 | - changes: 53 | - action: Added 54 | details: 55 | - "`pyexcel#54`, Book.datemode attribute of that workbook should be passed always." 56 | date: 15.03.2018 57 | version: 0.5.7 58 | - changes: 59 | - action: Added 60 | details: 61 | - "`pyexcel#120`, xlwt cannot save a book without any sheet. So, let's raise an exception in this case in order to warn the developers." 62 | date: 15.03.2018 63 | version: 0.5.6 64 | - changes: 65 | - action: Added 66 | details: 67 | - '`#25`, detect merged cell in .xls' 68 | date: 8.11.2017 69 | version: 0.5.5 70 | - changes: 71 | - action: Added 72 | details: 73 | - '`#24`, xlsx format cannot use skip_hidden_row_and_column. please use pyexcel-xlsx 74 | instead.' 75 | date: 2.11.2017 76 | version: 0.5.4 77 | - changes: 78 | - action: Added 79 | details: 80 | - '`#21`, skip hidden rows and columns under ''skip_hidden_row_and_column'' flag.' 81 | date: 2.11.2017 82 | version: 0.5.3 83 | - changes: 84 | - action: updated 85 | details: 86 | - pyexcel `pyexcel#105`, remove gease from setup_requires, introduced by 0.5.1. 87 | - remove python2.6 test support 88 | - update its dependecy on pyexcel-io to 0.5.3 89 | date: 23.10.2017 90 | version: 0.5.2 91 | - changes: 92 | - action: added 93 | details: 94 | - '`pyexcel#103`, include LICENSE file in MANIFEST.in, meaning LICENSE file will 95 | appear in the released tar ball.' 96 | date: 20.10.2017 97 | version: 0.5.1 98 | - changes: 99 | - action: Updated 100 | details: 101 | - '`#20`, is handled in pyexcel-io' 102 | - put dependency on pyexcel-io 0.5.0, which uses cStringIO instead of StringIO. Hence, 103 | there will be performance boost in handling files in memory. 104 | date: 30.08.2017 105 | version: 0.5.0 106 | - changes: 107 | - action: Updated 108 | details: 109 | - '`#20`, handle unseekable stream given by http response.' 110 | date: 25.08.2017 111 | version: 0.4.1 112 | - changes: 113 | - action: Updated 114 | details: 115 | - '`pyexcel-xlsx#15`, close file handle' 116 | - pyexcel-io plugin interface now updated to use `lml `_. 117 | date: 19.06.2017 118 | version: 0.4.0 119 | - changes: 120 | - action: Updated 121 | details: 122 | - '`#18`, pass on encoding_override and others to xlrd.' 123 | date: 30/05/2017 124 | version: 0.3.3 125 | - changes: 126 | - action: Updated 127 | details: 128 | - '`#16`, allow mmap to be passed as file content' 129 | date: 18.05.2017 130 | version: 0.3.2 131 | - changes: 132 | - action: Updated 133 | details: 134 | - '`#14`, Python 3.6 - cannot use LOCALE flag with a str pattern' 135 | - fix its dependency on pyexcel-io 0.3.0 136 | date: 16.01.2017 137 | version: 0.3.1 138 | - changes: 139 | - action: Updated 140 | details: 141 | - '`#13`, alert on empyty file content' 142 | - Support pyexcel-io v0.3.0 143 | date: 22.12.2016 144 | version: 0.3.0 145 | - changes: 146 | - action: Updated 147 | details: 148 | - '`#10`, To support generator as member of the incoming two dimensional data' 149 | date: 20.09.2016 150 | version: 0.2.3 151 | - changes: 152 | - action: Added 153 | details: 154 | - 'support pagination. two pairs: start_row, row_limit and start_column, column_limit 155 | help you deal with large files.' 156 | date: 31.08.2016 157 | version: 0.2.2 158 | - changes: 159 | - action: Added 160 | details: 161 | - '`#9`, `skip_hidden_sheets` is added. By default, hidden sheets are skipped 162 | when reading all sheets. Reading sheet by name or by index are not affected.' 163 | date: 13.07.2016 164 | version: 0.2.1 165 | - changes: 166 | - action: Added 167 | details: 168 | - By default, `float` will be converted to `int` where fits. `auto_detect_int`, a 169 | flag to switch off the autoatic conversion from `float` to `int`. 170 | - '''library=pyexcel-xls'' was added so as to inform pyexcel to use it instead 171 | of other libraries, in the situation where there are more than one plugin for a 172 | file type, e.g. xlsm' 173 | - action: Updated 174 | details: 175 | - support the auto-import feature of pyexcel-io 0.2.0 176 | - xlwt is now used for python 2 implementation while xlwt-future is used for python 177 | 3 178 | date: 01.06.2016 179 | version: 0.2.0 180 | - changes: 181 | - action: Added 182 | details: 183 | - Passing "streaming=True" to get_data, you will get the two dimensional array as 184 | a generator 185 | - Passing "data=your_generator" to save_data is acceptable too. 186 | date: 17.01.2016 187 | version: 0.1.0 188 | -------------------------------------------------------------------------------- /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-xls.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyexcel-xls.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-xls" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyexcel-xls" 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-xls.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pyexcel-xls.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 xls format. It' + 4 | ' reads xlsx and xlsm 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-xls' 26 | copyright = '2015-2025 Onni Software Ltd.' 27 | author = 'C.W.' 28 | # The short X.Y version 29 | version = '0.7.1' 30 | # The full version, including alpha/beta/rc tags 31 | release = '0.7.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-xls', 80 | 'pyexcel-xls Documentation', 81 | 'Onni Software Ltd.', 'pyexcel-xls', 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-xls documentation master file, created by 2 | sphinx-quickstart on Sun Nov 1 18:41: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 | -------------------------------------------------------------------------------- /format.sh: -------------------------------------------------------------------------------- 1 | isort $(find pyexcel_xls -name "*.py"|xargs echo) $(find tests -name "*.py"|xargs echo) 2 | black -l 79 pyexcel_xls 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-xls.yml: -------------------------------------------------------------------------------- 1 | overrides: "pyexcel.yaml" 2 | name: "pyexcel-xls" 3 | nick_name: xls 4 | version: 0.7.1 5 | current_version: 0.7.1 6 | release: 0.7.1 7 | file_type: xls 8 | is_on_conda: true 9 | dependencies: 10 | - pyexcel-io>=0.6.2 11 | - xlrd 12 | - xlwt 13 | test_dependencies: 14 | - pyexcel 15 | - pytest 16 | description: A wrapper library to read, manipulate and write data in xls format. It reads xlsx and xlsm format 17 | moban_command: false 18 | python_requires: ">=3.6" 19 | min_python_version: "3.6" 20 | use_pytest: true -------------------------------------------------------------------------------- /pyexcel_xls/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyexcel_xls 3 | ~~~~~~~~~~~~~~~~~~~ 4 | 5 | The lower level xls/xlsx/xlsm file format handler using xlrd/xlwt 6 | 7 | :copyright: (c) 2016-2021 by Onni Software Ltd 8 | :license: New BSD License 9 | """ 10 | 11 | import xlrd 12 | 13 | # flake8: noqa 14 | from pyexcel_io.io import get_data as read_data 15 | from pyexcel_io.io import isstream 16 | from pyexcel_io.io import save_data as write_data 17 | 18 | # this line has to be place above all else 19 | # because of dynamic import 20 | from pyexcel_io.plugins import IOPluginInfoChainV2 21 | 22 | __FILE_TYPE__ = "xls" 23 | 24 | 25 | def xlrd_version_2_or_greater(): 26 | xlrd_version = getattr(xlrd, "__version__") 27 | 28 | if xlrd_version: 29 | major = int(xlrd_version.split(".")[0]) 30 | if major >= 2: 31 | return True 32 | return False 33 | 34 | 35 | XLRD_VERSION_2_OR_ABOVE = xlrd_version_2_or_greater() 36 | supported_file_formats = [__FILE_TYPE__, "xlsx", "xlsm"] 37 | if XLRD_VERSION_2_OR_ABOVE: 38 | supported_file_formats.remove("xlsx") 39 | supported_file_formats.remove("xlsm") 40 | 41 | 42 | IOPluginInfoChainV2(__name__).add_a_reader( 43 | relative_plugin_class_path="xlsr.XLSInFile", 44 | locations=["file"], 45 | file_types=supported_file_formats, 46 | stream_type="binary", 47 | ).add_a_reader( 48 | relative_plugin_class_path="xlsr.XLSInMemory", 49 | locations=["memory"], 50 | file_types=supported_file_formats, 51 | stream_type="binary", 52 | ).add_a_reader( 53 | relative_plugin_class_path="xlsr.XLSInContent", 54 | locations=["content"], 55 | file_types=supported_file_formats, 56 | stream_type="binary", 57 | ).add_a_writer( 58 | relative_plugin_class_path="xlsw.XLSWriter", 59 | locations=["file", "memory"], 60 | file_types=[__FILE_TYPE__], 61 | stream_type="binary", 62 | ) 63 | 64 | 65 | def get_data(afile, file_type=None, **keywords): 66 | """standalone module function for reading module supported file type""" 67 | if isstream(afile) and file_type is None: 68 | file_type = __FILE_TYPE__ 69 | return read_data(afile, file_type=file_type, **keywords) 70 | 71 | 72 | def save_data(afile, data, file_type=None, **keywords): 73 | """standalone module function for writing module supported file type""" 74 | if isstream(afile) and file_type is None: 75 | file_type = __FILE_TYPE__ 76 | write_data(afile, data, file_type=file_type, **keywords) 77 | -------------------------------------------------------------------------------- /pyexcel_xls/xlsr.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyexcel_xlsr 3 | ~~~~~~~~~~~~~~~~~~~ 4 | 5 | The lower level xls/xlsm file format handler using xlrd 6 | 7 | :copyright: (c) 2016-2021 by Onni Software Ltd 8 | :license: New BSD License 9 | """ 10 | 11 | import datetime 12 | 13 | import xlrd 14 | from pyexcel_io.service import has_no_digits_in_float 15 | from pyexcel_io.plugin_api import ISheet, IReader 16 | 17 | XLS_KEYWORDS = [ 18 | "filename", 19 | "logfile", 20 | "verbosity", 21 | "use_mmap", 22 | "file_contents", 23 | "encoding_override", 24 | "formatting_info", 25 | "on_demand", 26 | "ragged_rows", 27 | ] 28 | DEFAULT_ERROR_VALUE = "#N/A" 29 | 30 | 31 | class MergedCell(object): 32 | def __init__(self, row_low, row_high, column_low, column_high): 33 | self.__rl = row_low 34 | self.__rh = row_high 35 | self.__cl = column_low 36 | self.__ch = column_high 37 | self.value = None 38 | 39 | def register_cells(self, registry): 40 | for rowx in range(self.__rl, self.__rh): 41 | for colx in range(self.__cl, self.__ch): 42 | key = "%s-%s" % (rowx, colx) 43 | registry[key] = self 44 | 45 | 46 | class XLSheet(ISheet): 47 | """ 48 | xls, xlsx, xlsm sheet reader 49 | 50 | Currently only support first sheet in the file 51 | """ 52 | 53 | def __init__(self, sheet, auto_detect_int=True, date_mode=0, **keywords): 54 | self.__auto_detect_int = auto_detect_int 55 | self.__hidden_cols = [] 56 | self.__hidden_rows = [] 57 | self.__merged_cells = {} 58 | self._book_date_mode = date_mode 59 | self.xls_sheet = sheet 60 | self._keywords = keywords 61 | if keywords.get("detect_merged_cells") is True: 62 | for merged_cell_ranges in sheet.merged_cells: 63 | merged_cells = MergedCell(*merged_cell_ranges) 64 | merged_cells.register_cells(self.__merged_cells) 65 | if keywords.get("skip_hidden_row_and_column") is True: 66 | for col_index, info in self.xls_sheet.colinfo_map.items(): 67 | if info.hidden == 1: 68 | self.__hidden_cols.append(col_index) 69 | for row_index, info in self.xls_sheet.rowinfo_map.items(): 70 | if info.hidden == 1: 71 | self.__hidden_rows.append(row_index) 72 | 73 | @property 74 | def name(self): 75 | return self.xls_sheet.name 76 | 77 | def row_iterator(self): 78 | number_of_rows = self.xls_sheet.nrows - len(self.__hidden_rows) 79 | return range(number_of_rows) 80 | 81 | def column_iterator(self, row): 82 | number_of_columns = self.xls_sheet.ncols - len(self.__hidden_cols) 83 | for column in range(number_of_columns): 84 | yield self.cell_value(row, column) 85 | 86 | def cell_value(self, row, column): 87 | """ 88 | Random access to the xls cells 89 | """ 90 | if self._keywords.get("skip_hidden_row_and_column") is True: 91 | row, column = self._offset_hidden_indices(row, column) 92 | cell_type = self.xls_sheet.cell_type(row, column) 93 | value = self.xls_sheet.cell_value(row, column) 94 | 95 | if cell_type == xlrd.XL_CELL_DATE: 96 | value = xldate_to_python_date(value, self._book_date_mode) 97 | elif cell_type == xlrd.XL_CELL_NUMBER and self.__auto_detect_int: 98 | if has_no_digits_in_float(value): 99 | value = int(value) 100 | elif cell_type == xlrd.XL_CELL_ERROR: 101 | value = DEFAULT_ERROR_VALUE 102 | 103 | if self.__merged_cells: 104 | merged_cell = self.__merged_cells.get("%s-%s" % (row, column)) 105 | if merged_cell: 106 | if merged_cell.value: 107 | value = merged_cell.value 108 | else: 109 | merged_cell.value = value 110 | return value 111 | 112 | def _offset_hidden_indices(self, row, column): 113 | row = calculate_offsets(row, self.__hidden_rows) 114 | column = calculate_offsets(column, self.__hidden_cols) 115 | return row, column 116 | 117 | 118 | def calculate_offsets(incoming_index, hidden_indices): 119 | offset = 0 120 | for index in hidden_indices: 121 | if index <= (incoming_index + offset): 122 | offset += 1 123 | return incoming_index + offset 124 | 125 | 126 | class XLSReader(IReader): 127 | """ 128 | XLSBook reader 129 | 130 | It reads xls, xlsm, xlsx work book 131 | """ 132 | 133 | def __init__(self, file_type, **keywords): 134 | self.__skip_hidden_sheets = keywords.get("skip_hidden_sheets", True) 135 | self.__skip_hidden_row_column = keywords.get( 136 | "skip_hidden_row_and_column", True 137 | ) 138 | self.__detect_merged_cells = keywords.get("detect_merged_cells", False) 139 | self._keywords = keywords 140 | xlrd_params = self._extract_xlrd_params() 141 | if self.__skip_hidden_row_column and file_type == "xls": 142 | xlrd_params["formatting_info"] = True 143 | if self.__detect_merged_cells: 144 | xlrd_params["formatting_info"] = True 145 | 146 | self.content_array = [] 147 | self.xls_book = self.get_xls_book(**xlrd_params) 148 | for sheet in self.xls_book.sheets(): 149 | if self.__skip_hidden_sheets and sheet.visibility != 0: 150 | continue 151 | self.content_array.append(sheet) 152 | 153 | def read_sheet(self, index): 154 | native_sheet = self.content_array[index] 155 | sheet = XLSheet( 156 | native_sheet, date_mode=self.xls_book.datemode, **self._keywords 157 | ) 158 | return sheet 159 | 160 | def close(self): 161 | if self.xls_book: 162 | self.xls_book.release_resources() 163 | self.xls_book = None 164 | 165 | def get_xls_book(self, **xlrd_params): 166 | xls_book = xlrd.open_workbook(**xlrd_params) 167 | return xls_book 168 | 169 | def _extract_xlrd_params(self): 170 | params = {} 171 | if self._keywords is not None: 172 | for key in list(self._keywords.keys()): 173 | if key in XLS_KEYWORDS: 174 | params[key] = self._keywords.pop(key) 175 | return params 176 | 177 | 178 | class XLSInFile(XLSReader): 179 | def __init__(self, file_name, file_type, **keywords): 180 | super().__init__(file_type, filename=file_name, **keywords) 181 | 182 | 183 | class XLSInContent(XLSReader): 184 | def __init__(self, file_content, file_type, **keywords): 185 | super().__init__(file_type, file_contents=file_content, **keywords) 186 | 187 | 188 | class XLSInMemory(XLSReader): 189 | def __init__(self, file_stream, file_type, **keywords): 190 | file_stream.seek(0) 191 | super().__init__( 192 | file_type, file_contents=file_stream.read(), **keywords 193 | ) 194 | 195 | 196 | def xldate_to_python_date(value, date_mode): 197 | """ 198 | convert xl date to python date 199 | """ 200 | date_tuple = xlrd.xldate_as_tuple(value, date_mode) 201 | 202 | ret = None 203 | if date_tuple == (0, 0, 0, 0, 0, 0): 204 | ret = datetime.datetime(1900, 1, 1, 0, 0, 0) 205 | elif date_tuple[0:3] == (0, 0, 0): 206 | ret = datetime.time(date_tuple[3], date_tuple[4], date_tuple[5]) 207 | elif date_tuple[3:6] == (0, 0, 0): 208 | ret = datetime.date(date_tuple[0], date_tuple[1], date_tuple[2]) 209 | else: 210 | ret = datetime.datetime( 211 | date_tuple[0], 212 | date_tuple[1], 213 | date_tuple[2], 214 | date_tuple[3], 215 | date_tuple[4], 216 | date_tuple[5], 217 | ) 218 | return ret 219 | -------------------------------------------------------------------------------- /pyexcel_xls/xlsw.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyexcel_xlsw 3 | ~~~~~~~~~~~~~~~~~~~ 4 | 5 | The lower level xls file format handler using xlwt 6 | 7 | :copyright: (c) 2016-2021 by Onni Software Ltd 8 | :license: New BSD License 9 | """ 10 | 11 | import datetime 12 | 13 | import xlrd 14 | from xlwt import XFStyle, Workbook 15 | from pyexcel_io import constants 16 | from pyexcel_io.plugin_api import IWriter, ISheetWriter 17 | 18 | DEFAULT_DATE_FORMAT = "DD/MM/YY" 19 | DEFAULT_TIME_FORMAT = "HH:MM:SS" 20 | DEFAULT_LONGTIME_FORMAT = "[HH]:MM:SS" 21 | DEFAULT_DATETIME_FORMAT = "%s %s" % (DEFAULT_DATE_FORMAT, DEFAULT_TIME_FORMAT) 22 | EMPTY_SHEET_NOT_ALLOWED = "xlwt does not support a book without any sheets" 23 | 24 | 25 | class XLSheetWriter(ISheetWriter): 26 | """ 27 | xls sheet writer 28 | """ 29 | 30 | def __init__(self, xls_book, xls_sheet, sheet_name): 31 | if sheet_name is None: 32 | sheet_name = constants.DEFAULT_SHEET_NAME 33 | self._xls_book = xls_book 34 | self._xls_sheet = xls_sheet 35 | self._xls_sheet = self._xls_book.add_sheet(sheet_name) 36 | self.current_row = 0 37 | 38 | def write_row(self, array): 39 | """ 40 | write a row into the file 41 | """ 42 | for i, value in enumerate(array): 43 | style = None 44 | tmp_array = [] 45 | if isinstance(value, datetime.datetime): 46 | tmp_array = [ 47 | value.year, 48 | value.month, 49 | value.day, 50 | value.hour, 51 | value.minute, 52 | value.second, 53 | ] 54 | value = xlrd.xldate.xldate_from_datetime_tuple(tmp_array, 0) 55 | style = XFStyle() 56 | style.num_format_str = DEFAULT_DATETIME_FORMAT 57 | elif isinstance(value, datetime.timedelta): 58 | value = value.days + value.seconds / 86_400 59 | style = XFStyle() 60 | style.num_format_str = DEFAULT_LONGTIME_FORMAT 61 | elif isinstance(value, datetime.date): 62 | tmp_array = [value.year, value.month, value.day] 63 | value = xlrd.xldate.xldate_from_date_tuple(tmp_array, 0) 64 | style = XFStyle() 65 | style.num_format_str = DEFAULT_DATE_FORMAT 66 | elif isinstance(value, datetime.time): 67 | tmp_array = [value.hour, value.minute, value.second] 68 | value = xlrd.xldate.xldate_from_time_tuple(tmp_array) 69 | style = XFStyle() 70 | style.num_format_str = DEFAULT_TIME_FORMAT 71 | if style: 72 | self._xls_sheet.write(self.current_row, i, value, style) 73 | else: 74 | self._xls_sheet.write(self.current_row, i, value) 75 | self.current_row += 1 76 | 77 | def close(self): 78 | pass 79 | 80 | 81 | class XLSWriter(IWriter): 82 | """ 83 | xls writer 84 | """ 85 | 86 | def __init__( 87 | self, 88 | file_alike_object, 89 | _, # file_type not used 90 | encoding="ascii", 91 | style_compression=2, 92 | **keywords, 93 | ): 94 | self.file_alike_object = file_alike_object 95 | self.work_book = Workbook( 96 | style_compression=style_compression, encoding=encoding 97 | ) 98 | 99 | def create_sheet(self, name): 100 | return XLSheetWriter(self.work_book, None, name) 101 | 102 | def write(self, incoming_dict): 103 | if incoming_dict: 104 | IWriter.write(self, incoming_dict) 105 | else: 106 | raise NotImplementedError(EMPTY_SHEET_NOT_ALLOWED) 107 | 108 | def close(self): 109 | """ 110 | This call actually save the file 111 | """ 112 | self.work_book.save(self.file_alike_object) 113 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --doctest-glob="*.rst" tests/ README.rst docs/source pyexcel_xls 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyexcel-io>=0.6.2 2 | xlrd 3 | xlwt 4 | -------------------------------------------------------------------------------- /rnd_requirements.txt: -------------------------------------------------------------------------------- 1 | https://github.com/pyexcel/pyexcel-io/archive/dev.zip 2 | 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 | PY2 = sys.version_info[0] == 2 17 | PY26 = PY2 and sys.version_info[1] < 7 18 | PY33 = sys.version_info < (3, 4) 19 | 20 | # Work around mbcs bug in distutils. 21 | # http://bugs.python.org/issue10945 22 | # This work around is only if a project supports Python < 3.4 23 | 24 | # Work around for locale not being set 25 | try: 26 | lc = locale.getlocale() 27 | pf = platform.system() 28 | if pf != "Windows" and lc == (None, None): 29 | locale.setlocale(locale.LC_ALL, "C.UTF-8") 30 | except (ValueError, UnicodeError, locale.Error): 31 | locale.setlocale(locale.LC_ALL, "en_US.UTF-8") 32 | 33 | NAME = "pyexcel-xls" 34 | AUTHOR = "C.W." 35 | VERSION = "0.7.1" 36 | EMAIL = "info@pyexcel.org" 37 | LICENSE = "New BSD" 38 | DESCRIPTION = ( 39 | "A wrapper library to read, manipulate and write data in xls format. It" + 40 | "reads xlsx and xlsm format" 41 | ) 42 | URL = "https://github.com/pyexcel/pyexcel-xls" 43 | DOWNLOAD_URL = "%s/archive/0.7.1.tar.gz" % URL 44 | FILES = ["README.rst","CONTRIBUTORS.rst", "CHANGELOG.rst"] 45 | KEYWORDS = [ 46 | "python", 47 | 'xls', 48 | 'xlsx', 49 | 'xlsm' 50 | ] 51 | 52 | CLASSIFIERS = [ 53 | "Topic :: Software Development :: Libraries", 54 | "Programming Language :: Python", 55 | "Intended Audience :: Developers", 56 | 57 | "Programming Language :: Python :: 3 :: Only", 58 | 59 | 60 | 61 | "Programming Language :: Python :: 3.6", 62 | "Programming Language :: Python :: 3.7", 63 | "Programming Language :: Python :: 3.8", 64 | 65 | 'Programming Language :: Python :: Implementation :: PyPy' 66 | ] 67 | 68 | PYTHON_REQUIRES = ">=3.6" 69 | 70 | INSTALL_REQUIRES = [ 71 | "pyexcel-io>=0.6.2", 72 | "xlrd", 73 | "xlwt", 74 | ] 75 | SETUP_COMMANDS = {} 76 | 77 | PACKAGES = find_packages(exclude=["ez_setup", "examples", "tests", "tests.*"]) 78 | EXTRAS_REQUIRE = { 79 | } 80 | # You do not need to read beyond this line 81 | PUBLISH_COMMAND = "{0} setup.py sdist bdist_wheel upload -r pypi".format(sys.executable) 82 | HERE = os.path.abspath(os.path.dirname(__file__)) 83 | 84 | GS_COMMAND = ("gease pyexcel-xls v0.7.1 " + 85 | "Find 0.7.1 in changelog for more details") 86 | NO_GS_MESSAGE = ("Automatic github release is disabled. " + 87 | "Please install gease to enable it.") 88 | UPLOAD_FAILED_MSG = ( 89 | 'Upload failed. please run "%s" yourself.' % PUBLISH_COMMAND) 90 | 91 | 92 | class PublishCommand(Command): 93 | """Support setup.py upload.""" 94 | 95 | description = "Build and publish the package on github and pypi" 96 | user_options = [] 97 | 98 | @staticmethod 99 | def status(s): 100 | """Prints things in bold.""" 101 | print("\033[1m{0}\033[0m".format(s)) 102 | 103 | def initialize_options(self): 104 | pass 105 | 106 | def finalize_options(self): 107 | pass 108 | 109 | def run(self): 110 | try: 111 | self.status("Removing previous builds...") 112 | rmtree(os.path.join(HERE, "dist")) 113 | rmtree(os.path.join(HERE, "build")) 114 | rmtree(os.path.join(HERE, "pyexcel_xls.egg-info")) 115 | except OSError: 116 | pass 117 | 118 | self.status("Building Source and Wheel (universal) distribution...") 119 | run_status = True 120 | if has_gease(): 121 | run_status = os.system(GS_COMMAND) == 0 122 | else: 123 | self.status(NO_GS_MESSAGE) 124 | if run_status: 125 | if os.system(PUBLISH_COMMAND) != 0: 126 | self.status(UPLOAD_FAILED_MSG) 127 | 128 | sys.exit() 129 | 130 | 131 | SETUP_COMMANDS.update({ 132 | "publish": PublishCommand 133 | }) 134 | 135 | def has_gease(): 136 | """ 137 | test if github release command is installed 138 | 139 | visit http://github.com/moremoban/gease for more info 140 | """ 141 | try: 142 | import gease # noqa 143 | return True 144 | except ImportError: 145 | return False 146 | 147 | 148 | def read_files(*files): 149 | """Read files into setup""" 150 | text = "" 151 | for single_file in files: 152 | content = read(single_file) 153 | text = text + content + "\n" 154 | return text 155 | 156 | 157 | def read(afile): 158 | """Read a file into setup""" 159 | the_relative_file = os.path.join(HERE, afile) 160 | with codecs.open(the_relative_file, "r", "utf-8") as opened_file: 161 | content = filter_out_test_code(opened_file) 162 | content = "".join(list(content)) 163 | return content 164 | 165 | 166 | def filter_out_test_code(file_handle): 167 | found_test_code = False 168 | for line in file_handle.readlines(): 169 | if line.startswith(".. testcode:"): 170 | found_test_code = True 171 | continue 172 | if found_test_code is True: 173 | if line.startswith(" "): 174 | continue 175 | else: 176 | empty_line = line.strip() 177 | if len(empty_line) == 0: 178 | continue 179 | else: 180 | found_test_code = False 181 | yield line 182 | else: 183 | for keyword in ["|version|", "|today|"]: 184 | if keyword in line: 185 | break 186 | else: 187 | yield line 188 | 189 | 190 | if __name__ == "__main__": 191 | setup( 192 | test_suite="tests", 193 | name=NAME, 194 | author=AUTHOR, 195 | version=VERSION, 196 | author_email=EMAIL, 197 | description=DESCRIPTION, 198 | url=URL, 199 | download_url=DOWNLOAD_URL, 200 | long_description=read_files(*FILES), 201 | license=LICENSE, 202 | keywords=KEYWORDS, 203 | python_requires=PYTHON_REQUIRES, 204 | extras_require=EXTRAS_REQUIRE, 205 | tests_require=["nose"], 206 | install_requires=INSTALL_REQUIRES, 207 | packages=PACKAGES, 208 | include_package_data=True, 209 | zip_safe=False, 210 | classifiers=CLASSIFIERS, 211 | cmdclass=SETUP_COMMANDS 212 | ) 213 | -------------------------------------------------------------------------------- /test.bat: -------------------------------------------------------------------------------- 1 | pip freeze 2 | pytest 3 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | pip freeze 3 | python -m pytest 4 | -------------------------------------------------------------------------------- /tests/_compact.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import sys 3 | 4 | if sys.version_info[0] == 2 and sys.version_info[1] < 7: 5 | from ordereddict import OrderedDict 6 | else: 7 | from collections import OrderedDict 8 | -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | import os # noqa 2 | import datetime # noqa 3 | 4 | import pyexcel 5 | 6 | 7 | def create_sample_file1(file): 8 | data = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", 1.1, 1] 9 | table = [] 10 | table.append(data[:4]) 11 | table.append(data[4:8]) 12 | table.append(data[8:12]) 13 | pyexcel.save_as(array=table, dest_file_name=file) 14 | 15 | 16 | class PyexcelHatWriterBase: 17 | """ 18 | Abstract functional test for hat writers 19 | """ 20 | 21 | content = { 22 | "X": [1, 2, 3, 4, 5], 23 | "Y": [6, 7, 8, 9, 10], 24 | "Z": [11, 12, 13, 14, 15], 25 | } 26 | 27 | def test_series_table(self): 28 | pyexcel.save_as(adict=self.content, dest_file_name=self.testfile) 29 | r = pyexcel.get_sheet(file_name=self.testfile, name_columns_by_row=0) 30 | assert r.dict == self.content 31 | 32 | 33 | class PyexcelWriterBase: 34 | """ 35 | Abstract functional test for writers 36 | 37 | testfile and testfile2 have to be initialized before 38 | it is used for testing 39 | """ 40 | 41 | content = [ 42 | [1, 2, 3, 4, 5], 43 | [1, 2, 3, 4, 5], 44 | [1, 2, 3, 4, 5], 45 | [1, 2, 3, 4, 5], 46 | ] 47 | 48 | def _create_a_file(self, file): 49 | pyexcel.save_as(dest_file_name=file, array=self.content) 50 | 51 | def test_write_array(self): 52 | self._create_a_file(self.testfile) 53 | r = pyexcel.get_sheet(file_name=self.testfile) 54 | actual = list(r.rows()) 55 | assert actual == self.content 56 | 57 | 58 | class PyexcelMultipleSheetBase: 59 | def _write_test_file(self, filename): 60 | pyexcel.save_book_as(bookdict=self.content, dest_file_name=filename) 61 | 62 | def _clean_up(self): 63 | if os.path.exists(self.testfile2): 64 | os.unlink(self.testfile2) 65 | if os.path.exists(self.testfile): 66 | os.unlink(self.testfile) 67 | 68 | def test_sheet_names(self): 69 | r = pyexcel.BookReader(self.testfile) 70 | expected = ["Sheet1", "Sheet2", "Sheet3"] 71 | sheet_names = r.sheet_names() 72 | for name in sheet_names: 73 | assert name in expected 74 | 75 | def test_reading_through_sheets(self): 76 | b = pyexcel.BookReader(self.testfile) 77 | data = list(b["Sheet1"].rows()) 78 | expected = [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]] 79 | assert data == expected 80 | data = list(b["Sheet2"].rows()) 81 | expected = [[4, 4, 4, 4], [5, 5, 5, 5], [6, 6, 6, 6]] 82 | assert data == expected 83 | data = list(b["Sheet3"].rows()) 84 | expected = [["X", "Y", "Z"], [1, 4, 7], [2, 5, 8], [3, 6, 9]] 85 | assert data == expected 86 | sheet3 = b["Sheet3"] 87 | sheet3.name_columns_by_row(0) 88 | data = list(b["Sheet3"].rows()) 89 | expected = [[1, 4, 7], [2, 5, 8], [3, 6, 9]] 90 | assert data == expected 91 | -------------------------------------------------------------------------------- /tests/fixtures/complex-merged-cells-sheet.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-xls/11da8e1c199d45b9818fd3a5d7ba4034d93c7bce/tests/fixtures/complex-merged-cells-sheet.xls -------------------------------------------------------------------------------- /tests/fixtures/complex_hidden_sheets.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-xls/11da8e1c199d45b9818fd3a5d7ba4034d93c7bce/tests/fixtures/complex_hidden_sheets.xls -------------------------------------------------------------------------------- /tests/fixtures/date_field.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-xls/11da8e1c199d45b9818fd3a5d7ba4034d93c7bce/tests/fixtures/date_field.xls -------------------------------------------------------------------------------- /tests/fixtures/file_with_an_empty_sheet.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-xls/11da8e1c199d45b9818fd3a5d7ba4034d93c7bce/tests/fixtures/file_with_an_empty_sheet.xls -------------------------------------------------------------------------------- /tests/fixtures/hidden.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-xls/11da8e1c199d45b9818fd3a5d7ba4034d93c7bce/tests/fixtures/hidden.xls -------------------------------------------------------------------------------- /tests/fixtures/hidden_sheets.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-xls/11da8e1c199d45b9818fd3a5d7ba4034d93c7bce/tests/fixtures/hidden_sheets.xls -------------------------------------------------------------------------------- /tests/fixtures/merged-cell-sheet.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-xls/11da8e1c199d45b9818fd3a5d7ba4034d93c7bce/tests/fixtures/merged-cell-sheet.xls -------------------------------------------------------------------------------- /tests/fixtures/merged-sheet-exploration.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-xls/11da8e1c199d45b9818fd3a5d7ba4034d93c7bce/tests/fixtures/merged-sheet-exploration.xls -------------------------------------------------------------------------------- /tests/fixtures/pyexcel_issue_151.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-xls/11da8e1c199d45b9818fd3a5d7ba4034d93c7bce/tests/fixtures/pyexcel_issue_151.xlsx -------------------------------------------------------------------------------- /tests/fixtures/test-date-format.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-xls/11da8e1c199d45b9818fd3a5d7ba4034d93c7bce/tests/fixtures/test-date-format.xls -------------------------------------------------------------------------------- /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 | pyexcel 13 | pytest 14 | -------------------------------------------------------------------------------- /tests/requirements3.txt: -------------------------------------------------------------------------------- 1 | lxml 2 | pyexcel-ods3 3 | pytest 4 | pytest-cov 5 | -------------------------------------------------------------------------------- /tests/test_bug_fixes.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This file keeps all fixes for issues found 4 | 5 | """ 6 | 7 | import os 8 | import datetime 9 | from unittest.mock import MagicMock, patch 10 | 11 | import pytest 12 | import pyexcel as pe 13 | from _compact import OrderedDict 14 | from pyexcel_xls import XLRD_VERSION_2_OR_ABOVE, save_data 15 | from pyexcel_xls.xlsr import xldate_to_python_date 16 | from pyexcel_xls.xlsw import XLSWriter as Writer 17 | 18 | IN_TRAVIS = "TRAVIS" in os.environ 19 | 20 | 21 | def test_pyexcel_issue_5(): 22 | """pyexcel issue #5 23 | 24 | datetime is not properly parsed 25 | """ 26 | s = pe.load(get_fixture("test-date-format.xls")) 27 | assert s[0, 0] == datetime.datetime(2015, 11, 11, 11, 12, 0) 28 | 29 | 30 | def test_pyexcel_xls_issue_2(): 31 | data = OrderedDict() 32 | array = [] 33 | for i in range(4100): 34 | array.append([datetime.datetime.now()]) 35 | data.update({"test": array}) 36 | save_data("test.xls", data) 37 | os.unlink("test.xls") 38 | 39 | 40 | def test_issue_9_hidden_sheet(): 41 | test_file = get_fixture("hidden_sheets.xls") 42 | book_dict = pe.get_book_dict(file_name=test_file) 43 | assert "hidden" not in book_dict 44 | assert book_dict["shown"] == [["A", "B"]] 45 | 46 | 47 | def test_issue_9_hidden_sheet_2(): 48 | test_file = get_fixture("hidden_sheets.xls") 49 | book_dict = pe.get_book_dict(file_name=test_file, skip_hidden_sheets=False) 50 | assert "hidden" in book_dict 51 | assert book_dict["shown"] == [["A", "B"]] 52 | assert book_dict["hidden"] == [["a", "b"]] 53 | 54 | 55 | def test_issue_10_generator_as_content(): 56 | def data_gen(): 57 | def custom_row_renderer(row): 58 | for e in row: 59 | yield e 60 | 61 | for i in range(2): 62 | yield custom_row_renderer([1, 2]) 63 | 64 | save_data("test.xls", {"sheet": data_gen()}) 65 | 66 | 67 | def test_issue_13_empty_file_content(): 68 | with pytest.raises(IOError): 69 | pe.get_sheet(file_content="", file_type="xls") 70 | 71 | 72 | def test_issue_16_file_stream_has_no_getvalue(): 73 | test_file = get_fixture("hidden_sheets.xls") 74 | with open(test_file, "rb") as f: 75 | pe.get_sheet(file_stream=f, file_type="xls") 76 | 77 | 78 | @patch("xlrd.open_workbook") 79 | def test_issue_18_encoding_override_isnt_passed(fake_open): 80 | fake_open.return_value = MagicMock(sheets=MagicMock(return_value=[])) 81 | test_encoding = "utf-32" 82 | from pyexcel_xls.xlsr import XLSInFile 83 | 84 | XLSInFile("fake_file.xls", "xls", encoding_override=test_encoding) 85 | keywords = fake_open.call_args[1] 86 | assert keywords["encoding_override"] == test_encoding 87 | 88 | 89 | def test_issue_20(): 90 | if not IN_TRAVIS: 91 | pytest.skip("Must be in CI for this test") 92 | pe.get_book( 93 | url="https://github.com/pyexcel/pyexcel-xls/raw/master/tests/fixtures/file_with_an_empty_sheet.xls" # noqa: E501 94 | ) 95 | 96 | 97 | def test_issue_151(): 98 | if XLRD_VERSION_2_OR_ABOVE: 99 | pytest.skip("XLRD<2 required for this test") 100 | s = pe.get_sheet( 101 | file_name=get_fixture("pyexcel_issue_151.xlsx"), 102 | skip_hidden_row_and_column=False, 103 | library="pyexcel-xls", 104 | ) 105 | assert "#N/A" == s[0, 0] 106 | 107 | 108 | def test_empty_book_pyexcel_issue_120(): 109 | """ 110 | https://github.com/pyexcel/pyexcel/issues/120 111 | """ 112 | with pytest.raises(NotImplementedError): 113 | writer = Writer("fake.xls", "xls") 114 | writer.write({}) 115 | 116 | 117 | def test_pyexcel_issue_54(): 118 | xlvalue = 41071.0 119 | date = xldate_to_python_date(xlvalue, 1) 120 | assert date == datetime.date(2016, 6, 12) 121 | 122 | 123 | def get_fixture(file_name): 124 | return os.path.join("tests", "fixtures", file_name) 125 | -------------------------------------------------------------------------------- /tests/test_filter.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pyexcel_io import get_data, save_data 4 | 5 | 6 | class TestFilter: 7 | def setup_method(self): 8 | self.test_file = "test_filter.xls" 9 | sample = [ 10 | [1, 21, 31], 11 | [2, 22, 32], 12 | [3, 23, 33], 13 | [4, 24, 34], 14 | [5, 25, 35], 15 | [6, 26, 36], 16 | ] 17 | save_data(self.test_file, sample) 18 | self.sheet_name = "pyexcel_sheet1" 19 | 20 | def test_filter_row(self): 21 | filtered_data = get_data( 22 | self.test_file, start_row=3, library="pyexcel-xls" 23 | ) 24 | expected = [[4, 24, 34], [5, 25, 35], [6, 26, 36]] 25 | assert filtered_data[self.sheet_name] == expected 26 | 27 | def test_filter_row_2(self): 28 | filtered_data = get_data( 29 | self.test_file, start_row=3, row_limit=1, library="pyexcel-xls" 30 | ) 31 | expected = [[4, 24, 34]] 32 | assert filtered_data[self.sheet_name] == expected 33 | 34 | def test_filter_column(self): 35 | filtered_data = get_data( 36 | self.test_file, start_column=1, library="pyexcel-xls" 37 | ) 38 | expected = [[21, 31], [22, 32], [23, 33], [24, 34], [25, 35], [26, 36]] 39 | assert filtered_data[self.sheet_name] == expected 40 | 41 | def test_filter_column_2(self): 42 | filtered_data = get_data( 43 | self.test_file, 44 | start_column=1, 45 | column_limit=1, 46 | library="pyexcel-xls", 47 | ) 48 | expected = [[21], [22], [23], [24], [25], [26]] 49 | assert filtered_data[self.sheet_name] == expected 50 | 51 | def test_filter_both_ways(self): 52 | filtered_data = get_data( 53 | self.test_file, start_column=1, start_row=3, library="pyexcel-xls" 54 | ) 55 | expected = [[24, 34], [25, 35], [26, 36]] 56 | assert filtered_data[self.sheet_name] == expected 57 | 58 | def test_filter_both_ways_2(self): 59 | filtered_data = get_data( 60 | self.test_file, 61 | start_column=1, 62 | column_limit=1, 63 | start_row=3, 64 | row_limit=1, 65 | library="pyexcel-xls", 66 | ) 67 | expected = [[24]] 68 | assert filtered_data[self.sheet_name] == expected 69 | 70 | def teardown_method(self): 71 | os.unlink(self.test_file) 72 | -------------------------------------------------------------------------------- /tests/test_formatters.py: -------------------------------------------------------------------------------- 1 | import os 2 | from textwrap import dedent 3 | 4 | import pyexcel as pe 5 | 6 | 7 | class TestDateFormat: 8 | def test_reading_date_format(self): 9 | """ 10 | date time 11 | 25/12/14 11:11:11 12 | 25/12/14 12:12:12 13 | 01/01/15 13:13:13 14 | 0.0 0.0 15 | """ 16 | import datetime 17 | 18 | r = pe.get_sheet( 19 | file_name=os.path.join("tests", "fixtures", "date_field.xls"), 20 | library="pyexcel-xls", 21 | ) 22 | assert isinstance(r[1, 0], datetime.date) 23 | assert r[1, 0].strftime("%d/%m/%y") == "25/12/14" 24 | assert isinstance(r[1, 1], datetime.time) is True 25 | assert r[1, 1].strftime("%H:%M:%S") == "11:11:11" 26 | assert r[4, 0].strftime("%d/%m/%Y") == "01/01/1900" 27 | assert r[4, 1].strftime("%H:%M:%S") == "00:00:00" 28 | 29 | def test_writing_date_format(self): 30 | import datetime 31 | 32 | excel_filename = "testdateformat.xls" 33 | data = [ 34 | [ 35 | datetime.date(2014, 12, 25), 36 | datetime.time(11, 11, 11), 37 | datetime.datetime(2014, 12, 25, 11, 11, 11), 38 | datetime.timedelta( 39 | days=50, 40 | seconds=27, 41 | microseconds=10, 42 | milliseconds=29000, 43 | minutes=5, 44 | hours=8, 45 | weeks=2, 46 | ), 47 | ] 48 | ] 49 | pe.save_as(dest_file_name=excel_filename, array=data) 50 | r = pe.get_sheet(file_name=excel_filename, library="pyexcel-xls") 51 | assert isinstance(r[0, 0], datetime.date) is True 52 | assert r[0, 0].strftime("%d/%m/%y") == "25/12/14" 53 | assert isinstance(r[0, 1], datetime.time) is True 54 | assert r[0, 1].strftime("%H:%M:%S") == "11:11:11" 55 | assert isinstance(r[0, 2], datetime.date) is True 56 | assert r[0, 2].strftime("%d/%m/%y %H:%M:%S") == "25/12/14 11:11:11" 57 | assert isinstance(r[0, 3], datetime.datetime) 58 | assert r[0, 3].strftime("%D-%H:%M:%S") == "03/04/00-08:05:56" 59 | os.unlink(excel_filename) 60 | 61 | 62 | class TestAutoDetectInt: 63 | def setup_method(self): 64 | self.content = [[1, 2, 3.1]] 65 | self.test_file = "test_auto_detect_init.xls" 66 | pe.save_as(array=self.content, dest_file_name=self.test_file) 67 | 68 | def test_auto_detect_int(self): 69 | sheet = pe.get_sheet(file_name=self.test_file, library="pyexcel-xls") 70 | expected = dedent( 71 | """ 72 | pyexcel_sheet1: 73 | +---+---+-----+ 74 | | 1 | 2 | 3.1 | 75 | +---+---+-----+""" 76 | ).strip() 77 | assert str(sheet) == expected 78 | 79 | def test_get_book_auto_detect_int(self): 80 | book = pe.get_book(file_name=self.test_file, library="pyexcel-xls") 81 | expected = dedent( 82 | """ 83 | pyexcel_sheet1: 84 | +---+---+-----+ 85 | | 1 | 2 | 3.1 | 86 | +---+---+-----+""" 87 | ).strip() 88 | assert str(book) == expected 89 | 90 | def test_auto_detect_int_false(self): 91 | sheet = pe.get_sheet( 92 | file_name=self.test_file, 93 | auto_detect_int=False, 94 | library="pyexcel-xls", 95 | ) 96 | expected = dedent( 97 | """ 98 | pyexcel_sheet1: 99 | +-----+-----+-----+ 100 | | 1.0 | 2.0 | 3.1 | 101 | +-----+-----+-----+""" 102 | ).strip() 103 | assert str(sheet) == expected 104 | 105 | def test_get_book_auto_detect_int_false(self): 106 | book = pe.get_book( 107 | file_name=self.test_file, 108 | auto_detect_int=False, 109 | library="pyexcel-xls", 110 | ) 111 | expected = dedent( 112 | """ 113 | pyexcel_sheet1: 114 | +-----+-----+-----+ 115 | | 1.0 | 2.0 | 3.1 | 116 | +-----+-----+-----+""" 117 | ).strip() 118 | assert str(book) == expected 119 | 120 | def teardown_method(self): 121 | os.unlink(self.test_file) 122 | -------------------------------------------------------------------------------- /tests/test_hidden.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pyexcel_xls import get_data 4 | 5 | 6 | def test_simple_hidden_sheets(): 7 | data = get_data( 8 | os.path.join("tests", "fixtures", "hidden.xls"), 9 | skip_hidden_row_and_column=True, 10 | ) 11 | expected = [[1, 3], [7, 9]] 12 | assert data["Sheet1"] == expected 13 | 14 | 15 | def test_complex_hidden_sheets(): 16 | data = get_data( 17 | os.path.join("tests", "fixtures", "complex_hidden_sheets.xls"), 18 | skip_hidden_row_and_column=True, 19 | ) 20 | expected = [[1, 3, 5, 7, 9], [31, 33, 35, 37, 39], [61, 63, 65, 67]] 21 | assert data["Sheet1"] == expected 22 | -------------------------------------------------------------------------------- /tests/test_merged_cells.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pyexcel_xls import get_data 4 | from pyexcel_xls.xlsr import MergedCell 5 | 6 | 7 | def test_merged_cells(): 8 | data = get_data( 9 | get_fixture("merged-cell-sheet.xls"), 10 | detect_merged_cells=True, 11 | library="pyexcel-xls", 12 | ) 13 | expected = [[1, 2, 3], [1, 5, 6], [1, 8, 9], [10, 11, 11]] 14 | assert data["Sheet1"] == expected 15 | 16 | 17 | def test_complex_merged_cells(): 18 | data = get_data( 19 | get_fixture("complex-merged-cells-sheet.xls"), 20 | detect_merged_cells=True, 21 | library="pyexcel-xls", 22 | ) 23 | expected = [ 24 | [1, 1, 2, 3, 15, 16, 22, 22, 24, 24], 25 | [1, 1, 4, 5, 15, 17, 22, 22, 24, 24], 26 | [6, 7, 8, 9, 15, 18, 22, 22, 24, 24], 27 | [10, 11, 11, 12, 19, 19, 23, 23, 24, 24], 28 | [13, 11, 11, 14, 20, 20, 23, 23, 24, 24], 29 | [21, 21, 21, 21, 21, 21, 23, 23, 24, 24], 30 | [25, 25, 25, 25, 25, 25, 25, 25, 25, 25], 31 | [25, 25, 25, 25, 25, 25, 25, 25, 25, 25], 32 | ] 33 | assert data["Sheet1"] == expected 34 | 35 | 36 | def test_exploration(): 37 | data = get_data( 38 | get_fixture("merged-sheet-exploration.xls"), 39 | detect_merged_cells=True, 40 | library="pyexcel-xls", 41 | ) 42 | expected_sheet1 = [ 43 | [1, 1, 1, 1, 1, 1], 44 | [2], 45 | [2], 46 | [2], 47 | [2], 48 | [2], 49 | [2], 50 | [2], 51 | [2], 52 | [2], 53 | ] 54 | assert data["Sheet1"] == expected_sheet1 55 | expected_sheet2 = [[3], [3], [3], [3, 4, 4, 4, 4, 4, 4], [3], [3], [3]] 56 | assert data["Sheet2"] == expected_sheet2 57 | expected_sheet3 = [ 58 | ["", "", "", "", "", 2, 2, 2], 59 | [], 60 | [], 61 | [], 62 | ["", "", "", 5], 63 | ["", "", "", 5], 64 | ["", "", "", 5], 65 | ["", "", "", 5], 66 | ["", "", "", 5], 67 | ] 68 | assert data["Sheet3"] == expected_sheet3 69 | 70 | 71 | def test_merged_cell_class(): 72 | test_dict = {} 73 | merged_cell = MergedCell(1, 4, 1, 4) 74 | merged_cell.register_cells(test_dict) 75 | keys = sorted(list(test_dict.keys())) 76 | expected = ["1-1", "1-2", "1-3", "2-1", "2-2", "2-3", "3-1", "3-2", "3-3"] 77 | assert keys == expected 78 | assert merged_cell == test_dict["3-1"] 79 | 80 | 81 | def get_fixture(file_name): 82 | return os.path.join("tests", "fixtures", file_name) 83 | -------------------------------------------------------------------------------- /tests/test_multiple_sheets.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import pytest 5 | import pyexcel 6 | from base import PyexcelMultipleSheetBase 7 | 8 | if sys.version_info[0] == 2 and sys.version_info[1] < 7: 9 | from ordereddict import OrderedDict 10 | else: 11 | from collections import OrderedDict 12 | 13 | 14 | class TestXlsNxlsMultipleSheets(PyexcelMultipleSheetBase): 15 | def setup_method(self): 16 | self.testfile = "multiple1.xls" 17 | self.testfile2 = "multiple1.xls" 18 | self.content = _produce_ordered_dict() 19 | self._write_test_file(self.testfile) 20 | 21 | def teardown_method(self): 22 | self._clean_up() 23 | 24 | 25 | class TestAddBooks: 26 | def _write_test_file(self, file): 27 | """ 28 | Make a test file as: 29 | 30 | 1,1,1,1 31 | 2,2,2,2 32 | 3,3,3,3 33 | """ 34 | self.rows = 3 35 | pyexcel.save_book_as(bookdict=self.content, dest_file_name=file) 36 | 37 | def setup_method(self): 38 | self.testfile = "multiple1.xls" 39 | self.testfile2 = "multiple2.xls" 40 | self.testfile3 = "multiple3.xls" 41 | self.content = _produce_ordered_dict() 42 | self._write_test_file(self.testfile) 43 | self._write_test_file(self.testfile2) 44 | 45 | def test_load_a_single_sheet(self): 46 | b1 = pyexcel.get_book(file_name=self.testfile, sheet_name="Sheet1") 47 | assert len(b1.sheet_names()) == 1 48 | assert b1["Sheet1"].to_array() == self.content["Sheet1"] 49 | 50 | def test_load_a_single_sheet2(self): 51 | b1 = pyexcel.load_book(self.testfile, sheet_index=0) 52 | assert len(b1.sheet_names()) == 1 53 | assert b1["Sheet1"].to_array() == self.content["Sheet1"] 54 | 55 | def test_load_a_single_sheet3(self): 56 | with pytest.raises(IndexError): 57 | pyexcel.get_book(file_name=self.testfile, sheet_index=10000) 58 | 59 | def test_load_a_single_sheet4(self): 60 | with pytest.raises(ValueError): 61 | pyexcel.get_book(file_name=self.testfile, sheet_name="Not exist") 62 | 63 | def test_delete_sheets(self): 64 | b1 = pyexcel.load_book(self.testfile) 65 | assert len(b1.sheet_names()) == 3 66 | del b1["Sheet1"] 67 | assert len(b1.sheet_names()) == 2 68 | try: 69 | del b1["Sheet1"] 70 | assert 1 == 2 71 | except KeyError: 72 | assert 1 == 1 73 | del b1[1] 74 | assert len(b1.sheet_names()) == 1 75 | try: 76 | del b1[1] 77 | assert 1 == 2 78 | except IndexError: 79 | assert 1 == 1 80 | 81 | def test_delete_sheets2(self): 82 | """repetitively delete first sheet""" 83 | b1 = pyexcel.load_book(self.testfile) 84 | del b1[0] 85 | assert len(b1.sheet_names()) == 2 86 | del b1[0] 87 | assert len(b1.sheet_names()) == 1 88 | del b1[0] 89 | assert len(b1.sheet_names()) == 0 90 | 91 | def test_add_book1(self): 92 | """ 93 | test this scenario: book3 = book1 + book2 94 | """ 95 | b1 = pyexcel.get_book(file_name=self.testfile) 96 | b2 = pyexcel.get_book(file_name=self.testfile2) 97 | b3 = b1 + b2 98 | content = b3.dict 99 | sheet_names = content.keys() 100 | assert len(sheet_names) == 6 101 | for name in sheet_names: 102 | if "Sheet3" in name: 103 | assert content[name] == self.content["Sheet3"] 104 | elif "Sheet2" in name: 105 | assert content[name] == self.content["Sheet2"] 106 | elif "Sheet1" in name: 107 | assert content[name] == self.content["Sheet1"] 108 | 109 | def test_add_book1_in_place(self): 110 | """ 111 | test this scenario: book1 += book2 112 | """ 113 | b1 = pyexcel.BookReader(self.testfile) 114 | b2 = pyexcel.BookReader(self.testfile2) 115 | b1 += b2 116 | content = b1.dict 117 | sheet_names = content.keys() 118 | assert len(sheet_names) == 6 119 | for name in sheet_names: 120 | if "Sheet3" in name: 121 | assert content[name] == self.content["Sheet3"] 122 | elif "Sheet2" in name: 123 | assert content[name] == self.content["Sheet2"] 124 | elif "Sheet1" in name: 125 | assert content[name] == self.content["Sheet1"] 126 | 127 | def test_add_book2(self): 128 | """ 129 | test this scenario: book3 = book1 + sheet3 130 | """ 131 | b1 = pyexcel.BookReader(self.testfile) 132 | b2 = pyexcel.BookReader(self.testfile2) 133 | b3 = b1 + b2["Sheet3"] 134 | content = b3.dict 135 | sheet_names = content.keys() 136 | assert len(sheet_names) == 4 137 | for name in sheet_names: 138 | if "Sheet3" in name: 139 | assert content[name] == self.content["Sheet3"] 140 | elif "Sheet2" in name: 141 | assert content[name] == self.content["Sheet2"] 142 | elif "Sheet1" in name: 143 | assert content[name] == self.content["Sheet1"] 144 | 145 | def test_add_book2_in_place(self): 146 | """ 147 | test this scenario: book3 = book1 + sheet3 148 | """ 149 | b1 = pyexcel.BookReader(self.testfile) 150 | b2 = pyexcel.BookReader(self.testfile2) 151 | b1 += b2["Sheet3"] 152 | content = b1.dict 153 | sheet_names = content.keys() 154 | assert len(sheet_names) == 4 155 | for name in sheet_names: 156 | if "Sheet3" in name: 157 | assert content[name] == self.content["Sheet3"] 158 | elif "Sheet2" in name: 159 | assert content[name] == self.content["Sheet2"] 160 | elif "Sheet1" in name: 161 | assert content[name] == self.content["Sheet1"] 162 | 163 | def test_add_book3(self): 164 | """ 165 | test this scenario: book3 = sheet1 + sheet2 166 | """ 167 | b1 = pyexcel.BookReader(self.testfile) 168 | b2 = pyexcel.BookReader(self.testfile2) 169 | b3 = b1["Sheet1"] + b2["Sheet3"] 170 | content = b3.dict 171 | sheet_names = content.keys() 172 | assert len(sheet_names) == 2 173 | assert content["Sheet3"] == self.content["Sheet3"] 174 | assert content["Sheet1"] == self.content["Sheet1"] 175 | 176 | def test_add_book4(self): 177 | """ 178 | test this scenario: book3 = sheet1 + book 179 | """ 180 | b1 = pyexcel.BookReader(self.testfile) 181 | b2 = pyexcel.BookReader(self.testfile2) 182 | b3 = b1["Sheet1"] + b2 183 | content = b3.dict 184 | sheet_names = content.keys() 185 | assert len(sheet_names) == 4 186 | for name in sheet_names: 187 | if "Sheet3" in name: 188 | assert content[name] == self.content["Sheet3"] 189 | elif "Sheet2" in name: 190 | assert content[name] == self.content["Sheet2"] 191 | elif "Sheet1" in name: 192 | assert content[name] == self.content["Sheet1"] 193 | 194 | def test_add_book_error(self): 195 | """ 196 | test this scenario: book3 = sheet1 + book 197 | """ 198 | b1 = pyexcel.BookReader(self.testfile) 199 | try: 200 | b1 + 12 201 | assert 1 == 2 202 | except TypeError: 203 | assert 1 == 1 204 | try: 205 | b1 += 12 206 | assert 1 == 2 207 | except TypeError: 208 | assert 1 == 1 209 | 210 | def teardown_method(self): 211 | if os.path.exists(self.testfile): 212 | os.unlink(self.testfile) 213 | if os.path.exists(self.testfile2): 214 | os.unlink(self.testfile2) 215 | 216 | 217 | class TestMultiSheetReader: 218 | def setup_method(self): 219 | self.testfile = "file_with_an_empty_sheet.xls" 220 | 221 | def test_reader_with_correct_sheets(self): 222 | r = pyexcel.BookReader( 223 | os.path.join("tests", "fixtures", self.testfile) 224 | ) 225 | assert r.number_of_sheets() == 3 226 | 227 | 228 | def _produce_ordered_dict(): 229 | data_dict = OrderedDict() 230 | data_dict.update({"Sheet1": [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]]}) 231 | data_dict.update({"Sheet2": [[4, 4, 4, 4], [5, 5, 5, 5], [6, 6, 6, 6]]}) 232 | data_dict.update( 233 | {"Sheet3": [["X", "Y", "Z"], [1, 4, 7], [2, 5, 8], [3, 6, 9]]} 234 | ) 235 | return data_dict 236 | -------------------------------------------------------------------------------- /tests/test_stringio.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pyexcel 4 | from base import create_sample_file1 5 | 6 | 7 | class TestStringIO: 8 | def test_xls_stringio(self): 9 | testfile = "cute.xls" 10 | create_sample_file1(testfile) 11 | with open(testfile, "rb") as f: 12 | content = f.read() 13 | r = pyexcel.get_sheet( 14 | file_type="xls", file_content=content, library="pyexcel-xls" 15 | ) 16 | result = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", 1.1, 1] 17 | actual = list(r.enumerate()) 18 | assert result == actual 19 | if os.path.exists(testfile): 20 | os.unlink(testfile) 21 | 22 | def test_xls_output_stringio(self): 23 | data = [[1, 2, 3], [4, 5, 6]] 24 | io = pyexcel.save_as(dest_file_type="xls", array=data) 25 | r = pyexcel.get_sheet( 26 | file_type="xls", file_content=io.getvalue(), library="pyexcel-xls" 27 | ) 28 | result = [1, 2, 3, 4, 5, 6] 29 | actual = list(r.enumerate()) 30 | assert result == actual 31 | -------------------------------------------------------------------------------- /tests/test_writer.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from base import PyexcelWriterBase, PyexcelHatWriterBase 4 | from pyexcel_xls import get_data 5 | from pyexcel_xls.xlsw import XLSWriter as Writer 6 | 7 | 8 | class TestNativeXLSWriter: 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.xls" 16 | writer = Writer(self.testfile, "xls") 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_method(self): 25 | if os.path.exists(self.testfile): 26 | os.unlink(self.testfile) 27 | 28 | 29 | class TestxlsnCSVWriter(PyexcelWriterBase): 30 | def setup_method(self): 31 | self.testfile = "test.xls" 32 | self.testfile2 = "test.csv" 33 | 34 | def teardown_method(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 TestxlsHatWriter(PyexcelHatWriterBase): 42 | def setup_method(self): 43 | self.testfile = "test.xls" 44 | 45 | def teardown_method(self): 46 | if os.path.exists(self.testfile): 47 | os.unlink(self.testfile) 48 | --------------------------------------------------------------------------------