├── .bumpversion.cfg ├── .cookiecutterrc ├── .coveragerc ├── .editorconfig ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── CHANGELOG.rst ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── appveyor.yml ├── ci ├── appveyor-bootstrap.py ├── appveyor-download.py ├── appveyor-with-compiler.cmd ├── bootstrap.py └── templates │ ├── .travis.yml │ ├── appveyor.yml │ └── tox.ini ├── docs ├── authors.rst ├── changelog.rst ├── conf.py ├── contributing.rst ├── index.rst ├── installation.rst ├── readme.rst ├── reference │ ├── conditions.rst │ └── index.rst ├── requirements.txt ├── spelling_wordlist.txt └── usage.rst ├── example ├── example.py └── some.log ├── setup.cfg ├── setup.py ├── src └── conditions │ ├── __init__.py │ ├── exceptions.py │ ├── handlers.py │ ├── restarts.py │ └── signals.py ├── tests └── test_conditions.py └── tox.ini /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | 8 | [bumpversion:file:docs/conf.py] 9 | 10 | [bumpversion:file:src/conditions/__init__.py] 11 | 12 | -------------------------------------------------------------------------------- /.cookiecutterrc: -------------------------------------------------------------------------------- 1 | # This file exists so you can easily regenerate your project. 2 | # 3 | # `cookiepatcher` is a convenient shim around `cookiecutter` 4 | # for regenerating projects (it will generate a .cookiecutterrc 5 | # automatically for any template). To use it: 6 | # 7 | # pip install cookiepatcher 8 | # cookiepatcher gh:ionelmc/cookiecutter-pylibrary project-path 9 | # 10 | # See: 11 | # https://pypi.python.org/pypi/cookiecutter 12 | # 13 | # Alternatively, you can run: 14 | # 15 | # cookiecutter --overwrite-if-exists --config-file=project-path/.cookiecutterrc gh:ionelmc/cookiecutter-pylibrary 16 | 17 | default_context: 18 | 19 | appveyor: 'no' 20 | c_extension_cython: 'no' 21 | c_extension_optional: 'no' 22 | c_extension_support: 'no' 23 | codacy: 'no' 24 | codeclimate: 'no' 25 | codecov: 'yes' 26 | command_line_interface: 'no' 27 | command_line_interface_bin_name: 'nameless' 28 | coveralls: 'no' 29 | distribution_name: 'conditions' 30 | email: 'svetlyak.40wt@gmail.com' 31 | full_name: 'Alexander Artemenko' 32 | github_username: 'svetlyak40wt' 33 | landscape: 'no' 34 | package_name: 'conditions' 35 | project_name: 'cl-conditions' 36 | project_short_description: "Implementation of the Common Lisp's conditions system in Python." 37 | release_date: 'today' 38 | repo_name: 'python-cl-conditions' 39 | requiresio: 'yes' 40 | scrutinizer: 'no' 41 | sphinx_doctest: 'no' 42 | sphinx_theme: 'sphinx-py3doc-enhanced-theme' 43 | test_matrix_configurator: 'yes' 44 | test_matrix_separate_coverage: 'no' 45 | test_runner: 'pytest' 46 | travis: 'yes' 47 | version: '0.1.0' 48 | website: 'http://dev.svetlyak.ru' 49 | year: 'now' 50 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [paths] 2 | source = src 3 | 4 | [run] 5 | branch = True 6 | source = src 7 | parallel = true 8 | 9 | [report] 10 | show_missing = true 11 | precision = 2 12 | omit = *migrations* 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | charset = utf-8 11 | 12 | [*.{bat,cmd,ps1}] 13 | end_of_line = crlf 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | .eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | venv*/ 22 | pyvenv*/ 23 | 24 | # Installer logs 25 | pip-log.txt 26 | 27 | # Unit test / coverage reports 28 | .coverage 29 | .tox 30 | .coverage.* 31 | nosetests.xml 32 | coverage.xml 33 | htmlcov 34 | 35 | # Translations 36 | *.mo 37 | 38 | # Mr Developer 39 | .mr.developer.cfg 40 | .project 41 | .pydevproject 42 | .idea 43 | *.iml 44 | *.komodoproject 45 | 46 | # Complexity 47 | output/*.html 48 | output/*/index.html 49 | 50 | # Sphinx 51 | docs/_build 52 | 53 | .DS_Store 54 | *~ 55 | .*.sw[po] 56 | .build 57 | .ve 58 | .env 59 | .cache 60 | .pytest 61 | .bootstrap 62 | .appveyor.token 63 | *.bak 64 | /env 65 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: '3.5' 3 | sudo: false 4 | env: 5 | global: 6 | - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so 7 | - SEGFAULT_SIGNALS=all 8 | matrix: 9 | - TOXENV=check 10 | - TOXENV=docs 11 | 12 | - TOXENV=2.7-cover,codecov 13 | - TOXENV=2.7-nocov 14 | - TOXENV=3.3-cover,codecov 15 | - TOXENV=3.3-nocov 16 | - TOXENV=3.4-cover,codecov 17 | - TOXENV=3.4-nocov 18 | - TOXENV=3.5-cover,codecov 19 | - TOXENV=3.5-nocov 20 | - TOXENV=pypy-cover,codecov 21 | - TOXENV=pypy-nocov 22 | before_install: 23 | - python --version 24 | - uname -a 25 | - lsb_release -a 26 | install: 27 | - pip install tox 28 | - virtualenv --version 29 | - easy_install --version 30 | - pip --version 31 | - tox --version 32 | script: 33 | - tox -v 34 | after_failure: 35 | - more .tox/log/* | cat 36 | - more .tox/*/log/* | cat 37 | before_cache: 38 | - rm -rf $HOME/.cache/pip/log 39 | cache: 40 | directories: 41 | - $HOME/.cache/pip 42 | notifications: 43 | email: 44 | on_success: never 45 | on_failure: always 46 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | 2 | Authors 3 | ======= 4 | 5 | * Alexander Artemenko - http://dev.svetlyak.ru 6 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.2.0 (2016-04-05) 5 | ------------------ 6 | 7 | * Added context manager ``restarts`` and manager ``restart`` now gets 8 | only a function and returns a function like to call code 9 | to be restarted. 10 | 11 | 0.1.0 (2016-03-29) 12 | ------------------ 13 | 14 | * First release on PyPI. 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | Bug reports 9 | =========== 10 | 11 | When `reporting a bug `_ please include: 12 | 13 | * Your operating system name and version. 14 | * Any details about your local setup that might be helpful in troubleshooting. 15 | * Detailed steps to reproduce the bug. 16 | 17 | Documentation improvements 18 | ========================== 19 | 20 | cl-conditions could always use more documentation, whether as part of the 21 | official cl-conditions docs, in docstrings, or even on the web in blog posts, 22 | articles, and such. 23 | 24 | Feature requests and feedback 25 | ============================= 26 | 27 | The best way to send feedback is to file an issue at https://github.com/svetlyak40wt/python-cl-conditions/issues. 28 | 29 | If you are proposing a feature: 30 | 31 | * Explain in detail how it would work. 32 | * Keep the scope as narrow as possible, to make it easier to implement. 33 | * Remember that this is a volunteer-driven project, and that code contributions are welcome :) 34 | 35 | Development 36 | =========== 37 | 38 | To set up `python-cl-conditions` for local development: 39 | 40 | 1. Fork `python-cl-conditions `_ 41 | (look for the "Fork" button). 42 | 2. Clone your fork locally:: 43 | 44 | git clone git@github.com:your_name_here/python-cl-conditions.git 45 | 46 | 3. Create a branch for local development:: 47 | 48 | git checkout -b name-of-your-bugfix-or-feature 49 | 50 | Now you can make your changes locally. 51 | 52 | 4. When you're done making changes, run all the checks, doc builder and spell checker with `tox `_ one command:: 53 | 54 | tox 55 | 56 | 5. Commit your changes and push your branch to GitHub:: 57 | 58 | git add . 59 | git commit -m "Your detailed description of your changes." 60 | git push origin name-of-your-bugfix-or-feature 61 | 62 | 6. Submit a pull request through the GitHub website. 63 | 64 | Pull Request Guidelines 65 | ----------------------- 66 | 67 | If you need some code review or feedback while you're developing the code just make the pull request. 68 | 69 | For merging, you should: 70 | 71 | 1. Include passing tests (run ``tox``) [1]_. 72 | 2. Update documentation when there's new API, functionality etc. 73 | 3. Add a note to ``CHANGELOG.rst`` about the changes. 74 | 4. Add yourself to ``AUTHORS.rst``. 75 | 76 | .. [1] If you don't have all the necessary python versions available locally you can rely on Travis - it will 77 | `run the tests `_ for each change you add in the pull request. 78 | 79 | It will be slower though ... 80 | 81 | Tips 82 | ---- 83 | 84 | To run a subset of tests:: 85 | 86 | tox -e envname -- py.test -k test_myfeature 87 | 88 | To run all the test environments in *parallel* (you need to ``pip install detox``):: 89 | 90 | detox 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Alexander Artemenko 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 8 | disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 16 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 17 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 18 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 19 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft docs 2 | graft examples 3 | graft src 4 | graft ci 5 | graft tests 6 | 7 | include .bumpversion.cfg 8 | include .coveragerc 9 | include .cookiecutterrc 10 | include .editorconfig 11 | include .isort.cfg 12 | 13 | include AUTHORS.rst 14 | include CHANGELOG.rst 15 | include CONTRIBUTING.rst 16 | include LICENSE 17 | include README.rst 18 | 19 | include tox.ini .travis.yml appveyor.yml 20 | 21 | recursive-include example *.log 22 | recursive-include example *.py 23 | 24 | global-exclude *.py[cod] __pycache__ *.so *.dylib 25 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Overview 3 | ======== 4 | 5 | Implementation of the Common Lisp's conditions system in Python. 6 | 7 | Free software: BSD license. 8 | 9 | .. start-badges 10 | 11 | .. list-table:: 12 | :stub-columns: 1 13 | 14 | * - docs 15 | - |docs| 16 | * - tests 17 | - | |travis| |requires| 18 | | |codecov| 19 | * - package 20 | - |version| |downloads| |wheel| |supported-versions| |supported-implementations| 21 | 22 | .. |docs| image:: https://readthedocs.org/projects/python-cl-conditions/badge/?style=flat 23 | :target: https://readthedocs.org/projects/python-cl-conditions 24 | :alt: Documentation Status 25 | 26 | .. |travis| image:: https://travis-ci.org/svetlyak40wt/python-cl-conditions.svg?branch=master 27 | :alt: Travis-CI Build Status 28 | :target: https://travis-ci.org/svetlyak40wt/python-cl-conditions 29 | 30 | .. |requires| image:: https://requires.io/github/svetlyak40wt/python-cl-conditions/requirements.svg?branch=master 31 | :alt: Requirements Status 32 | :target: https://requires.io/github/svetlyak40wt/python-cl-conditions/requirements/?branch=master 33 | 34 | .. |codecov| image:: https://codecov.io/github/svetlyak40wt/python-cl-conditions/coverage.svg?branch=master 35 | :alt: Coverage Status 36 | :target: https://codecov.io/github/svetlyak40wt/python-cl-conditions 37 | 38 | .. |version| image:: https://img.shields.io/pypi/v/conditions.svg?style=flat 39 | :alt: PyPI Package latest release 40 | :target: https://pypi.python.org/pypi/conditions 41 | 42 | .. |downloads| image:: https://img.shields.io/pypi/dm/conditions.svg?style=flat 43 | :alt: PyPI Package monthly downloads 44 | :target: https://pypi.python.org/pypi/conditions 45 | 46 | .. |wheel| image:: https://img.shields.io/pypi/wheel/conditions.svg?style=flat 47 | :alt: PyPI Wheel 48 | :target: https://pypi.python.org/pypi/conditions 49 | 50 | .. |supported-versions| image:: https://img.shields.io/pypi/pyversions/conditions.svg?style=flat 51 | :alt: Supported versions 52 | :target: https://pypi.python.org/pypi/conditions 53 | 54 | .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/conditions.svg?style=flat 55 | :alt: Supported implementations 56 | :target: https://pypi.python.org/pypi/conditions 57 | 58 | 59 | .. end-badges 60 | 61 | Rationale 62 | ========= 63 | 64 | Common Lisp (CL) has a very rich condition system. Conditions in CL are a kind 65 | of signals, used not only for exception handling but also in some other patterns. 66 | There is a very good explanation of how they work – a chapter from the book 67 | Practical Common Lisp by Peter Seibel: 68 | `Beyond Exception Handling: Conditions and Restarts`_. 69 | 70 | Python's exceptions cover only one scenario from this book, but Common Lisp's conditions 71 | allows more interesting usage, particularly "restarts". A restart is a way to continue 72 | code execution after an exception was signaled, without unwinding the call stack. 73 | I'll repeat: without unwinding the call stack. 74 | 75 | Moreover, conditions allow the author of the library to define various cases to be 76 | chosen to handle the exception. 77 | 78 | .. _`Beyond Exception Handling: Conditions and Restarts`: http://www.gigamonkeys.com/book/beyond-exception-handling-conditions-and-restarts.html 79 | 80 | Example 81 | ------- 82 | 83 | Here is an example from the book, but implemented in Python using the `conditions`_ library:: 84 | 85 | def parse_log_entry(text): 86 | """This function does all real job on log line parsing. 87 | it setup two cases for restart parsing if a line 88 | with wrong format was found. 89 | 90 | Restarts: 91 | - use_value: just retuns an object it was passed. This can 92 | be any value. 93 | - reparse: calls `parse_log_entry` again with other text value. 94 | Beware, this call can lead to infinite recursion. 95 | """ 96 | text = text.strip() 97 | 98 | if well_formed_log_entry_p(text): 99 | return LogEntry(text) 100 | else: 101 | def use_value(obj): 102 | return obj 103 | def reparse(text): 104 | return parse_log_entry(text) 105 | 106 | with restarts(use_value, 107 | reparse) as call: 108 | return call(signal, MalformedLogEntryError(text)) 109 | 110 | 111 | def log_analyzer(path): 112 | """This procedure replaces every line which can't be parsed 113 | with special object MalformedLogEntry. 114 | """ 115 | with handle(MalformedLogEntryError, 116 | lambda (c): 117 | invoke_restart('use_value', 118 | MalformedLogEntry(c.text))): 119 | for filename in find_all_logs(path): 120 | analyze_log(filename) 121 | 122 | 123 | def log_analyzer2(path): 124 | """This procedure considers every line which can't be parsed 125 | as a line with ERROR level. 126 | """ 127 | with handle(MalformedLogEntryError, 128 | lambda (c): 129 | invoke_restart('reparse', 130 | 'ERROR: ' + c.text)): 131 | for filename in find_all_logs(path): 132 | analyze_log(filename) 133 | 134 | What we have here is a function ``parse_log_entry``, which defines two ways of 135 | handling an exceptional situation: ``use_value`` and ``reparse``. But the 136 | decision of how bad lines should be handled is made by the high level function 137 | ``log_analyser``. The original book chapter has only one version of 138 | ``log_analyser``, but I've added an alternative ``log_analyser2`` to illustrate 139 | why restarts are a useful pattern. The value of this pattern is in the ability 140 | to move the decision making code from low level library functions into the 141 | higher level business logic. 142 | 143 | The full version of this example can be found in `example/example.py`_. 144 | 145 | .. _conditions: https://github.com/svetlyak40wt/python-cl-conditions 146 | .. _`example/example.py`: https://github.com/svetlyak40wt/python-cl-conditions/blob/master/example/example.py 147 | 148 | Installation 149 | ============ 150 | 151 | :: 152 | 153 | pip install conditions 154 | 155 | Documentation 156 | ============= 157 | 158 | https://python-cl-conditions.readthedocs.org/ 159 | 160 | Development 161 | =========== 162 | 163 | To run all tests run:: 164 | 165 | tox 166 | 167 | Note, to combine the coverage data from all the tox environments run: 168 | 169 | .. list-table:: 170 | :widths: 10 90 171 | :stub-columns: 1 172 | 173 | - - Windows 174 | - :: 175 | 176 | set PYTEST_ADDOPTS=--cov-append 177 | tox 178 | 179 | - - Other 180 | - :: 181 | 182 | PYTEST_ADDOPTS=--cov-append tox 183 | 184 | 185 | Related projects 186 | ================ 187 | 188 | There is also the withrestart_ Python library, created with 189 | the same intent as conditions_. But it has a clunky API and 190 | a weird name, and seems abandoned since 2010. 191 | 192 | .. _withrestart: https://pypi.python.org/pypi/withrestart 193 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{branch}-{build}' 2 | build: off 3 | cache: 4 | - '%LOCALAPPDATA%\pip\Cache' 5 | environment: 6 | global: 7 | WITH_COMPILER: 'cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd' 8 | matrix: 9 | - TOXENV: check 10 | PYTHON_HOME: C:\Python27 11 | PYTHON_VERSION: '2.7' 12 | PYTHON_ARCH: '32' 13 | 14 | - TOXENV: '2.7-cover,codecov' 15 | TOXPYTHON: C:\Python27\python.exe 16 | PYTHON_HOME: C:\Python27 17 | PYTHON_VERSION: '2.7' 18 | PYTHON_ARCH: '32' 19 | 20 | - TOXENV: '2.7-cover,codecov' 21 | TOXPYTHON: C:\Python27-x64\python.exe 22 | WINDOWS_SDK_VERSION: v7.0 23 | PYTHON_HOME: C:\Python27-x64 24 | PYTHON_VERSION: '2.7' 25 | PYTHON_ARCH: '64' 26 | 27 | - TOXENV: '2.7-nocov' 28 | TOXPYTHON: C:\Python27\python.exe 29 | PYTHON_HOME: C:\Python27 30 | PYTHON_VERSION: '2.7' 31 | PYTHON_ARCH: '32' 32 | 33 | - TOXENV: '2.7-nocov' 34 | TOXPYTHON: C:\Python27-x64\python.exe 35 | WINDOWS_SDK_VERSION: v7.0 36 | PYTHON_HOME: C:\Python27-x64 37 | PYTHON_VERSION: '2.7' 38 | PYTHON_ARCH: '64' 39 | 40 | - TOXENV: '3.3-cover,codecov' 41 | TOXPYTHON: C:\Python33\python.exe 42 | PYTHON_HOME: C:\Python33 43 | PYTHON_VERSION: '3.3' 44 | PYTHON_ARCH: '32' 45 | 46 | - TOXENV: '3.3-cover,codecov' 47 | TOXPYTHON: C:\Python33-x64\python.exe 48 | WINDOWS_SDK_VERSION: v7.1 49 | PYTHON_HOME: C:\Python33-x64 50 | PYTHON_VERSION: '3.3' 51 | PYTHON_ARCH: '64' 52 | 53 | - TOXENV: '3.3-nocov' 54 | TOXPYTHON: C:\Python33\python.exe 55 | PYTHON_HOME: C:\Python33 56 | PYTHON_VERSION: '3.3' 57 | PYTHON_ARCH: '32' 58 | 59 | - TOXENV: '3.3-nocov' 60 | TOXPYTHON: C:\Python33-x64\python.exe 61 | WINDOWS_SDK_VERSION: v7.1 62 | PYTHON_HOME: C:\Python33-x64 63 | PYTHON_VERSION: '3.3' 64 | PYTHON_ARCH: '64' 65 | 66 | - TOXENV: '3.4-cover,codecov' 67 | TOXPYTHON: C:\Python34\python.exe 68 | PYTHON_HOME: C:\Python34 69 | PYTHON_VERSION: '3.4' 70 | PYTHON_ARCH: '32' 71 | 72 | - TOXENV: '3.4-cover,codecov' 73 | TOXPYTHON: C:\Python34-x64\python.exe 74 | WINDOWS_SDK_VERSION: v7.1 75 | PYTHON_HOME: C:\Python34-x64 76 | PYTHON_VERSION: '3.4' 77 | PYTHON_ARCH: '64' 78 | 79 | - TOXENV: '3.4-nocov' 80 | TOXPYTHON: C:\Python34\python.exe 81 | PYTHON_HOME: C:\Python34 82 | PYTHON_VERSION: '3.4' 83 | PYTHON_ARCH: '32' 84 | 85 | - TOXENV: '3.4-nocov' 86 | TOXPYTHON: C:\Python34-x64\python.exe 87 | WINDOWS_SDK_VERSION: v7.1 88 | PYTHON_HOME: C:\Python34-x64 89 | PYTHON_VERSION: '3.4' 90 | PYTHON_ARCH: '64' 91 | 92 | - TOXENV: '3.5-cover,codecov' 93 | TOXPYTHON: C:\Python35\python.exe 94 | PYTHON_HOME: C:\Python35 95 | PYTHON_VERSION: '3.5' 96 | PYTHON_ARCH: '32' 97 | 98 | - TOXENV: '3.5-cover,codecov' 99 | TOXPYTHON: C:\Python35-x64\python.exe 100 | PYTHON_HOME: C:\Python35-x64 101 | PYTHON_VERSION: '3.5' 102 | PYTHON_ARCH: '64' 103 | 104 | - TOXENV: '3.5-nocov' 105 | TOXPYTHON: C:\Python35\python.exe 106 | PYTHON_HOME: C:\Python35 107 | PYTHON_VERSION: '3.5' 108 | PYTHON_ARCH: '32' 109 | 110 | - TOXENV: '3.5-nocov' 111 | TOXPYTHON: C:\Python35-x64\python.exe 112 | PYTHON_HOME: C:\Python35-x64 113 | PYTHON_VERSION: '3.5' 114 | PYTHON_ARCH: '64' 115 | 116 | init: 117 | - ps: echo $env:TOXENV 118 | - ps: ls C:\Python* 119 | install: 120 | - python -u ci\appveyor-bootstrap.py 121 | - '%PYTHON_HOME%\Scripts\virtualenv --version' 122 | - '%PYTHON_HOME%\Scripts\easy_install --version' 123 | - '%PYTHON_HOME%\Scripts\pip --version' 124 | - '%PYTHON_HOME%\Scripts\tox --version' 125 | test_script: 126 | - '%WITH_COMPILER% %PYTHON_HOME%\Scripts\tox' 127 | 128 | on_failure: 129 | - ps: dir "env:" 130 | - ps: get-content .tox\*\log\* 131 | artifacts: 132 | - path: dist\* 133 | 134 | ### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): 135 | # on_finish: 136 | # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) 137 | -------------------------------------------------------------------------------- /ci/appveyor-bootstrap.py: -------------------------------------------------------------------------------- 1 | """ 2 | AppVeyor will at least have few Pythons around so there's no point of implementing a bootstrapper in PowerShell. 3 | 4 | This is a port of https://github.com/pypa/python-packaging-user-guide/blob/master/source/code/install.ps1 5 | with various fixes and improvements that just weren't feasible to implement in PowerShell. 6 | """ 7 | from __future__ import print_function 8 | from os import environ 9 | from os.path import exists 10 | from subprocess import check_call 11 | 12 | try: 13 | from urllib.request import urlretrieve 14 | except ImportError: 15 | from urllib import urlretrieve 16 | 17 | BASE_URL = "https://www.python.org/ftp/python/" 18 | GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" 19 | GET_PIP_PATH = "C:\get-pip.py" 20 | URLS = { 21 | ("2.7", "64"): BASE_URL + "2.7.10/python-2.7.10.amd64.msi", 22 | ("2.7", "32"): BASE_URL + "2.7.10/python-2.7.10.msi", 23 | # NOTE: no .msi installer for 3.3.6 24 | ("3.3", "64"): BASE_URL + "3.3.3/python-3.3.3.amd64.msi", 25 | ("3.3", "32"): BASE_URL + "3.3.3/python-3.3.3.msi", 26 | ("3.4", "64"): BASE_URL + "3.4.3/python-3.4.3.amd64.msi", 27 | ("3.4", "32"): BASE_URL + "3.4.3/python-3.4.3.msi", 28 | ("3.5", "64"): BASE_URL + "3.5.0/python-3.5.0-amd64.exe", 29 | ("3.5", "32"): BASE_URL + "3.5.0/python-3.5.0.exe", 30 | } 31 | INSTALL_CMD = { 32 | # Commands are allowed to fail only if they are not the last command. Eg: uninstall (/x) allowed to fail. 33 | "2.7": [["msiexec.exe", "/L*+!", "install.log", "/qn", "/x", "{path}"], 34 | ["msiexec.exe", "/L*+!", "install.log", "/qn", "/i", "{path}", "TARGETDIR={home}"]], 35 | "3.3": [["msiexec.exe", "/L*+!", "install.log", "/qn", "/x", "{path}"], 36 | ["msiexec.exe", "/L*+!", "install.log", "/qn", "/i", "{path}", "TARGETDIR={home}"]], 37 | "3.4": [["msiexec.exe", "/L*+!", "install.log", "/qn", "/x", "{path}"], 38 | ["msiexec.exe", "/L*+!", "install.log", "/qn", "/i", "{path}", "TARGETDIR={home}"]], 39 | "3.5": [["{path}", "/quiet", "TargetDir={home}"]], 40 | } 41 | 42 | 43 | def download_file(url, path): 44 | print("Downloading: {} (into {})".format(url, path)) 45 | progress = [0, 0] 46 | 47 | def report(count, size, total): 48 | progress[0] = count * size 49 | if progress[0] - progress[1] > 1000000: 50 | progress[1] = progress[0] 51 | print("Downloaded {:,}/{:,} ...".format(progress[1], total)) 52 | 53 | dest, _ = urlretrieve(url, path, reporthook=report) 54 | return dest 55 | 56 | 57 | def install_python(version, arch, home): 58 | print("Installing Python", version, "for", arch, "bit architecture to", home) 59 | if exists(home): 60 | return 61 | 62 | path = download_python(version, arch) 63 | print("Installing", path, "to", home) 64 | success = False 65 | for cmd in INSTALL_CMD[version]: 66 | cmd = [part.format(home=home, path=path) for part in cmd] 67 | print("Running:", " ".join(cmd)) 68 | try: 69 | check_call(cmd) 70 | except Exception as exc: 71 | print("Failed command", cmd, "with:", exc) 72 | if exists("install.log"): 73 | with open("install.log") as fh: 74 | print(fh.read()) 75 | else: 76 | success = True 77 | if success: 78 | print("Installation complete!") 79 | else: 80 | print("Installation failed") 81 | 82 | 83 | def download_python(version, arch): 84 | for _ in range(3): 85 | try: 86 | return download_file(URLS[version, arch], "installer.exe") 87 | except Exception as exc: 88 | print("Failed to download:", exc) 89 | print("Retrying ...") 90 | 91 | 92 | def install_pip(home): 93 | pip_path = home + "/Scripts/pip.exe" 94 | python_path = home + "/python.exe" 95 | if exists(pip_path): 96 | print("pip already installed.") 97 | else: 98 | print("Installing pip...") 99 | download_file(GET_PIP_URL, GET_PIP_PATH) 100 | print("Executing:", python_path, GET_PIP_PATH) 101 | check_call([python_path, GET_PIP_PATH]) 102 | 103 | 104 | def install_packages(home, *packages): 105 | cmd = [home + "/Scripts/pip.exe", "install"] 106 | cmd.extend(packages) 107 | check_call(cmd) 108 | 109 | 110 | if __name__ == "__main__": 111 | install_python(environ['PYTHON_VERSION'], environ['PYTHON_ARCH'], environ['PYTHON_HOME']) 112 | install_pip(environ['PYTHON_HOME']) 113 | install_packages(environ['PYTHON_HOME'], "setuptools>=18.0.1", "wheel", "tox", "virtualenv>=13.1.0") 114 | -------------------------------------------------------------------------------- /ci/appveyor-download.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Use the AppVeyor API to download Windows artifacts. 4 | 5 | Taken from: https://bitbucket.org/ned/coveragepy/src/tip/ci/download_appveyor.py 6 | # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 7 | # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 8 | """ 9 | from __future__ import unicode_literals 10 | 11 | import argparse 12 | import os 13 | import requests 14 | import zipfile 15 | 16 | 17 | def make_auth_headers(): 18 | """Make the authentication headers needed to use the Appveyor API.""" 19 | path = os.path.expanduser("~/.appveyor.token") 20 | if not os.path.exists(path): 21 | raise RuntimeError( 22 | "Please create a file named `.appveyor.token` in your home directory. " 23 | "You can get the token from https://ci.appveyor.com/api-token" 24 | ) 25 | with open(path) as f: 26 | token = f.read().strip() 27 | 28 | headers = { 29 | 'Authorization': 'Bearer {}'.format(token), 30 | } 31 | return headers 32 | 33 | 34 | def download_latest_artifacts(account_project, build_id): 35 | """Download all the artifacts from the latest build.""" 36 | if build_id is None: 37 | url = "https://ci.appveyor.com/api/projects/{}".format(account_project) 38 | else: 39 | url = "https://ci.appveyor.com/api/projects/{}/build/{}".format(account_project, build_id) 40 | build = requests.get(url, headers=make_auth_headers()).json() 41 | jobs = build['build']['jobs'] 42 | print(u"Build {0[build][version]}, {1} jobs: {0[build][message]}".format(build, len(jobs))) 43 | 44 | for job in jobs: 45 | name = job['name'] 46 | print(u" {0}: {1[status]}, {1[artifactsCount]} artifacts".format(name, job)) 47 | 48 | url = "https://ci.appveyor.com/api/buildjobs/{}/artifacts".format(job['jobId']) 49 | response = requests.get(url, headers=make_auth_headers()) 50 | artifacts = response.json() 51 | 52 | for artifact in artifacts: 53 | is_zip = artifact['type'] == "Zip" 54 | filename = artifact['fileName'] 55 | print(u" {0}, {1} bytes".format(filename, artifact['size'])) 56 | 57 | url = "https://ci.appveyor.com/api/buildjobs/{}/artifacts/{}".format(job['jobId'], filename) 58 | download_url(url, filename, make_auth_headers()) 59 | 60 | if is_zip: 61 | unpack_zipfile(filename) 62 | os.remove(filename) 63 | 64 | 65 | def ensure_dirs(filename): 66 | """Make sure the directories exist for `filename`.""" 67 | dirname, _ = os.path.split(filename) 68 | if dirname and not os.path.exists(dirname): 69 | os.makedirs(dirname) 70 | 71 | 72 | def download_url(url, filename, headers): 73 | """Download a file from `url` to `filename`.""" 74 | ensure_dirs(filename) 75 | response = requests.get(url, headers=headers, stream=True) 76 | if response.status_code == 200: 77 | with open(filename, 'wb') as f: 78 | for chunk in response.iter_content(16 * 1024): 79 | f.write(chunk) 80 | else: 81 | print(u" Error downloading {}: {}".format(url, response)) 82 | 83 | 84 | def unpack_zipfile(filename): 85 | """Unpack a zipfile, using the names in the zip.""" 86 | with open(filename, 'rb') as fzip: 87 | z = zipfile.ZipFile(fzip) 88 | for name in z.namelist(): 89 | print(u" extracting {}".format(name)) 90 | ensure_dirs(name) 91 | z.extract(name) 92 | 93 | parser = argparse.ArgumentParser(description='Download artifacts from AppVeyor.') 94 | parser.add_argument('--id', 95 | metavar='PROJECT_ID', 96 | default='svetlyak40wt/python-cl-conditions', 97 | help='Project ID in AppVeyor.') 98 | parser.add_argument('build', 99 | nargs='?', 100 | metavar='BUILD_ID', 101 | help='Build ID in AppVeyor. Eg: master-123') 102 | 103 | if __name__ == "__main__": 104 | # import logging 105 | # logging.basicConfig(level="DEBUG") 106 | args = parser.parse_args() 107 | download_latest_artifacts(args.id, args.build) 108 | -------------------------------------------------------------------------------- /ci/appveyor-with-compiler.cmd: -------------------------------------------------------------------------------- 1 | :: To build extensions for 64 bit Python 3, we need to configure environment 2 | :: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: 3 | :: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1) 4 | :: 5 | :: To build extensions for 64 bit Python 2, we need to configure environment 6 | :: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of: 7 | :: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0) 8 | :: 9 | :: 32 bit builds do not require specific environment configurations. 10 | :: 11 | :: Note: this script needs to be run with the /E:ON and /V:ON flags for the 12 | :: cmd interpreter, at least for (SDK v7.0) 13 | :: 14 | :: More details at: 15 | :: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows 16 | :: http://stackoverflow.com/a/13751649/163740 17 | :: 18 | :: Author: Olivier Grisel 19 | :: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ 20 | SET COMMAND_TO_RUN=%* 21 | SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows 22 | SET WIN_WDK="c:\Program Files (x86)\Windows Kits\10\Include\wdf" 23 | ECHO SDK: %WINDOWS_SDK_VERSION% ARCH: %PYTHON_ARCH% 24 | 25 | 26 | IF "%PYTHON_VERSION%"=="3.5" ( 27 | IF EXIST %WIN_WDK% ( 28 | REM See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/ 29 | REN %WIN_WDK% 0wdf 30 | ) 31 | GOTO main 32 | ) 33 | 34 | IF "%PYTHON_ARCH%"=="32" ( 35 | GOTO main 36 | ) 37 | 38 | SET DISTUTILS_USE_SDK=1 39 | SET MSSdk=1 40 | "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% 41 | CALL "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release 42 | 43 | :main 44 | 45 | ECHO Executing: %COMMAND_TO_RUN% 46 | CALL %COMMAND_TO_RUN% || EXIT 1 47 | -------------------------------------------------------------------------------- /ci/bootstrap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import, print_function, unicode_literals 4 | 5 | import os 6 | import sys 7 | from os.path import exists 8 | from os.path import join 9 | from os.path import dirname 10 | from os.path import abspath 11 | 12 | 13 | if __name__ == "__main__": 14 | base_path = dirname(dirname(abspath(__file__))) 15 | print("Project path: {0}".format(base_path)) 16 | env_path = join(base_path, ".tox", "bootstrap") 17 | if sys.platform == "win32": 18 | bin_path = join(env_path, "Scripts") 19 | else: 20 | bin_path = join(env_path, "bin") 21 | if not exists(env_path): 22 | import subprocess 23 | print("Making bootstrap env in: {0} ...".format(env_path)) 24 | try: 25 | subprocess.check_call(["virtualenv", env_path]) 26 | except Exception: 27 | subprocess.check_call([sys.executable, "-m", "virtualenv", env_path]) 28 | print("Installing `jinja2` and `matrix` into bootstrap environment ...") 29 | subprocess.check_call([join(bin_path, "pip"), "install", "jinja2", "matrix"]) 30 | activate = join(bin_path, "activate_this.py") 31 | exec(compile(open(activate, "rb").read(), activate, "exec"), dict(__file__=activate)) 32 | 33 | import jinja2 34 | 35 | import matrix 36 | 37 | 38 | jinja = jinja2.Environment( 39 | loader=jinja2.FileSystemLoader(join(base_path, "ci", "templates")), 40 | trim_blocks=True, 41 | lstrip_blocks=True, 42 | keep_trailing_newline=True 43 | ) 44 | 45 | tox_environments = {} 46 | for (alias, conf) in matrix.from_file(join(base_path, "setup.cfg")).items(): 47 | python = conf["python_versions"] 48 | deps = conf["dependencies"] 49 | if "coverage_flags" in conf: 50 | cover = {"false": False, "true": True}[conf["coverage_flags"].lower()] 51 | if "environment_variables" in conf: 52 | env_vars = conf["environment_variables"] 53 | 54 | tox_environments[alias] = { 55 | "python": "python" + python if "py" not in python else python, 56 | "deps": deps.split(), 57 | } 58 | if "coverage_flags" in conf: 59 | tox_environments[alias].update(cover=cover) 60 | if "environment_variables" in conf: 61 | tox_environments[alias].update(env_vars=env_vars.split()) 62 | 63 | 64 | for name in os.listdir(join("ci", "templates")): 65 | with open(join(base_path, name), "w") as fh: 66 | fh.write(jinja.get_template(name).render(tox_environments=tox_environments)) 67 | print("Wrote {}".format(name)) 68 | print("DONE.") 69 | -------------------------------------------------------------------------------- /ci/templates/.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: '3.5' 3 | sudo: false 4 | env: 5 | global: 6 | - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so 7 | - SEGFAULT_SIGNALS=all 8 | matrix: 9 | - TOXENV=check 10 | - TOXENV=docs 11 | {% for env, config in tox_environments|dictsort %}{{ '' }} 12 | - TOXENV={{ env }}{% if config.cover %},codecov{% endif -%} 13 | {% endfor %} 14 | 15 | before_install: 16 | - python --version 17 | - uname -a 18 | - lsb_release -a 19 | install: 20 | - pip install tox 21 | - virtualenv --version 22 | - easy_install --version 23 | - pip --version 24 | - tox --version 25 | script: 26 | - tox -v 27 | after_failure: 28 | - more .tox/log/* | cat 29 | - more .tox/*/log/* | cat 30 | before_cache: 31 | - rm -rf $HOME/.cache/pip/log 32 | cache: 33 | directories: 34 | - $HOME/.cache/pip 35 | notifications: 36 | email: 37 | on_success: never 38 | on_failure: always 39 | -------------------------------------------------------------------------------- /ci/templates/appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{branch}-{build}' 2 | build: off 3 | cache: 4 | - '%LOCALAPPDATA%\pip\Cache' 5 | environment: 6 | global: 7 | WITH_COMPILER: 'cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd' 8 | matrix: 9 | - TOXENV: check 10 | PYTHON_HOME: C:\Python27 11 | PYTHON_VERSION: '2.7' 12 | PYTHON_ARCH: '32' 13 | 14 | {% for env, config in tox_environments|dictsort %}{% if config.python in ('python2.7', 'python3.3', 'python3.4', 'python3.5') %} 15 | - TOXENV: '{{ env }}{% if config.cover %},codecov{% endif %}' 16 | TOXPYTHON: C:\{{ config.python.replace('.', '').capitalize() }}\python.exe 17 | PYTHON_HOME: C:\{{ config.python.replace('.', '').capitalize() }} 18 | PYTHON_VERSION: '{{ config.python[-3:] }}' 19 | PYTHON_ARCH: '32' 20 | 21 | - TOXENV: '{{ env }}{% if config.cover %},codecov{% endif %}' 22 | TOXPYTHON: C:\{{ config.python.replace('.', '').capitalize() }}-x64\python.exe 23 | {%- if config.python != 'python3.5' %} 24 | 25 | WINDOWS_SDK_VERSION: v7.{{ '1' if config.python[-3] == '3' else '0' }} 26 | {%- endif %} 27 | 28 | PYTHON_HOME: C:\{{ config.python.replace('.', '').capitalize() }}-x64 29 | PYTHON_VERSION: '{{ config.python[-3:] }}' 30 | PYTHON_ARCH: '64' 31 | 32 | {% endif %}{% endfor %} 33 | init: 34 | - ps: echo $env:TOXENV 35 | - ps: ls C:\Python* 36 | install: 37 | - python -u ci\appveyor-bootstrap.py 38 | - '%PYTHON_HOME%\Scripts\virtualenv --version' 39 | - '%PYTHON_HOME%\Scripts\easy_install --version' 40 | - '%PYTHON_HOME%\Scripts\pip --version' 41 | - '%PYTHON_HOME%\Scripts\tox --version' 42 | test_script: 43 | - '%WITH_COMPILER% %PYTHON_HOME%\Scripts\tox' 44 | 45 | on_failure: 46 | - ps: dir "env:" 47 | - ps: get-content .tox\*\log\* 48 | artifacts: 49 | - path: dist\* 50 | 51 | ### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): 52 | # on_finish: 53 | # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) 54 | -------------------------------------------------------------------------------- /ci/templates/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | clean, 4 | check, 5 | {% for env in tox_environments|sort %} 6 | {{ env }}, 7 | {% endfor %} 8 | report, 9 | docs 10 | 11 | [testenv] 12 | basepython = 13 | {docs,spell}: python2.7 14 | {clean,check,report,extension-coveralls,coveralls,codecov}: python3.5 15 | setenv = 16 | PYTHONPATH={toxinidir}/tests 17 | PYTHONUNBUFFERED=yes 18 | passenv = 19 | * 20 | deps = 21 | pytest 22 | pytest-travis-fold 23 | commands = 24 | {posargs:py.test -vv --ignore=src} 25 | 26 | [testenv:spell] 27 | setenv = 28 | SPELLCHECK=1 29 | commands = 30 | sphinx-build -b spelling docs dist/docs 31 | skip_install = true 32 | usedevelop = false 33 | deps = 34 | -r{toxinidir}/docs/requirements.txt 35 | sphinxcontrib-spelling 36 | pyenchant 37 | 38 | [testenv:docs] 39 | deps = 40 | -r{toxinidir}/docs/requirements.txt 41 | commands = 42 | sphinx-build {posargs:-E} -b html docs dist/docs 43 | sphinx-build -b linkcheck docs dist/docs 44 | 45 | [testenv:bootstrap] 46 | deps = 47 | jinja2 48 | matrix 49 | skip_install = true 50 | usedevelop = false 51 | commands = 52 | python ci/bootstrap.py 53 | passenv = 54 | * 55 | 56 | [testenv:check] 57 | deps = 58 | docutils 59 | check-manifest 60 | flake8 61 | readme-renderer 62 | pygments 63 | skip_install = true 64 | usedevelop = false 65 | commands = 66 | python setup.py check --strict --metadata --restructuredtext 67 | check-manifest {toxinidir} 68 | flake8 src tests setup.py 69 | 70 | [testenv:coveralls] 71 | deps = 72 | coveralls 73 | skip_install = true 74 | usedevelop = false 75 | commands = 76 | coverage combine 77 | coverage report 78 | coveralls [] 79 | 80 | [testenv:codecov] 81 | deps = 82 | codecov 83 | skip_install = true 84 | usedevelop = false 85 | commands = 86 | coverage combine 87 | coverage report 88 | coverage xml --ignore-errors 89 | codecov [] 90 | 91 | 92 | [testenv:report] 93 | deps = coverage 94 | skip_install = true 95 | usedevelop = false 96 | commands = 97 | coverage combine 98 | coverage report 99 | coverage html 100 | 101 | [testenv:clean] 102 | commands = coverage erase 103 | skip_install = true 104 | usedevelop = false 105 | deps = coverage 106 | 107 | {% for env, config in tox_environments|dictsort %} 108 | [testenv:{{ env }}] 109 | basepython = {env:TOXPYTHON:{{ config.python }}} 110 | {% if config.cover or config.env_vars %} 111 | setenv = 112 | {[testenv]setenv} 113 | {% endif %} 114 | {% for var in config.env_vars %} 115 | {{ var }} 116 | {% endfor %} 117 | {% if config.cover %} 118 | WITH_COVERAGE=yes 119 | usedevelop = true 120 | commands = 121 | {posargs:py.test --cov --cov-report=term-missing -vv} 122 | {% endif %} 123 | {% if config.cover or config.deps %} 124 | deps = 125 | {[testenv]deps} 126 | {% endif %} 127 | {% if config.cover %} 128 | pytest-cov 129 | {% endif %} 130 | {% for dep in config.deps %} 131 | {{ dep }} 132 | {% endfor %} 133 | 134 | {% endfor %} 135 | 136 | 137 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import os 5 | import re 6 | 7 | 8 | extensions = [ 9 | 'sphinx.ext.autodoc', 10 | 'sphinx.ext.autosummary', 11 | 'sphinx.ext.coverage', 12 | 'sphinx.ext.doctest', 13 | 'sphinx.ext.extlinks', 14 | 'sphinx.ext.ifconfig', 15 | 'sphinx.ext.napoleon', 16 | 'sphinx.ext.todo', 17 | 'sphinx.ext.viewcode', 18 | ] 19 | if os.getenv('SPELLCHECK'): 20 | extensions += 'sphinxcontrib.spelling', 21 | spelling_show_suggestions = True 22 | spelling_lang = 'en_US' 23 | 24 | source_suffix = '.rst' 25 | master_doc = 'index' 26 | project = u'cl-conditions' 27 | year = '2016' 28 | author = u'Alexander Artemenko' 29 | copyright = '{0}, {1}'.format(year, author) 30 | 31 | 32 | def read_version(): 33 | """Read version from the first line starting with digit 34 | """ 35 | regex = re.compile('^(?P\d.*?) .*$') 36 | 37 | with open('../CHANGELOG.rst') as f: 38 | for line in f: 39 | match = regex.match(line) 40 | if match: 41 | return match.group('number') 42 | 43 | version = read_version() 44 | 45 | pygments_style = 'trac' 46 | templates_path = ['.'] 47 | extlinks = { 48 | 'issue': ('https://github.com/svetlyak40wt/python-cl-conditions/issues/%s', '#'), 49 | 'pr': ('https://github.com/svetlyak40wt/python-cl-conditions/pull/%s', 'PR #'), 50 | } 51 | import sphinx_py3doc_enhanced_theme 52 | html_theme = "sphinx_py3doc_enhanced_theme" 53 | html_theme_path = [sphinx_py3doc_enhanced_theme.get_html_theme_path()] 54 | html_theme_options = { 55 | 'githuburl': 'https://github.com/svetlyak40wt/python-cl-conditions/' 56 | } 57 | 58 | html_use_smartypants = True 59 | html_last_updated_fmt = '%b %d, %Y' 60 | html_split_index = True 61 | html_sidebars = { 62 | '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], 63 | } 64 | html_short_title = '%s-%s' % (project, version) 65 | 66 | napoleon_use_ivar = True 67 | napoleon_use_rtype = False 68 | napoleon_use_param = False 69 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Contents 3 | ======== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | readme 9 | installation 10 | usage 11 | reference/index 12 | contributing 13 | authors 14 | changelog 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | 23 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | At the command line:: 6 | 7 | pip install conditions 8 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /docs/reference/conditions.rst: -------------------------------------------------------------------------------- 1 | conditions 2 | ========== 3 | 4 | .. http://www.sphinx-doc.org/en/stable/ext/autodoc.html 5 | 6 | .. automodule:: conditions.signals 7 | :members: 8 | :undoc-members: 9 | 10 | .. automodule:: conditions.handlers 11 | :members: 12 | :undoc-members: 13 | 14 | .. automodule:: conditions.restarts 15 | :members: 16 | :undoc-members: 17 | 18 | .. automodule:: conditions.exceptions 19 | :members: 20 | :undoc-members: 21 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ========= 3 | 4 | .. toctree:: 5 | :glob: 6 | 7 | conditions* 8 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # there is a bug in version 1.4: 2 | # https://github.com/sphinx-doc/sphinx/issues/2400 3 | 4 | sphinx>=1.3,<1.4 5 | sphinx-py3doc-enhanced-theme 6 | -e . 7 | -------------------------------------------------------------------------------- /docs/spelling_wordlist.txt: -------------------------------------------------------------------------------- 1 | builtin 2 | builtins 3 | classmethod 4 | staticmethod 5 | classmethods 6 | staticmethods 7 | args 8 | kwargs 9 | callstack 10 | Changelog 11 | Indices 12 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | To use cl-conditions in a project:: 6 | 7 | import conditions 8 | -------------------------------------------------------------------------------- /example/example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | This code is a full reimplementation of the log parser 5 | from "Beyond Exception Handling: Conditions and Restarts" 6 | chapter of the book: 7 | 8 | http://www.gigamonkeys.com/book/beyond-exception-handling-conditions-and-restarts.html 9 | """ 10 | 11 | import os 12 | 13 | from conditions import ( 14 | signal, 15 | restarts, 16 | invoke_restart, 17 | handle) 18 | 19 | 20 | class MalformedLogEntryError(RuntimeError): 21 | def __init__(self, text): 22 | self.text = text 23 | super(MalformedLogEntryError, self).__init__('Malformed entry') 24 | 25 | 26 | class LogEntry(object): 27 | def __init__(self, text): 28 | self.text = text 29 | def __str__(self): 30 | return u'LogEntry: {0}'.format(self.text) 31 | 32 | 33 | class MalformedLogEntry(object): 34 | def __init__(self, text): 35 | self.text = text 36 | def __str__(self): 37 | return u'MalformedEntry: {0}'.format(self.text) 38 | 39 | 40 | def well_formed_log_entry_p(text): 41 | return any(text.startswith(level) 42 | for level in ('ERROR: ', 43 | 'INFO: ', 44 | 'DEBUG: ')) 45 | 46 | 47 | def parse_log_file(filename): 48 | with open(filename) as f: 49 | return map(parse_log_entry, f.readlines()) 50 | 51 | 52 | def analyze_log(filename): 53 | for entry in parse_log_file(filename): 54 | analyze_entry(entry) 55 | 56 | 57 | def analyze_entry(entry): 58 | print(entry) 59 | 60 | 61 | def find_all_logs(path): 62 | return (os.path.join(path, f) 63 | for f in os.listdir(path) 64 | if f.endswith('.log')) 65 | 66 | 67 | def parse_log_entry(text): 68 | """This function does all real job on log line parsing. 69 | it setup two cases for restart parsing if a line 70 | with wrong format was found. 71 | 72 | Restarts: 73 | - use_value: just retuns an object it was passed. This can 74 | be any value. 75 | - reparse: calls `parse_log_entry` again with other text value. 76 | Beware, this call can lead to infinite recursion. 77 | """ 78 | text = text.strip() 79 | 80 | if well_formed_log_entry_p(text): 81 | return LogEntry(text) 82 | else: 83 | def use_value(obj): 84 | return obj 85 | def reparse(text): 86 | return parse_log_entry(text) 87 | 88 | with restarts(use_value, 89 | reparse) as call: 90 | return call(signal, MalformedLogEntryError(text)) 91 | 92 | 93 | def log_analyzer(path): 94 | """This procedure replaces every line which can't be parsed 95 | with special object MalformedLogEntry. 96 | """ 97 | with handle(MalformedLogEntryError, 98 | lambda (c): 99 | invoke_restart('use_value', 100 | MalformedLogEntry(c.text))): 101 | for filename in find_all_logs(path): 102 | analyze_log(filename) 103 | 104 | 105 | def log_analyzer2(path): 106 | """This procedure considers every line which can't be parsed 107 | as a line with ERROR level. 108 | """ 109 | with handle(MalformedLogEntryError, 110 | lambda (c): 111 | invoke_restart('reparse', 112 | 'ERROR: ' + c.text)): 113 | for filename in find_all_logs(path): 114 | analyze_log(filename) 115 | 116 | 117 | if __name__ == '__main__': 118 | current_dir = os.path.dirname(__file__) or '.' 119 | log_analyzer(current_dir) 120 | -------------------------------------------------------------------------------- /example/some.log: -------------------------------------------------------------------------------- 1 | INFO: Starting server 2 | INFO: Horay 3 | DEBUG: Listening on 0.0.0.0:8000 4 | Some strange line 5 | ERROR: blah happened 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | max-line-length = 140 6 | exclude = tests/*,*/migrations/*,*/south_migrations/* 7 | 8 | [pytest] 9 | norecursedirs = 10 | .git 11 | .tox 12 | .env 13 | dist 14 | build 15 | south_migrations 16 | migrations 17 | python_files = 18 | test_*.py 19 | *_test.py 20 | tests.py 21 | addopts = 22 | -rxEfsw 23 | --strict 24 | --ignore=docs/conf.py 25 | --ignore=setup.py 26 | --ignore=ci 27 | --ignore=.eggs 28 | --doctest-modules 29 | --doctest-glob=\*.rst 30 | --tb=short 31 | 32 | [isort] 33 | force_single_line=True 34 | line_length=120 35 | known_first_party=conditions 36 | default_section=THIRDPARTY 37 | forced_separate=test_conditions 38 | 39 | [matrix] 40 | # This is the configuration for the `./bootstrap.py` script. 41 | # It generates `.travis.yml`, `tox.ini` and `appveyor.yml`. 42 | # 43 | # Syntax: [alias:] value [!variable[glob]] [&variable[glob]] 44 | # 45 | # alias: 46 | # - is used to generate the tox environment 47 | # - it's optional 48 | # - if not present the alias will be computed from the `value` 49 | # value: 50 | # - a value of "-" means empty 51 | # !variable[glob]: 52 | # - exclude the combination of the current `value` with 53 | # any value matching the `glob` in `variable` 54 | # - can use as many you want 55 | # &variable[glob]: 56 | # - only include the combination of the current `value` 57 | # when there's a value matching `glob` in `variable` 58 | # - can use as many you want 59 | 60 | python_versions = 61 | 2.7 62 | 3.3 63 | 3.4 64 | 3.5 65 | pypy 66 | 67 | dependencies = 68 | # 1.4: Django==1.4.16 !python_versions[3.*] 69 | # 1.5: Django==1.5.11 70 | # 1.6: Django==1.6.8 71 | # 1.7: Django==1.7.1 !python_versions[2.6] 72 | # Deps commented above are provided as examples. That's what you would use in a Django project. 73 | 74 | coverage_flags = 75 | cover: true 76 | nocov: false 77 | 78 | environment_variables = 79 | - 80 | 81 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | from __future__ import absolute_import, print_function 4 | 5 | import io 6 | import re 7 | from glob import glob 8 | from os.path import basename 9 | from os.path import dirname 10 | from os.path import join 11 | from os.path import splitext 12 | 13 | from setuptools import find_packages 14 | from setuptools import setup 15 | 16 | 17 | def read(*names, **kwargs): 18 | return io.open( 19 | join(dirname(__file__), *names), 20 | encoding=kwargs.get('encoding', 'utf8') 21 | ).read() 22 | 23 | 24 | setup( 25 | name='conditions', 26 | version='0.2.0', 27 | license='BSD', 28 | description="Implementation of the Common Lisp's conditions system in Python.", 29 | long_description='%s\n%s' % ( 30 | re.compile('^.. start-badges.*^.. end-badges', re.M | re.S).sub('', read('README.rst')), 31 | re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst')) 32 | ), 33 | author='Alexander Artemenko', 34 | author_email='svetlyak.40wt@gmail.com', 35 | url='https://github.com/svetlyak40wt/python-cl-conditions', 36 | packages=find_packages('src'), 37 | package_dir={'': 'src'}, 38 | py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], 39 | include_package_data=True, 40 | zip_safe=False, 41 | classifiers=[ 42 | # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers 43 | 'Development Status :: 4 - Beta', 44 | 'Intended Audience :: Developers', 45 | 'License :: OSI Approved :: BSD License', 46 | 'Operating System :: Unix', 47 | 'Operating System :: POSIX', 48 | 'Operating System :: Microsoft :: Windows', 49 | 'Programming Language :: Python', 50 | 'Programming Language :: Python :: 2.7', 51 | 'Programming Language :: Python :: 3', 52 | 'Programming Language :: Python :: 3.3', 53 | 'Programming Language :: Python :: 3.4', 54 | 'Programming Language :: Python :: 3.5', 55 | 'Programming Language :: Python :: Implementation :: CPython', 56 | 'Programming Language :: Python :: Implementation :: PyPy', 57 | 'Topic :: Utilities', 58 | ], 59 | keywords=[ 60 | 'condition', 61 | 'restart', 62 | 'error', 63 | 'exception', 64 | 'cl', 65 | 'common lisp', 66 | ], 67 | ) 68 | -------------------------------------------------------------------------------- /src/conditions/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from .restarts import restart, restarts, invoke_restart 3 | from .handlers import handle 4 | from .signals import signal 5 | 6 | __version__ = "0.2.0" 7 | -------------------------------------------------------------------------------- /src/conditions/exceptions.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | 4 | class InvokeRestart(Exception): 5 | def __init__(self, callback, *args, **kwargs): 6 | """This exception helps to unwind stack and to call 7 | given callback on the frame where it was bound. 8 | """ 9 | self.callback = lambda: callback(*args, **kwargs) 10 | 11 | 12 | class RestartNotFoundError(RuntimeError): 13 | pass 14 | -------------------------------------------------------------------------------- /src/conditions/handlers.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from contextlib import contextmanager 4 | from collections import deque 5 | 6 | 7 | _handlers = threading.local() 8 | _handlers.stack = deque() 9 | 10 | 11 | @contextmanager 12 | def handle(cls, callback): 13 | _handlers.stack.appendleft((cls, callback)) 14 | 15 | try: 16 | yield 17 | finally: 18 | _handlers.stack.popleft() 19 | 20 | 21 | def find_handler(e): 22 | for handled_cls, callback in _handlers.stack: 23 | if isinstance(e, handled_cls): 24 | return callback 25 | -------------------------------------------------------------------------------- /src/conditions/restarts.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import threading 4 | from collections import deque 5 | 6 | from .exceptions import ( 7 | RestartNotFoundError, 8 | InvokeRestart) 9 | from .signals import signal 10 | 11 | 12 | _restarts = threading.local() 13 | _restarts.stack = deque() 14 | 15 | 16 | def find_restart(name): 17 | for restart_name, callback in _restarts.stack: 18 | if restart_name == name: 19 | return callback 20 | 21 | 22 | def invoke_restart(name, *args, **kwargs): 23 | callback = find_restart(name) 24 | if callback is None: 25 | raise RestartNotFoundError(name) 26 | raise InvokeRestart(callback, *args, **kwargs) 27 | 28 | 29 | class restarts(object): 30 | def __init__(self, *callbacks): 31 | self.callbacks = callbacks 32 | 33 | def __enter__(self): 34 | for callback in self.callbacks: 35 | name = callback.__name__ 36 | if name == '': 37 | raise RuntimeError('Restart function should have name') 38 | _restarts.stack.appendleft((name, callback)) 39 | return self 40 | 41 | def __call__(self, callback, *args, **kwargs): 42 | try: 43 | return callback(*args, **kwargs) 44 | except Exception as e: 45 | try: 46 | return signal(e) 47 | except InvokeRestart as e: 48 | return e.callback() 49 | 50 | def __exit__(self, *args): 51 | for i in range(len(self.callbacks)): 52 | _restarts.stack.popleft() 53 | 54 | 55 | def restart(callback): 56 | return restarts(callback) 57 | -------------------------------------------------------------------------------- /src/conditions/signals.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import print_function 4 | 5 | import os 6 | import traceback 7 | 8 | from .handlers import find_handler 9 | 10 | 11 | _activate_debugger = os.environ.get('DEBUG') == 'yes' 12 | if _activate_debugger: 13 | try: 14 | from trepan.api import debug 15 | set_trace = debug 16 | except ImportError: 17 | import pdb 18 | set_trace = pdb.set_trace 19 | 20 | 21 | def signal(e): 22 | """ 23 | Some docstrings. 24 | """ 25 | callback = find_handler(e) 26 | if callback is None: 27 | if _activate_debugger: 28 | print('Handler for error {0} not found'.format(type(e))) 29 | traceback.print_stack() 30 | set_trace() 31 | raise e 32 | else: 33 | return callback(e) 34 | -------------------------------------------------------------------------------- /tests/test_conditions.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from unittest import TestCase 4 | 5 | from hamcrest import assert_that, contains 6 | 7 | from conditions.exceptions import RestartNotFoundError 8 | from conditions import ( 9 | restart, 10 | restarts, 11 | handle, 12 | signal, 13 | invoke_restart) 14 | 15 | 16 | class TestError(RuntimeError): 17 | pass 18 | 19 | 20 | class HandlerTests(TestCase): 21 | messages = [] 22 | 23 | def log(self, message): 24 | self.messages.append(message) 25 | 26 | def setUp(self): 27 | self.messages[:] = [] 28 | 29 | 30 | def foo(self): 31 | self.log('FOO before signal') 32 | signal(TestError()) 33 | self.log('FOO after signal') 34 | 35 | def bar(self): 36 | self.log('BAR') 37 | 38 | def blah(self): 39 | def choose_bar(): 40 | self.log('Calling bar instead of failed foo') 41 | self.bar() 42 | 43 | with restart(choose_bar) as call: 44 | self.log('Calling foo') 45 | call(self.foo) 46 | 47 | def test_without_handler(self): 48 | self.assertRaises(TestError, self.foo) 49 | 50 | assert_that(self.messages, 51 | contains('FOO before signal')) 52 | 53 | def test_with_handler(self): 54 | def continue_execution(e): 55 | self.log('Continuing execution') 56 | 57 | with handle(TestError, continue_execution): 58 | self.foo() 59 | self.bar() 60 | 61 | assert_that(self.messages, 62 | contains('FOO before signal', 63 | 'Continuing execution', 64 | 'FOO after signal', 65 | 'BAR')) 66 | 67 | def test_with_handler_for_parent_exception_class(self): 68 | def continue_execution(e): 69 | self.log('Continuing execution') 70 | 71 | # in this case, it should work as well 72 | with handle(RuntimeError, continue_execution): 73 | self.foo() 74 | self.bar() 75 | 76 | assert_that(self.messages, 77 | contains('FOO before signal', 78 | 'Continuing execution', 79 | 'FOO after signal', 80 | 'BAR')) 81 | 82 | def test_with_restarts(self): 83 | def choose_bar(e): 84 | invoke_restart('choose_bar') 85 | 86 | with handle(TestError, choose_bar): 87 | self.blah() 88 | 89 | assert_that(self.messages, 90 | contains( 91 | 'Calling foo', 92 | 'FOO before signal', 93 | 'Calling bar instead of failed foo', 94 | 'BAR')) 95 | 96 | def test_when_restart_not_found(self): 97 | def choose_bar(e): 98 | invoke_restart('some_strange_restart') 99 | 100 | with handle(TestError, choose_bar): 101 | self.assertRaises(RestartNotFoundError, self.blah) 102 | 103 | assert_that(self.messages, 104 | contains( 105 | 'Calling foo', 106 | 'FOO before signal')) 107 | 108 | def test_when_restart_returns_value(self): 109 | def use_value(value): 110 | return value 111 | 112 | def handle_error(e): 113 | invoke_restart('use_value', 12345) 114 | 115 | def good_function(): 116 | with restart(use_value) as call: 117 | return call(bad_function) 118 | 119 | def bad_function(): 120 | signal(RuntimeError()) 121 | 122 | with handle(RuntimeError, handle_error): 123 | result = good_function() 124 | 125 | assert result == 12345 126 | 127 | 128 | def test_when_multiple_restarts(self): 129 | def handle_error_using_value(e): 130 | invoke_restart('use_value', -1) 131 | 132 | def handle_error_trying_value(e): 133 | invoke_restart('try_value', 5) 134 | 135 | def handle_error_continuing(e): 136 | invoke_restart('throw', e) 137 | 138 | def good_function(b): 139 | def use_value(value): 140 | return value 141 | 142 | def try_value(value): 143 | return good_function(value) 144 | 145 | def throw(e): 146 | raise e 147 | 148 | with restarts(use_value, 149 | try_value, 150 | throw) as call: 151 | return call(bad_function, 1000, b) 152 | 153 | def bad_function(a, b): 154 | return a / b 155 | 156 | result = good_function(10) 157 | assert result == 100 158 | 159 | with handle(ZeroDivisionError, handle_error_using_value): 160 | result = good_function(0) 161 | assert result == -1 162 | 163 | with handle(ZeroDivisionError, handle_error_trying_value): 164 | result = good_function(0) 165 | assert result == 200 166 | 167 | with handle(ZeroDivisionError, handle_error_continuing): 168 | self.assertRaises(ZeroDivisionError, good_function, 0) 169 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | clean, 4 | check, 5 | 2.7-cover, 6 | 2.7-nocov, 7 | 3.3-cover, 8 | 3.3-nocov, 9 | 3.4-cover, 10 | 3.4-nocov, 11 | 3.5-cover, 12 | 3.5-nocov, 13 | pypy-cover, 14 | pypy-nocov, 15 | report, 16 | docs 17 | 18 | [testenv] 19 | basepython = 20 | {docs,spell}: python2.7 21 | {clean,check,report,extension-coveralls,coveralls,codecov}: python3.5 22 | setenv = 23 | PYTHONPATH={toxinidir}/tests 24 | PYTHONUNBUFFERED=yes 25 | passenv = 26 | * 27 | deps = 28 | trepan 29 | pyhamcrest 30 | pytest 31 | pytest-travis-fold 32 | commands = 33 | {posargs:py.test tests} 34 | 35 | [testenv:spell] 36 | setenv = 37 | SPELLCHECK=1 38 | commands = 39 | sphinx-build -b spelling docs dist/docs 40 | skip_install = true 41 | usedevelop = false 42 | deps = 43 | -r{toxinidir}/docs/requirements.txt 44 | sphinxcontrib-spelling 45 | pyenchant 46 | 47 | [testenv:docs] 48 | deps = 49 | -r{toxinidir}/docs/requirements.txt 50 | commands = 51 | sphinx-build {posargs:-E} -b html docs dist/docs 52 | # sphinx-build -b linkcheck docs dist/docs 53 | 54 | [testenv:bootstrap] 55 | deps = 56 | jinja2 57 | matrix 58 | skip_install = true 59 | usedevelop = false 60 | commands = 61 | python ci/bootstrap.py 62 | passenv = 63 | * 64 | 65 | [testenv:check] 66 | deps = 67 | docutils 68 | check-manifest 69 | flake8 70 | readme-renderer 71 | pygments 72 | skip_install = true 73 | usedevelop = false 74 | commands = 75 | python setup.py check --strict --metadata --restructuredtext 76 | check-manifest {toxinidir} 77 | flake8 src tests setup.py 78 | 79 | [testenv:coveralls] 80 | deps = 81 | coveralls 82 | skip_install = true 83 | usedevelop = false 84 | commands = 85 | coverage combine 86 | coverage report 87 | coveralls [] 88 | 89 | [testenv:codecov] 90 | deps = 91 | codecov 92 | skip_install = true 93 | usedevelop = false 94 | commands = 95 | coverage combine 96 | coverage report 97 | coverage xml --ignore-errors 98 | codecov [] 99 | 100 | 101 | [testenv:report] 102 | deps = coverage 103 | skip_install = true 104 | usedevelop = false 105 | commands = 106 | coverage combine 107 | coverage report 108 | coverage html 109 | 110 | [testenv:clean] 111 | commands = coverage erase 112 | skip_install = true 113 | usedevelop = false 114 | deps = coverage 115 | 116 | [testenv:2.7-cover] 117 | basepython = {env:TOXPYTHON:python2.7} 118 | setenv = 119 | {[testenv]setenv} 120 | WITH_COVERAGE=yes 121 | usedevelop = true 122 | commands = 123 | {posargs:py.test --cov --cov-report=term-missing -vv tests} 124 | deps = 125 | {[testenv]deps} 126 | pytest-cov 127 | 128 | [testenv:2.7-nocov] 129 | basepython = {env:TOXPYTHON:python2.7} 130 | 131 | [testenv:3.3-cover] 132 | basepython = {env:TOXPYTHON:python3.3} 133 | setenv = 134 | {[testenv]setenv} 135 | WITH_COVERAGE=yes 136 | usedevelop = true 137 | commands = 138 | {posargs:py.test --cov --cov-report=term-missing -vv tests} 139 | deps = 140 | {[testenv]deps} 141 | pytest-cov 142 | 143 | [testenv:3.3-nocov] 144 | basepython = {env:TOXPYTHON:python3.3} 145 | 146 | [testenv:3.4-cover] 147 | basepython = {env:TOXPYTHON:python3.4} 148 | setenv = 149 | {[testenv]setenv} 150 | WITH_COVERAGE=yes 151 | usedevelop = true 152 | commands = 153 | {posargs:py.test --cov --cov-report=term-missing -vv tests} 154 | deps = 155 | {[testenv]deps} 156 | pytest-cov 157 | 158 | [testenv:3.4-nocov] 159 | basepython = {env:TOXPYTHON:python3.4} 160 | 161 | [testenv:3.5-cover] 162 | basepython = {env:TOXPYTHON:python3.5} 163 | setenv = 164 | {[testenv]setenv} 165 | WITH_COVERAGE=yes 166 | usedevelop = true 167 | commands = 168 | {posargs:py.test --cov --cov-report=term-missing -vv tests} 169 | deps = 170 | {[testenv]deps} 171 | pytest-cov 172 | 173 | [testenv:3.5-nocov] 174 | basepython = {env:TOXPYTHON:python3.5} 175 | 176 | [testenv:pypy-cover] 177 | basepython = {env:TOXPYTHON:pypy} 178 | setenv = 179 | {[testenv]setenv} 180 | WITH_COVERAGE=yes 181 | usedevelop = true 182 | commands = 183 | {posargs:py.test --cov --cov-report=term-missing -vv tests} 184 | deps = 185 | {[testenv]deps} 186 | pytest-cov 187 | 188 | [testenv:pypy-nocov] 189 | basepython = {env:TOXPYTHON:pypy} 190 | 191 | 192 | 193 | --------------------------------------------------------------------------------