├── .env ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── bootstrap.py ├── debian └── changelog ├── dev-requirements.txt ├── docs ├── CONTRIBUTING.rst ├── LICENSE.rst ├── _static │ ├── css │ │ └── custom.css │ └── img │ │ ├── favicon.ico │ │ ├── invoke-release-prep-changes.png │ │ ├── jenkins-description.png │ │ ├── logo-200.png │ │ ├── logo-60.png │ │ ├── logo.svg │ │ ├── py-generic-project-logo.png │ │ ├── symbol-200.png │ │ ├── symbol-48.png │ │ └── symbol.svg ├── announce.txt ├── api-reference.rst ├── api │ ├── modules.rst │ ├── rituals.acts.rst │ ├── rituals.rst │ ├── rituals.util.rst │ └── rituals.util.scm.rst ├── conf.py ├── copyright.rst ├── customize.rst ├── index.rst ├── requirements.txt ├── tasks.rst └── usage.rst ├── frozen-requirements.txt ├── invoke.yaml ├── project.d ├── classifiers.txt ├── coverage.cfg └── pylint.cfg ├── requirements.txt ├── setup.cfg ├── setup.py ├── src ├── rituals │ ├── __init__.py │ ├── acts │ │ ├── __init__.py │ │ ├── basic.py │ │ ├── devpi.py │ │ ├── documentation.py │ │ ├── github.py │ │ ├── inspection.py │ │ ├── jenkins.py │ │ ├── pkgdeb.py │ │ ├── releasing.py │ │ └── testing.py │ ├── config.py │ ├── easy.py │ └── util │ │ ├── __init__.py │ │ ├── _compat.py │ │ ├── antglob.py │ │ ├── buildsys.py │ │ ├── filesys.py │ │ ├── notify.py │ │ ├── scm │ │ ├── __init__.py │ │ ├── base.py │ │ ├── git.py │ │ └── null.py │ │ ├── shell.py │ │ └── which.py └── tests │ ├── conftest.py │ ├── test_acts.py │ ├── test_acts_basic.py │ ├── test_acts_devpi.py │ ├── test_acts_documentation.py │ ├── test_acts_github.py │ ├── test_acts_inspection.py │ ├── test_acts_releasing.py │ ├── test_acts_testing.py │ ├── test_antglob.py │ ├── test_compat.py │ ├── test_config.py │ ├── test_easy.py │ ├── test_filesys.py │ ├── test_notify.py │ ├── test_util.py │ └── test_which_py ├── task-requirements.txt ├── tasks.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | src/*.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # virtualenv 27 | bin/ 28 | include/ 29 | lib/ 30 | share/ 31 | local/ 32 | pyvenv.cfg 33 | .venv/ 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | pip-selfcheck.json 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .coverage 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | 61 | # Sphinx documentation 62 | docs/_build/ 63 | 64 | # PyBuilder 65 | target/ 66 | 67 | # Jekyll 68 | _site/ 69 | 70 | # Project 71 | docs/README.rst 72 | .pytest_cache/ 73 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Travis Project Descriptor 3 | # 4 | # See http://docs.travis-ci.com/user/build-configuration/ 5 | # 6 | 7 | # build matrix 8 | language: python 9 | cache: pip 10 | python: 11 | - "3.6" 12 | - "3.7" 13 | - "3.8" 14 | # - "pypy" 15 | 16 | # command to install dependencies 17 | install: 18 | - "pip install -r dev-requirements.txt" 19 | - "python setup.py develop -U" 20 | 21 | # command to run tests 22 | script: invoke --echo --pty ci 23 | 24 | # report to coveralls.io 25 | after_success: 26 | - coveralls --rcfile project.d/coverage.cfg 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | ## Overview 4 | 5 | Contributing to this project is easy, and reporting an issue or adding to the documentation 6 | also improves things for every user. You don't need to be a developer to contribute. 7 | 8 | 9 | ### Reporting issues 10 | 11 | Please use the *GitHub issue tracker*, and describe your problem so that it can be easily 12 | reproduced. Providing relevant version information on the project itself and your environment helps with that. 13 | 14 | 15 | ### Improving documentation 16 | 17 | The easiest way to provide examples or related documentation that helps other users 18 | is the *GitHub wiki*. 19 | 20 | If you are comfortable with the Sphinx documentation tool, you can also prepare a 21 | pull request with changes to the core documentation. 22 | GitHub's built-in text editor makes this especially easy, when you choose the 23 | _“Create a new branch for this commit and start a pull request”_ option on saving. 24 | Small fixes for typos and the like are a matter of minutes when using that tool. 25 | 26 | 27 | ### Code contributions 28 | 29 | Here's a quick guide to improve the code: 30 | 31 | 1. Fork the repo, and clone the fork to your machine. 32 | 1. Add your improvements, the technical details are further below. 33 | 1. Run the tests and make sure they're passing (`invoke test`). 34 | 1. Check for violations of code conventions (`invoke check`). 35 | 1. Make sure the documentation builds without errors (`invoke build --docs`). 36 | 1. Push to your fork and submit a [pull request](https://help.github.com/articles/using-pull-requests/). 37 | 38 | Please be patient while waiting for a review. Life & work tend to interfere. 39 | 40 | 41 | ## Details on contributing code 42 | 43 | This project is written in [Python](http://www.python.org/), 44 | and the documentation is generated using [Sphinx](https://pypi.python.org/pypi/Sphinx). 45 | [setuptools](https://packaging.python.org/en/latest/projects.html#setuptools) 46 | and [Invoke](http://www.pyinvoke.org/) are used to build and manage the project. 47 | Tests are written and executed using [pytest](http://pytest.org/) and 48 | [tox](https://testrun.org/tox/ ). 49 | 50 | 51 | ### Set up a working development environment 52 | 53 | To set up a working directory from your own fork, 54 | follow [these steps](https://github.com/jhermann/rituals/blob/master/README.md#contributing), 55 | but replace the repository `https` URLs with SSH ones that point to your fork. 56 | 57 | For that to work on Debian type systems, you need the 58 | `git`, `python`, and `python-virtualenv` 59 | packages installed. Other distributions are similar. 60 | 61 | 62 | ### Add your changes to a feature branch 63 | 64 | For any cohesive set of changes, create a *new* branch based on the current upstream `master`, 65 | with a name reflecting the essence of your improvement. 66 | 67 | ```sh 68 | git branch "name-for-my-fixes" origin/master 69 | git checkout "name-for-my-fixes" 70 | … make changes… 71 | invoke ci # check output for broken tests, or PEP8 violations and the like 72 | … commit changes… 73 | git push origin "name-for-my-fixes" 74 | ``` 75 | 76 | Please don't create large lumps of unrelated changes in a single pull request. 77 | Also take extra care to avoid spurious changes, like mass whitespace diffs. 78 | All Python sources use spaces to indent, not TABs. 79 | 80 | 81 | ### Make sure your changes work 82 | 83 | Some things that will increase the chance that your pull request is accepted: 84 | 85 | * Follow style conventions you see used in the source already (and read [PEP8](http://www.python.org/dev/peps/pep-0008/)). 86 | * Include tests that fail *without* your code, and pass *with* it. Only minor refactoring and documentation changes require no new tests. If you are adding functionality or fixing a bug, please also add a test for it! 87 | * Update any documentation or examples impacted by your change. 88 | * Styling conventions and code quality are checked with `invoke check`, tests are run using `invoke test`, and the docs can be built locally using `invoke build --docs`. 89 | 90 | Following these hints also expedites the whole procedure, since it avoids unnecessary feedback cycles. 91 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # 2 | # Additional files for source distribution 3 | # 4 | include README.md LICENSE CONTRIBUTING.md *.txt *.py tox.ini invoke.yaml 5 | include .travis.yml .env .gitignore 6 | 7 | recursive-include debian changelog .env .gitignore invoke.yaml 8 | 9 | recursive-include docs conf.py *.rst *.txt 10 | recursive-include docs/_static/css *.css 11 | recursive-include docs/_static/img *.ico *.png *.svg 12 | 13 | recursive-include project.d *.cfg *.txt *.py 14 | recursive-include src/tests conftest.py test_*.py 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rituals 2 | 3 | Common tasks for [Invoke](http://www.pyinvoke.org/) that are needed again and again. 4 | 5 | ![GINOSAJI](https://raw.githubusercontent.com/jhermann/rituals/master/docs/_static/img/symbol-200.png) … and again and again. 6 | 7 |  [![Groups](https://img.shields.io/badge/Google_groups-rituals--dev-orange.svg)](https://groups.google.com/forum/#!forum/rituals-dev) 8 |  [![Travis CI](https://api.travis-ci.org/jhermann/rituals.svg)](https://travis-ci.org/jhermann/rituals) 9 |  [![Coveralls](https://img.shields.io/coveralls/jhermann/rituals.svg)](https://coveralls.io/r/jhermann/rituals) 10 |  [![GitHub Issues](https://img.shields.io/github/issues/jhermann/rituals.svg)](https://github.com/jhermann/rituals/issues) 11 |  [![License](https://img.shields.io/pypi/l/rituals.svg)](https://github.com/jhermann/rituals/blob/master/LICENSE) 12 |  [![Development Status](https://img.shields.io/pypi/status/rituals.svg)](https://pypi.python.org/pypi/rituals/) 13 |  [![Latest Version](https://img.shields.io/pypi/v/rituals.svg)](https://pypi.python.org/pypi/rituals/) 14 | 15 | 16 | ## Overview 17 | 18 | “Rituals” is a task library for [Invoke](http://www.pyinvoke.org/) that keeps the 19 | most common tasks you always need out of your project, and makes them centrally maintained. 20 | This leaves your `tasks.py` small and to the point, 21 | with only things specific to the project at hand. 22 | The following lists the common task implementations that the ``rituals.easy`` module offers. 23 | See the [full docs](https://rituals.readthedocs.io/en/latest/usage.html#adding-rituals-to-your-project) 24 | on how to integrate them into your `tasks.py`. 25 | 26 | * ``help`` – Default task, when invoked with no task names. 27 | * ``clean`` – Perform house-cleaning. 28 | * ``build`` – Build the project. 29 | * ``test`` – Perform standard unittests. 30 | * ``check`` – Perform source code checks. 31 | * ``release.bump`` – Bump a development version. 32 | * ``release.dist`` – Distribute the project. 33 | * ``release.prep`` – Prepare for a release (perform QA checks, and switch to non-dev versioning). 34 | * … and *many* more, see `inv -l` for a complete list. 35 | 36 | The guiding principle for these tasks is to strictly separate 37 | low-level tasks for building and installing (via ``setup.py``) 38 | from high-level convenience tasks a developer uses (via ``invoke``). 39 | Invoke tasks can use Setuptools ones as building blocks, 40 | but never the other way 'round 41 | – this avoids any bootstrapping headaches during package installations. 42 | 43 | Use ``inv -h ‹task›`` as usual to get details on the options of these tasks. 44 | Look at the modules in [acts](https://github.com/jhermann/rituals/blob/master/src/rituals/acts) 45 | if you want to know what these tasks do exactly. 46 | Also consult the [full documentation](https://rituals.readthedocs.io/) 47 | for a complete reference. 48 | 49 | :bulb: | The easiest way to get a working project using `rituals` is the [py-generic-project](https://github.com/Springerle/py-generic-project) cookiecutter archetype. That way you have a working project skeleton within minutes that is fully equipped, with all aspects of building, testing, quality checks, continuous integration, documentation, and releasing covered. 50 | ---- | :---- 51 | 52 | 53 | ## Some Practical Examples 54 | 55 | The following table shows a selection of typical use-cases and how to 56 | carry them out in projects that include *Rituals* in their `tasks.py` 57 | (e.g. this one). 58 | 59 | Command | Description 60 | ----: | :---- 61 | `inv docs -w -b` | Start a `sphinx-autobuild` watchdog and open the resulting live-reload preview in your browser. 62 | `inv test.tox --clean -e py34` | Run `tox` for Python 3.4 with a clean status, i.e. an empty `.tox` directory. 63 | `inv release.bump` | Set the `tag_build` value in `setup.cfg` to something like `0.3.0.dev117+0.2.0.g993edd3.20150408t1747`, uniquely identifying dev builds, even in dirty working directories. 64 | 65 | See the [full documentation](https://rituals.readthedocs.io/) 66 | for more examples and a complete reference. 67 | 68 | 69 | ## Contributing 70 | 71 | To create a working directory for this project, call these commands: 72 | 73 | ```sh 74 | git clone "https://github.com/jhermann/rituals.git" 75 | cd rituals 76 | command . .env --yes --develop # add '--virtualenv /usr/bin/virtualenv' for Python2 77 | invoke build --docs test check 78 | ``` 79 | 80 | To use the source in this working directory within another project, 81 | change your current directory to _this_ project, 82 | then call `bin/pip` from *that* project's virtualenv like so: 83 | 84 | …/.venv/…/bin/pip install -e . 85 | 86 | See [CONTRIBUTING](https://github.com/jhermann/rituals/blob/master/CONTRIBUTING.md) for more. 87 | 88 | [![Throughput Graph](https://graphs.waffle.io/jhermann/rituals/throughput.svg)](https://waffle.io/jhermann/rituals/metrics) 89 | 90 | 91 | ## Releasing 92 | 93 | This is the process of releasing ``rituals`` itself, 94 | projects that use it will have an identical to very similar sequence of commands. 95 | 96 | ```sh 97 | inv release.prep 98 | inv release.dist --devpi # local release + tox testing 99 | 100 | git push && git push --tags # … and wait for Travis CI to do its thing 101 | 102 | twine upload -r pypi dist/* 103 | ``` 104 | 105 | If you have any pending changes, staged or unstaged, you'll get an error like this: 106 | 107 | ![uncommitted changes](https://raw.githubusercontent.com/jhermann/rituals/master/docs/_static/img/invoke-release-prep-changes.png) 108 | 109 | 110 | ## Related Projects 111 | 112 | * [Springerle/py-generic-project](https://github.com/Springerle/py-generic-project) – Cookiecutter template that creates a basic Python project, which can be later on augmented with various optional accessories. 113 | * [pyinvoke/invoke](https://github.com/pyinvoke/invoke) – Task execution tool & library. 114 | * [pyinvoke/invocations](https://github.com/pyinvoke/invocations) – A collection of reusable Invoke tasks and task modules. 115 | 116 | 117 | ## Acknowledgements 118 | 119 | * Logo elements from [clker.com Free Clipart](http://www.clker.com/). 120 | * In case you wonder about the logo, [watch this](http://youtu.be/9VDvgL58h_Y). 121 | -------------------------------------------------------------------------------- /bootstrap.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ Python Project 0dependency Bootstrap Wizard. 4 | 5 | Right now, just a set of notes for a later implementation. 6 | 7 | Requirements: 8 | 9 | * Support at least Python 2.7 and 3.4. 10 | * One simple command to set up a working development environment. 11 | * Report any problems of the underlying tools in a way humans can understand. 12 | * Abstract away environment specifics (auto-detect typical scenarios). 13 | * Work "offline", i.e. without a direct connection to the Internet. 14 | [detect devpi? load a pre-build dependency bundle from an env var location? 15 | work with available pre-installed packages? …] 16 | * Self-upgrade from well-known location on demand (python bootstrap.py upgrade). 17 | 18 | Sources of inspiration / re-use: 19 | * https://pypi.python.org/pypi/pyutilib.virtualenv/ 20 | * https://virtualenv.pypa.io/en/latest/reference.html?highlight=bootstrap#creating-your-own-bootstrap-scripts 21 | * https://github.com/socialplanning/fassembler/blob/master/fassembler/create-venv-script.py 22 | * https://github.com/pypa/virtualenv/issues/632 23 | 24 | 25 | Ubuntu Trusty castrates pyvenv (omits ensurepip), thus: 26 | 27 | /usr/bin/pyvenv-3.4 --without-pip py34 28 | cd py34 29 | curl -skS https://bootstrap.pypa.io/get-pip.py | bin/python - 30 | 31 | 32 | Copyright ⓒ 2015 Jürgen Hermann 33 | 34 | This program is free software; you can redistribute it and/or modify 35 | it under the terms of the GNU General Public License version 2 as 36 | published by the Free Software Foundation. 37 | 38 | This program is distributed in the hope that it will be useful, 39 | but WITHOUT ANY WARRANTY; without even the implied warranty of 40 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 41 | GNU General Public License for more details. 42 | 43 | You should have received a copy of the GNU General Public License along 44 | with this program; if not, write to the Free Software Foundation, Inc., 45 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 46 | 47 | The full LICENSE file and source are available at 48 | https://github.com/jhermann/rituals 49 | """ 50 | # TODO: Actually implement this 51 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | rituals (0.5.1) unstable; urgency=medium 2 | 3 | * release.pex: '--windows' option for cross-platform builds 4 | * release.shiv: new task for single-binary releases 5 | * rituals.acts.releasing.upload_artifacts: helper function 6 | you can use for WebDAV uploads from your custom tasks 7 | * release.bump: only consider annotated tags, and exclude 'stable' 8 | * docs autobuild: add sources to watch dirs when using API docs 9 | * docs.sphinx: no markdown file conversion when recommonmark is used 10 | * Officially supported Python versions now 3.6 .. 3.8 11 | 12 | -- Juergen Hermann Tue, 21 Jul 2020 06:54:10 +0200 13 | 14 | rituals (0.4.1) unstable; urgency=medium 15 | 16 | * docs.upload: add 'git_path' as url variable 17 | * docs.upload: fix for 'ssh://' git urls 18 | * sdist: fixed MANIFEST omissions 19 | * chg: Removed Python 2.7 20 | 21 | -- Juergen Hermann Tue, 16 Jul 2019 13:44:59 +0200 22 | 23 | rituals (0.3.1) unstable; urgency=low 24 | 25 | * new: 'docs.sphinx' and 'docs.upload' task (to PyPI/WebDAV/Artifactory) 26 | * new: convenient control of 'sphinx-autobuild' daemon 27 | * new: added 'test' namespace and 'tox' task 28 | * new: 'release.pex' to create PEX artifacts 29 | * new: optional inclusion of PyRun in PEX artifacts 30 | * new: 'release.bump' to create unique development versions 31 | * new: build a DEB package (new 'deb' task) 32 | * new: 'devpi' namespace with a 'refresh' task 33 | * new: added 'freeze' as a root task 34 | * new: 'jenkins.description' to display project metadata in the Jenkins UI 35 | * new: support for projects with 'py_modules' 36 | * new: 'util.filesys' module with a 'pushd' helper 37 | * new: 'fail' helper function in 'rituals.easy' 38 | * new: 'with_root' param for FileSet.walk() 39 | * chg: 'test.pytest' as the default in 'test' namespace 40 | * chg: 'prep' renamed to 'release.prep' 41 | * chg: added '--clean' option to 'test.tox', and 'clean --tox' 42 | * chg: exclude SCM dirs from 'clean' 43 | * chg: improved autoenv script 44 | * chg: better HTTP error reporting for docs upload 45 | * chg: more unit tests (still missing a lot) 46 | * chg: Python 3.4 support 47 | * chg: Python3 is now the workdir default 48 | * fix: non-git workdirs are handled correctly in 'release.prep' 49 | * fix: support for Invoke v1 (`ctask` rename) 50 | 51 | -- Juergen Hermann Thu, 24 Jan 2019 14:07:23 +0100 52 | 53 | rituals (0.2.0) unstable; urgency=low 54 | 55 | * new: added 'release-prep' task 56 | * new: added --skip-root to 'check', and checking './*.py' too 57 | * new: 'dist' task automatically creates wheels if possible 58 | * chg: better handling of 'build --docs' 59 | * chg: added help for task parameters (closes #4) 60 | * chg: warn about missing Sphinx docs (when '--docs' is provided) 61 | * fix: get src package list for 'check' from 'project.packages' 62 | * fix: use 'which' to look for 'py.test' binary (closes #2) 63 | 64 | -- Juergen Hermann Mon, 16 Mar 2015 18:02:42 +0100 65 | 66 | rituals (0.1.0) unstable; urgency=low 67 | 68 | * Initial release. 69 | 70 | -- Juergen Hermann Thu, 12 Mar 2015 18:20:16 +0100 71 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Development requirements 3 | # 4 | 5 | bpython==0.21 6 | yolk3k==0.9 7 | 8 | pip==21.2.2 9 | pip-upgrader>1.4 10 | tox==3.24.1 11 | twine==3.4.2 12 | 13 | -r task-requirements.txt 14 | sphinx-rtd-theme==0.5.2 15 | 16 | -r requirements.txt 17 | -e . 18 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contribution Guidelines 2 | ======================= 3 | 4 | Overview 5 | -------- 6 | 7 | Contributing to this project is easy, and reporting an issue or adding 8 | to the documentation also improves things for every user. You don't need 9 | to be a developer to contribute. 10 | 11 | Reporting issues 12 | ~~~~~~~~~~~~~~~~ 13 | 14 | Please use the *GitHub issue tracker*, and describe your problem so that 15 | it can be easily reproduced. Providing relevant version information on 16 | the project itself and your environment helps with that. 17 | 18 | Improving documentation 19 | ~~~~~~~~~~~~~~~~~~~~~~~ 20 | 21 | The easiest way to provide examples or related documentation that helps 22 | other users is the *GitHub wiki*. 23 | 24 | If you are comfortable with the Sphinx documentation tool, you can also 25 | prepare a pull request with changes to the core documentation. GitHub's 26 | built-in text editor makes this especially easy, when you choose the 27 | *“Create a new branch for this commit and start a pull request”* option 28 | on saving. Small fixes for typos and the like are a matter of minutes 29 | when using that tool. 30 | 31 | Code contributions 32 | ~~~~~~~~~~~~~~~~~~ 33 | 34 | Here's a quick guide to improve the code: 35 | 36 | 1. Fork the repo, and clone the fork to your machine. 37 | 2. Add your improvements, the technical details are further below. 38 | 3. Run the tests and make sure they're passing (``invoke test``). 39 | 4. Check for violations of code conventions (``invoke check``). 40 | 5. Make sure the documentation builds without errors 41 | (``invoke build --docs``). 42 | 6. Push to your fork and submit a `pull 43 | request `__. 44 | 45 | Please be patient while waiting for a review. Life & work tend to 46 | interfere. 47 | 48 | Details on contributing code 49 | ---------------------------- 50 | 51 | This project is written in `Python `__, and the 52 | documentation is generated using 53 | `Sphinx `__. 54 | `setuptools `__ 55 | and `Invoke `__ are used to build and manage 56 | the project. Tests are written and executed using 57 | `pytest `__ and `tox `__. 58 | 59 | Set up a working development environment 60 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 61 | 62 | To set up a working directory from your own fork, follow `these 63 | steps `__, 64 | but replace the repository ``https`` URLs with SSH ones that point to 65 | your fork. 66 | 67 | For that to work on Debian type systems, you need the ``git``, 68 | ``python``, and ``python-virtualenv`` packages installed. Other 69 | distributions are similar. 70 | 71 | Add your changes to a feature branch 72 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 73 | 74 | For any cohesive set of changes, create a *new* branch based on the 75 | current upstream ``master``, with a name reflecting the essence of your 76 | improvement. 77 | 78 | .. code:: sh 79 | 80 | git branch "name-for-my-fixes" origin/master 81 | git checkout "name-for-my-fixes" 82 | … make changes… 83 | invoke ci # check output for broken tests, or PEP8 violations and the like 84 | … commit changes… 85 | git push origin "name-for-my-fixes" 86 | 87 | Please don't create large lumps of unrelated changes in a single pull 88 | request. Also take extra care to avoid spurious changes, like mass 89 | whitespace diffs. All Python sources use spaces to indent, not TABs. 90 | 91 | Make sure your changes work 92 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 93 | 94 | Some things that will increase the chance that your pull request is 95 | accepted: 96 | 97 | - Follow style conventions you see used in the source already (and read 98 | `PEP8 `__). 99 | - Include tests that fail *without* your code, and pass *with* it. Only 100 | minor refactoring and documentation changes require no new tests. If 101 | you are adding functionality or fixing a bug, please also add a test 102 | for it! 103 | - Update any documentation or examples impacted by your change. 104 | - Styling conventions and code quality are checked with 105 | ``invoke check``, tests are run using ``invoke test``, and the docs 106 | can be built locally using ``invoke build --docs``. 107 | 108 | Following these hints also expedites the whole procedure, since it 109 | avoids unnecessary feedback cycles. 110 | -------------------------------------------------------------------------------- /docs/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | /* Custom styles */ 2 | 3 | @import url("theme.css"); 4 | 5 | code, .rst-content tt, .rst-content code { 6 | font-size: 95%; 7 | } 8 | .note p code, .note div div pre { 9 | background-color: #E7F2FC; 10 | } 11 | .warning p code, .warning div div pre { 12 | background-color: #FFF8DD; 13 | } 14 | -------------------------------------------------------------------------------- /docs/_static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhermann/rituals/39022c50d0072428fb7392d3c3a71eaaa636e261/docs/_static/img/favicon.ico -------------------------------------------------------------------------------- /docs/_static/img/invoke-release-prep-changes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhermann/rituals/39022c50d0072428fb7392d3c3a71eaaa636e261/docs/_static/img/invoke-release-prep-changes.png -------------------------------------------------------------------------------- /docs/_static/img/jenkins-description.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhermann/rituals/39022c50d0072428fb7392d3c3a71eaaa636e261/docs/_static/img/jenkins-description.png -------------------------------------------------------------------------------- /docs/_static/img/logo-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhermann/rituals/39022c50d0072428fb7392d3c3a71eaaa636e261/docs/_static/img/logo-200.png -------------------------------------------------------------------------------- /docs/_static/img/logo-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhermann/rituals/39022c50d0072428fb7392d3c3a71eaaa636e261/docs/_static/img/logo-60.png -------------------------------------------------------------------------------- /docs/_static/img/py-generic-project-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhermann/rituals/39022c50d0072428fb7392d3c3a71eaaa636e261/docs/_static/img/py-generic-project-logo.png -------------------------------------------------------------------------------- /docs/_static/img/symbol-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhermann/rituals/39022c50d0072428fb7392d3c3a71eaaa636e261/docs/_static/img/symbol-200.png -------------------------------------------------------------------------------- /docs/_static/img/symbol-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhermann/rituals/39022c50d0072428fb7392d3c3a71eaaa636e261/docs/_static/img/symbol-48.png -------------------------------------------------------------------------------- /docs/_static/img/symbol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 41 | 43 | 44 | 46 | image/svg+xml 47 | 49 | 50 | 51 | 52 | 53 | 58 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /docs/announce.txt: -------------------------------------------------------------------------------- 1 | Version 0.3.0 of `rituals` adds a complete set of documentation, 2 | support for Sphinx including options to conveniently control a 3 | 'sphinx-autobuild' daemon, and a 'release.pex' task for single- 4 | file Python application releases (optionally embedding a Python 5 | interpreter). 6 | 7 | There is also a mailing list now (Google groups), see "Links" 8 | below. 9 | 10 | 11 | ABOUT RITUALS 12 | ============= 13 | 14 | The `rituals` package provides PyInvoke tasks that work for any 15 | project, based on its project metadata, to automate common 16 | developer chores like 'clean', 'build', 'dist', 'test', 'check', 17 | and 'release-prep' (for the moment). 18 | 19 | The guiding principle for these tasks is to strictly separate 20 | low-level tasks for building and installing (via setup.py) from 21 | high-level convenience tasks a developer uses (via tasks.py). 22 | Invoke tasks can use Setuptools ones as building blocks, but 23 | never the other way 'round – this avoids bootstrapping head- 24 | aches during package installations using `pip`. 25 | 26 | The easiest way to get a working project using `rituals` is 27 | the `py-generic-project` cookiecutter template. That way you have 28 | a working project skeleton within minutes that is fully equipped, 29 | with all aspects of bootstrapping, building, testing, quality 30 | checks, continuous integration, documentation, and releasing 31 | covered. 32 | 33 | Enjoy, Jürgen 34 | 35 | ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– 36 | Links: 37 | 38 | * https://pypi.python.org/pypi/rituals/0.3.0 39 | * https://rituals.readthedocs.org/en/latest/ 40 | * https://groups.google.com/forum/#!forum/rituals-dev 41 | * http://springerle.github.io/py-generic-project/ 42 | 43 | Detailed changes: 44 | * new: 'docs.sphinx' and 'docs.upload' task (to PyPI/WebDAV/Artifactory) 45 | * new: convenient control of 'sphinx-autobuild' daemon 46 | * new: added 'test' namespace and 'tox' task 47 | * new: 'release.pex' to create PEX artifacts 48 | * new: optional inclusion of PyRun in PEX artifacts 49 | * new: 'release.bump' to create unique development versions 50 | * new: build a DEB package (new 'deb' task) 51 | * new: 'devpi' namespace with a 'refresh' task 52 | * new: added 'freeze' as a root task 53 | * new: 'jenkins.description' to display project metadata in the Jenkins UI 54 | * new: support for projects with 'py_modules' 55 | * new: 'util.filesys' module with a 'pushd' helper 56 | * new: 'with_root' param for FileSet.walk() 57 | * chg: 'test.pytest' as the default in 'test' namespace 58 | * chg: 'prep' renamed to 'release.prep' 59 | * chg: added '--clean' option to 'test.tox', and 'clean --tox' 60 | * chg: exclude SCM dirs from 'clean' 61 | * chg: improved autoenv script 62 | * chg: more unit tests (still missing a lot) 63 | * chg: Python 3.4 support 64 | * fix: non-git workdirs are handled correctly in 'release.prep' 65 | 66 | See also https://github.com/jhermann/rituals/releases/tag/v0.3.0 67 | -------------------------------------------------------------------------------- /docs/api-reference.rst: -------------------------------------------------------------------------------- 1 | .. full API docs 2 | 3 | Copyright ⓒ 2015 Jürgen Hermann 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License version 2 as 7 | published by the Free Software Foundation. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | The full LICENSE file and source are available at 19 | https://github.com/jhermann/rituals 20 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 21 | 22 | 23 | Complete API Reference 24 | ====================== 25 | 26 | The following is a complete API reference generated from source. 27 | 28 | .. toctree:: 29 | :maxdepth: 4 30 | 31 | api/rituals 32 | -------------------------------------------------------------------------------- /docs/api/modules.rst: -------------------------------------------------------------------------------- 1 | rituals 2 | ======= 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | rituals 8 | -------------------------------------------------------------------------------- /docs/api/rituals.acts.rst: -------------------------------------------------------------------------------- 1 | rituals.acts package 2 | ==================== 3 | 4 | .. automodule:: rituals.acts 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | rituals.acts.basic module 13 | ------------------------- 14 | 15 | .. automodule:: rituals.acts.basic 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | rituals.acts.devpi module 21 | ------------------------- 22 | 23 | .. automodule:: rituals.acts.devpi 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | rituals.acts.documentation module 29 | --------------------------------- 30 | 31 | .. automodule:: rituals.acts.documentation 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | rituals.acts.github module 37 | -------------------------- 38 | 39 | .. automodule:: rituals.acts.github 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | rituals.acts.inspection module 45 | ------------------------------ 46 | 47 | .. automodule:: rituals.acts.inspection 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | rituals.acts.jenkins module 53 | --------------------------- 54 | 55 | .. automodule:: rituals.acts.jenkins 56 | :members: 57 | :undoc-members: 58 | :show-inheritance: 59 | 60 | rituals.acts.pkgdeb module 61 | -------------------------- 62 | 63 | .. automodule:: rituals.acts.pkgdeb 64 | :members: 65 | :undoc-members: 66 | :show-inheritance: 67 | 68 | rituals.acts.releasing module 69 | ----------------------------- 70 | 71 | .. automodule:: rituals.acts.releasing 72 | :members: 73 | :undoc-members: 74 | :show-inheritance: 75 | 76 | rituals.acts.testing module 77 | --------------------------- 78 | 79 | .. automodule:: rituals.acts.testing 80 | :members: 81 | :undoc-members: 82 | :show-inheritance: 83 | -------------------------------------------------------------------------------- /docs/api/rituals.rst: -------------------------------------------------------------------------------- 1 | rituals package 2 | =============== 3 | 4 | .. automodule:: rituals 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | rituals.acts 16 | rituals.util 17 | 18 | Submodules 19 | ---------- 20 | 21 | rituals.config module 22 | --------------------- 23 | 24 | .. automodule:: rituals.config 25 | :members: 26 | :undoc-members: 27 | :show-inheritance: 28 | 29 | rituals.easy module 30 | ------------------- 31 | 32 | .. automodule:: rituals.easy 33 | :members: 34 | :undoc-members: 35 | :show-inheritance: 36 | -------------------------------------------------------------------------------- /docs/api/rituals.util.rst: -------------------------------------------------------------------------------- 1 | rituals.util package 2 | ==================== 3 | 4 | .. automodule:: rituals.util 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | rituals.util.scm 16 | 17 | Submodules 18 | ---------- 19 | 20 | rituals.util.antglob module 21 | --------------------------- 22 | 23 | .. automodule:: rituals.util.antglob 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | rituals.util.buildsys module 29 | ---------------------------- 30 | 31 | .. automodule:: rituals.util.buildsys 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | rituals.util.filesys module 37 | --------------------------- 38 | 39 | .. automodule:: rituals.util.filesys 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | rituals.util.notify module 45 | -------------------------- 46 | 47 | .. automodule:: rituals.util.notify 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | rituals.util.shell module 53 | ------------------------- 54 | 55 | .. automodule:: rituals.util.shell 56 | :members: 57 | :undoc-members: 58 | :show-inheritance: 59 | 60 | rituals.util.which module 61 | ------------------------- 62 | 63 | .. automodule:: rituals.util.which 64 | :members: 65 | :undoc-members: 66 | :show-inheritance: 67 | -------------------------------------------------------------------------------- /docs/api/rituals.util.scm.rst: -------------------------------------------------------------------------------- 1 | rituals.util.scm package 2 | ======================== 3 | 4 | .. automodule:: rituals.util.scm 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | rituals.util.scm.base module 13 | ---------------------------- 14 | 15 | .. automodule:: rituals.util.scm.base 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | rituals.util.scm.git module 21 | --------------------------- 22 | 23 | .. automodule:: rituals.util.scm.git 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | rituals.util.scm.null module 29 | ---------------------------- 30 | 31 | .. automodule:: rituals.util.scm.null 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # 2 | # Sphinx documentation build configuration file. 3 | # 4 | # This file is execfile()d with the current directory set to its containing dir. 5 | # 6 | # Note that not all possible configuration values are present in this 7 | # autogenerated file. 8 | # 9 | # All configuration values have a default; values that are commented out 10 | # serve to show the default. 11 | 12 | import os 13 | import re 14 | import sys 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath("..")) 20 | sys.path.insert(0, os.path.abspath("../src")) 21 | from setup import project as meta 22 | 23 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 24 | if not on_rtd: 25 | import sphinx_rtd_theme 26 | 27 | # -- General configuration ----------------------------------------------------- 28 | 29 | # If your documentation needs a minimal Sphinx version, state it here. 30 | needs_sphinx = '1.3' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be extensions 33 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 34 | extensions = [ 35 | 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.coverage', 36 | 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # The suffix of source filenames. 43 | source_suffix = '.rst' 44 | 45 | # The encoding of source files. 46 | #source_encoding = 'utf-8-sig' 47 | 48 | # The master toctree document. 49 | master_doc = 'index' 50 | 51 | # General information about the project. 52 | project = meta["name"] 53 | copyright = ' '.join([i for i in meta["long_description"].splitlines() if "Copyright" in i][0].split()[2:]) 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The full version, including alpha/beta/rc tags. 60 | release = meta["version"] 61 | # The short X.Y version. 62 | version = '.'.join(re.split("[^\d]+", release)[:2]) 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | #language = None 67 | 68 | # There are two options for replacing |today|: either, you set today to some 69 | # non-false value, then it is used: 70 | #today = '' 71 | # Else, today_fmt is used as the format for a strftime call. 72 | today_fmt = '%Y-%m-%d' 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | exclude_patterns = [ 77 | "*~", "README.rst", "api/modules.rst", 78 | ] 79 | 80 | # The reST default role (used for this markup: `text`) to use for all documents. 81 | #default_role = None 82 | 83 | # If true, '()' will be appended to :func: etc. cross-reference text. 84 | #add_function_parentheses = True 85 | 86 | # If true, the current module name will be prepended to all description 87 | # unit titles (such as .. function::). 88 | #add_module_names = True 89 | 90 | # If true, sectionauthor and moduleauthor directives will be shown in the 91 | # output. They are ignored by default. 92 | #show_authors = False 93 | 94 | # The name of the Pygments (syntax highlighting) style to use. 95 | pygments_style = 'sphinx' 96 | 97 | # A list of ignored prefixes for module index sorting. 98 | #modindex_common_prefix = [] 99 | 100 | # Napoleon settings 101 | napoleon_numpy_docstring = False 102 | 103 | 104 | # -- Options for HTML output --------------------------------------------------- 105 | if not on_rtd: 106 | # The theme to use for HTML and HTML Help pages. See the documentation for 107 | # a list of builtin themes. 108 | html_theme = 'sphinx_rtd_theme' 109 | 110 | # Add any paths that contain custom themes here, relative to this directory. 111 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 112 | 113 | html_style = 'css/custom.css' 114 | 115 | # Theme options are theme-specific and customize the look and feel of a theme 116 | # further. For a list of options available for each theme, see the 117 | # documentation. 118 | html_theme_options = dict( 119 | ) 120 | 121 | # The name for this set of Sphinx documents. If None, it defaults to 122 | # " v documentation". 123 | #html_title = None 124 | 125 | # A shorter title for the navigation bar. Default is the same as html_title. 126 | #html_short_title = None 127 | 128 | # The name of an image file (relative to this directory) to place at the top 129 | # of the sidebar. 130 | html_logo = "_static/img/symbol-48.png" 131 | 132 | # The name of an image file (within the static path) to use as favicon of the 133 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 134 | # pixels large. 135 | html_favicon = "_static/img/favicon.ico" 136 | 137 | # Add any paths that contain custom static files (such as style sheets) here, 138 | # relative to this directory. They are copied after the builtin static files, 139 | # so a file named "default.css" will overwrite the builtin "default.css". 140 | html_static_path = ['_static'] 141 | 142 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 143 | # using the given strftime format. 144 | html_last_updated_fmt = '%Y-%m-%d' 145 | 146 | # If true, SmartyPants will be used to convert quotes and dashes to 147 | # typographically correct entities. 148 | html_use_smartypants = True 149 | 150 | # Custom sidebar templates, maps document names to template names. 151 | #html_sidebars = {} 152 | 153 | # Additional templates that should be rendered to pages, maps page names to 154 | # template names. 155 | #html_additional_pages = {} 156 | 157 | # If false, no module index is generated. 158 | html_domain_indices = True 159 | 160 | # If false, no index is generated. 161 | html_use_index = True 162 | 163 | # If true, the index is split into individual pages for each letter. 164 | #html_split_index = False 165 | 166 | # If true, links to the reST sources are added to the pages. 167 | #html_show_sourcelink = True 168 | 169 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 170 | html_show_sphinx = False 171 | 172 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 173 | #html_show_copyright = True 174 | 175 | # If true, an OpenSearch description file will be output, and all pages will 176 | # contain a tag referring to it. The value of this option must be the 177 | # base URL from which the finished HTML is served. 178 | #html_use_opensearch = '' 179 | 180 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 181 | #html_file_suffix = None 182 | 183 | # Output file base name for HTML help builder. 184 | htmlhelp_basename = project + 'doc' 185 | 186 | 187 | # -- Options for LaTeX output -------------------------------------------------- 188 | 189 | latex_elements = { 190 | # The paper size ('letterpaper' or 'a4paper'). 191 | #'papersize': 'letterpaper', 192 | 193 | # The font size ('10pt', '11pt' or '12pt'). 194 | #'pointsize': '10pt', 195 | 196 | # Additional stuff for the LaTeX preamble. 197 | #'preamble': '', 198 | } 199 | 200 | # Grouping the document tree into LaTeX files. List of tuples 201 | # (source start file, target name, title, author, documentclass [howto/manual]). 202 | latex_documents = [ 203 | ('index', project + '.tex', project + u' Documentation', meta["author"], 'manual'), 204 | ] 205 | 206 | # The name of an image file (relative to this directory) to place at the top of 207 | # the title page. 208 | #latex_logo = None 209 | 210 | # For "manual" documents, if this is true, then toplevel headings are parts, 211 | # not chapters. 212 | #latex_use_parts = False 213 | 214 | # If true, show page references after internal links. 215 | #latex_show_pagerefs = False 216 | 217 | # If true, show URL addresses after external links. 218 | #latex_show_urls = False 219 | 220 | # Documents to append as an appendix to all manuals. 221 | #latex_appendices = [] 222 | 223 | # If false, no module index is generated. 224 | #latex_domain_indices = True 225 | 226 | 227 | # -- Options for manual page output -------------------------------------------- 228 | 229 | # One entry per manual page. List of tuples 230 | # (source start file, name, description, authors, manual section). 231 | man_pages = [ 232 | ('index', project, project + u' Documentation', [meta["author"]], 1) 233 | ] 234 | 235 | # If true, show URL addresses after external links. 236 | #man_show_urls = False 237 | 238 | 239 | # -- Options for Texinfo output ------------------------------------------------ 240 | 241 | # Grouping the document tree into Texinfo files. List of tuples 242 | # (source start file, target name, title, author, 243 | # dir menu entry, description, category) 244 | texinfo_documents = [ 245 | ('index', project, project + u' Documentation', meta["author"], project, meta["description"], 'Miscellaneous'), 246 | ] 247 | 248 | # Documents to append as an appendix to all manuals. 249 | #texinfo_appendices = [] 250 | 251 | # If false, no module index is generated. 252 | #texinfo_domain_indices = True 253 | 254 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 255 | #texinfo_show_urls = 'footnote' 256 | -------------------------------------------------------------------------------- /docs/copyright.rst: -------------------------------------------------------------------------------- 1 | .. copyright redirect 2 | 3 | The name 'copyright' in the top-level toc-tree is special, 4 | and makes 'Copyright' at the bottom of each page a link. 5 | 6 | .. include:: LICENSE.rst 7 | -------------------------------------------------------------------------------- /docs/customize.rst: -------------------------------------------------------------------------------- 1 | .. documentation: customize 2 | 3 | Copyright ⓒ 2015 Jürgen Hermann 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License version 2 as 7 | published by the Free Software Foundation. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | The full LICENSE file and source are available at 19 | https://github.com/jhermann/rituals 20 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 21 | 22 | Configuration Reference 23 | ======================= 24 | 25 | Please read `Invoke's Configuration Guide`_ on the concepts and basic mechanisms 26 | of its hierarchy of configuration files, environment variables, task namespaces 27 | and CLI flags. 28 | This reference guide lists the configuration options specific to tasks provided by *Rituals*. 29 | 30 | .. note:: 31 | 32 | In the following tables of configurations settings, the root namespace of 33 | 'rituals' is implied, so to access them in a task you'd use ``ctx.rituals.‹name›``, 34 | and ``INVOKE_RITUALS_‹NAME›`` to define an environment variable. 35 | 36 | 37 | ----------------------------------------------------------------------------- 38 | General Options 39 | ----------------------------------------------------------------------------- 40 | 41 | To make Python versions available that are not part of the host's default 42 | installation, ``rituals.snakepits`` is used, e.g. when performing multi-environment 43 | testing. The default is ``/opt/pyenv/bin:/opt/pyrun/bin``. 44 | 45 | =================== ========================================================= 46 | Name Description 47 | =================== ========================================================= 48 | snakepits Lookup path for Python interpreters 49 | =================== ========================================================= 50 | 51 | 52 | .. _customize-test: 53 | 54 | ----------------------------------------------------------------------------- 55 | Options for 'test' 56 | ----------------------------------------------------------------------------- 57 | 58 | If one of the directories in ``rituals.snakepits`` exists, it's added to the 59 | ``PATH`` of ``tox``. 60 | 61 | 62 | .. _customize-docs: 63 | 64 | ----------------------------------------------------------------------------- 65 | Options for 'docs' 66 | ----------------------------------------------------------------------------- 67 | 68 | The defaults for the ``docs`` task should almost always fit, but if you need 69 | to change them, you can. 70 | 71 | =================== ========================================================= 72 | Name Description 73 | =================== ========================================================= 74 | docs.sources Documentation source folder (``docs``) 75 | docs.build Build area within the source folder (``_build``) 76 | docs.watchdog.host IP to bind ``sphinx-autobuild`` to (``127.0.0.1``) 77 | docs.watchdog.port Port to bind ``sphinx-autobuild`` to (``8840``) 78 | =================== ========================================================= 79 | 80 | 81 | .. _customize-release: 82 | 83 | ----------------------------------------------------------------------------- 84 | Options for 'release' 85 | ----------------------------------------------------------------------------- 86 | 87 | When ``release.prep`` changes the project configuration for a release and then 88 | tags the resulting changeset, the values from the following table are used for 89 | messages and names. 90 | 91 | ======================= ===================================================== 92 | Name Description 93 | ======================= ===================================================== 94 | release.commit.message Message used (``:package: Release v{version}``) 95 | release.tag.name Release tag (``v{version}``) 96 | release.tag.message Tag annotation (``Release v{version}``) 97 | ======================= ===================================================== 98 | 99 | 100 | The ``release.pex`` task has an ``--upload`` option to upload the created archive 101 | to a WebDAV repository, e.g. a local `Artifactory`_ server or to `Bintray`_. 102 | The best way to make this usable in each of your projects is to insert the base URL 103 | of your Python repository into your shell environment: 104 | 105 | .. code:: sh 106 | 107 | export INVOKE_RITUALS_RELEASE_UPLOAD_BASE_URL=\ 108 | "http://repo.example.com/artifactory/pypi-releases-local/" 109 | 110 | ======================= ===================================================== 111 | Name Description 112 | ======================= ===================================================== 113 | release.upload.base_url WebDAV server end-point 114 | release.upload.path WebDAV path (``{name}/{version}/{filename}``) 115 | ======================= ===================================================== 116 | 117 | 118 | The following settings are used when building self-contained releases that integrate `eGenix PyRun`_. 119 | 120 | =================== ========================================================= 121 | Name Description 122 | =================== ========================================================= 123 | pyrun.version The version of *PyRun* to use (e.g. ``2.1.0``) 124 | pyrun.python The version of *Python* to use (``2.6``, ``2.7``, 125 | or ``3.4``) 126 | pyrun.ucs Unicode code points size (``ucs2`` or ``ucs4``) 127 | pyrun.platform The platform ID (e.g. ``linux-x86_64``, 128 | ``macosx-10.5-x86_64``) 129 | pyrun.base_url Download location base URL pattern 130 | pyrun.archive Download location file name pattern 131 | =================== ========================================================= 132 | 133 | The ``rituals.pyrun.base_url`` value can be a local ``http[s]`` URL 134 | of an `Artifactory`_ repository or some similar webserver, or else 135 | a ``file://`` URL of a file system cache. Note that you should keep the 136 | unchanged name of the original download location, i.e. do not change 137 | ``rituals.pyrun.archive``. The location patterns can contain the ``pyrun`` 138 | settings as placeholders, e.g. ``{version}``. 139 | 140 | This sets a local download cache: 141 | 142 | .. code:: sh 143 | 144 | export INVOKE_RITUALS_PYRUN_BASE_URL="file://$HOME/Downloads" 145 | 146 | You have to download the *PyRun* releases you plan to use to that directory, 147 | using your browser or ``curl``. 148 | 149 | 150 | .. _customize-devpi: 151 | 152 | ----------------------------------------------------------------------------- 153 | Options for 'devpi' 154 | ----------------------------------------------------------------------------- 155 | 156 | When you call the ``devpi.refresh`` task without any option, the value of 157 | ``rituals.devpi.requirements`` is the name of the file parsed for the list 158 | of packages to refresh in the active ``devpi`` server. It defaults to 159 | ``dev-requirements.txt``. 160 | 161 | =================== ========================================================= 162 | Name Description 163 | =================== ========================================================= 164 | devpi.requirements Name of requirements file to use for refreshing 165 | =================== ========================================================= 166 | 167 | 168 | .. _`Invoke's Configuration Guide`: https://invoke.readthedocs.org/en/latest/concepts/configuration.html 169 | .. _`Artifactory`: http://www.jfrog.com/open-source/#os-arti 170 | .. _`Bintray`: https://bintray.com/ 171 | .. _`eGenix PyRun`: https://www.egenix.com/products/python/PyRun/ 172 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. documentation master file 2 | 3 | Copyright ⓒ 2015 Jürgen Hermann 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License version 2 as 7 | published by the Free Software Foundation. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | The full LICENSE file and source are available at 19 | https://github.com/jhermann/rituals 20 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 21 | 22 | Welcome to the “Rituals” manual! 23 | ================================ 24 | 25 | .. image:: https://img.shields.io/pypi/v/rituals.svg?style=flat 26 | :alt: Latest Version 27 | :target: https://pypi.python.org/pypi/rituals/ 28 | 29 | .. image:: https://api.travis-ci.org/jhermann/rituals.svg?style=flat 30 | :alt: Travis CI 31 | :target: https://travis-ci.org/jhermann/rituals 32 | 33 | .. image:: https://img.shields.io/coveralls/jhermann/rituals.svg?style=flat 34 | :alt: Coveralls 35 | :target: https://coveralls.io/r/jhermann/rituals 36 | 37 | .. image:: https://img.shields.io/github/issues/jhermann/rituals.svg?style=flat 38 | :alt: GitHub Issues 39 | :target: https://github.com/jhermann/rituals/issues 40 | 41 | 42 | Overview 43 | -------- 44 | 45 | “Rituals” is a task library for `Invoke `_ 46 | that keeps the most common tasks you always need out of your project, 47 | and makes them centrally maintained. This leaves your ``tasks.py`` small 48 | and to the point, with only things specific to the project at hand. 49 | 50 | For more information, continue with the :doc:`usage`. 51 | 52 | 53 | Important Links 54 | --------------- 55 | 56 | * `GitHub Project `_ 57 | * `Issue Tracker `_ 58 | * `PyPI `_ 59 | * `Latest Documentation `_ 60 | * `Google Group `_ 61 | 62 | 63 | Documentation Contents 64 | ---------------------- 65 | 66 | .. toctree:: 67 | :maxdepth: 4 68 | 69 | usage 70 | tasks 71 | customize 72 | api-reference 73 | CONTRIBUTING 74 | copyright 75 | 76 | 77 | References 78 | ---------- 79 | 80 | Tools 81 | ^^^^^ 82 | 83 | * `Cookiecutter `_ 84 | * `PyInvoke `_ 85 | * `pytest `_ 86 | * `tox `_ 87 | * `Pylint `_ 88 | * `twine `_ 89 | * `bpython `_ 90 | * `yolk3k `_ 91 | 92 | 93 | Indices and Tables 94 | ------------------ 95 | 96 | * :ref:`genindex` 97 | * :ref:`modindex` 98 | * :ref:`search` 99 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # "Read The Docs" requirements 3 | # 4 | 5 | -r ../requirements.txt 6 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | .. documentation: usage 2 | 3 | Copyright ⓒ 2015 Jürgen Hermann 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License version 2 as 7 | published by the Free Software Foundation. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | The full LICENSE file and source are available at 19 | https://github.com/jhermann/rituals 20 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 21 | 22 | User's Manual 23 | ============= 24 | 25 | ----------------------------------------------------------------------------- 26 | Introduction 27 | ----------------------------------------------------------------------------- 28 | 29 | “Rituals” is a task library for `Invoke `_ 30 | that keeps the most common tasks you always need out of your project, 31 | and makes them centrally maintained. This leaves your ``tasks.py`` small 32 | and to the point, with only things specific to the project at hand. 33 | 34 | The following lists the common task implementations that the 35 | ``rituals.easy`` module offers. See :ref:`below ` 36 | on how to integrate them into your ``tasks.py``. 37 | 38 | * ``help`` – Default task, when invoked with no task names. 39 | * ``clean`` – Perform house-cleaning. 40 | * ``build`` – Build the project. 41 | * ``docs`` – Build the documentation. 42 | * ``test`` – Perform standard unittests. 43 | * ``check`` – Perform source code checks. 44 | * ``release.bump`` – Bump a development version. 45 | * ``release.dist`` – Distribute the project. 46 | * ``release.prep`` – Prepare for a release. 47 | * … and *many* more, see ``inv -l`` for a complete list. 48 | 49 | The guiding principle for these tasks is to strictly separate low-level 50 | tasks for building and installing (via ``setup.py``) from high-level 51 | convenience tasks a developer uses (via ``invoke``). *Invoke* tasks can 52 | use *Setuptools* ones as building blocks, but never the other way 'round – 53 | this avoids any bootstrapping headaches during package installations. 54 | 55 | Use ``inv -h ‹task›`` as usual to get details on the options of these 56 | tasks. 57 | The :doc:`tasks` explains them in more detail. 58 | Look at the modules in 59 | `rituals.acts `__ 60 | if you want to know every nuance of what these tasks do. 61 | 62 | .. note:: 63 | 64 | .. image:: _static/img/py-generic-project-logo.png 65 | :align: left 66 | 67 | The easiest way to get a working project using ``rituals`` is the 68 | `py-generic-project`_ cookiecutter archetype, which is tightly 69 | integrated with the tasks defined here. 70 | 71 | That way you have a working project skeleton 72 | within minutes that is fully equipped, with all aspects of building, 73 | testing, quality checks, continuous integration, documentation, and 74 | releasing covered. 75 | 76 | 77 | .. _import-rituals-easy: 78 | 79 | ----------------------------------------------------------------------------- 80 | Adding Rituals to Your Project 81 | ----------------------------------------------------------------------------- 82 | 83 | First of all, include ``rituals`` as a dependency in your ``dev-requirements.txt`` 84 | or a similar file, to get a release from PyPI. 85 | To refer to the current GitHub ``master`` branch instead, use a ``pip`` 86 | requirement like this:: 87 | 88 | -e git+https://github.com/jhermann/rituals.git#egg=rituals 89 | 90 | Then at the start of your ``tasks.py``, use the following statement to define 91 | *all* tasks that are considered standard: 92 | 93 | .. code:: py 94 | 95 | from rituals.easy import * 96 | 97 | This works by defining the ``namespace`` identifier containing Ritual's default tasks. 98 | Note that it also defines Invoke's ``Collection`` and ``task`` identifiers, 99 | and some other common helpers assembled in :py:mod:`rituals.easy`. 100 | `Rituals' own tasks.py`_ can serve as an example. 101 | 102 | Of course, you may also do more selective imports, or build your own 103 | *Invoke* namespaces with the specific tasks you need. 104 | 105 | .. warning:: 106 | 107 | These tasks expect an importable ``setup.py`` that defines 108 | a ``project`` dict with the setup parameters, see 109 | `rudiments `_ and 110 | `py-generic-project`_ for examples. The needed changes are minimal: 111 | 112 | .. code:: py 113 | 114 | project = dict( # this would usually be a setup(…) call 115 | name='…', 116 | ... 117 | ) 118 | if __name__ == '__main__': 119 | setup(**project) 120 | 121 | 122 | .. _`py-generic-project`: https://github.com/Springerle/py-generic-project 123 | 124 | 125 | .. _task-namespaces: 126 | 127 | ----------------------------------------------------------------------------- 128 | Task Namespaces 129 | ----------------------------------------------------------------------------- 130 | 131 | The Root Namespace 132 | ^^^^^^^^^^^^^^^^^^ 133 | 134 | The tasks useful for any (Python) project are organized in a root namespace. 135 | When you use the ``from rituals.easy import *`` statement, that also imports 136 | this root namespace. By convention of *Invoke*, when the identifier ``namespace`` 137 | is defined, that one is taken instead of constructing one automatically from 138 | all defined tasks. 139 | 140 | It contains some fundamentals like ``clean``, and nested namespaces handling 141 | specific topics. Examples of nested namespaces are ``test``, ``check``, 142 | ``docs``, and ``release``. See :doc:`tasks` for a complete list. 143 | 144 | The root namespace has ``help`` as the default task, and 145 | most nested namespaces also have a default with the most commonly performed 146 | action. These default tasks are automatically aliased to the name of the 147 | namespace, so for example ``docs.sphinx`` can also be called as ``docs``. 148 | 149 | 150 | Adding Local Task Definitions 151 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 152 | 153 | Having an explicit root namespace 154 | means that within ``tasks.py``, you need to register your own tasks 155 | using its ``add_task`` method, if you want them to be 156 | available as top-level names: 157 | 158 | .. code:: py 159 | 160 | @task 161 | def my_own_task(ctx): 162 | """Something project-specific.""" 163 | ... 164 | 165 | namespace.add_task(my_own_task) 166 | 167 | `Rituals' own tasks.py`_ uses this to add some local tasks. 168 | 169 | Another strategy is to add them in bulk, 170 | so when you write a new task you cannot forget to make it visible: 171 | 172 | .. code:: py 173 | 174 | # Register local tasks in root namespace 175 | from invoke import Task 176 | for _task in globals().values(): 177 | if isinstance(_task, Task) and _task.body.__module__ == __name__: 178 | namespace.add_task(_task) 179 | 180 | Add the above snippet to the *end* of your ``tasks.py``, 181 | and every *local* task definition gets added to the root namespace. 182 | 183 | 184 | Constructing Your Own Namespace 185 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 186 | 187 | When you want to have more control, you can exclude the ``namespace`` 188 | identifier from the import and instead define your own. 189 | This example taken from the 190 | `tasks.py of py-generic-project `_ 191 | shows how it's done: 192 | 193 | .. code:: py 194 | 195 | from rituals.easy import task, Collection 196 | from rituals.acts.documentation import namespace as _docs 197 | 198 | ... 199 | 200 | namespace = Collection.from_module(sys.modules[__name__], name='') 201 | namespace.add_collection(_docs) 202 | 203 | Note that the ``name=''`` makes this a root namespace. 204 | If you need to be even more selective, import individual tasks from modules 205 | in :py:mod:`rituals.acts` and add them to your namespaces. 206 | 207 | 208 | .. _`Rituals' own tasks.py`: https://github.com/jhermann/rituals/blob/master/tasks.py#L3 209 | 210 | 211 | ----------------------------------------------------------------------------- 212 | How-Tos 213 | ----------------------------------------------------------------------------- 214 | 215 | Change default project layout 216 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 217 | 218 | By default, sources are expected in ``src/‹packagename›`` and tests in 219 | ``src/tests``. However, if you import ``rituals.easy``, an auto-detection 220 | for other layouts (described below) is performed, and only if that fails 221 | for some reason, you need to set the layout type explicitly. 222 | 223 | You can change the layout by calling one of the following functions, directly 224 | after the import from ``rituals.invoke_tasks``. 225 | 226 | * ``config.set_maven_layout()`` – Changes locations to 227 | ``src/main/python/‹packagename›`` and ``src/test/python``. 228 | * ``config.set_flat_layout()`` – Changes locations to ``‹packagename›`` 229 | and ``tests``. 230 | 231 | 232 | Change default project configuration 233 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 234 | 235 | If you want to override the configuration defaults of various tasks, 236 | without using environment variables, add an ``invoke.yaml`` file in 237 | the same directory where your ``tasks.py`` is located 238 | – usually the project root directory. 239 | 240 | This example makes *Sphinx* (as called by the default ``docs`` task) 241 | place generated files in the top-level ``build`` directory instead 242 | of a sub-directory in ``docs``. 243 | 244 | .. literalinclude:: ../invoke.yaml 245 | :caption: invoke.yaml 246 | :language: yaml 247 | :emphasize-lines: 3 248 | 249 | See :doc:`customize` for a list of possible options. 250 | -------------------------------------------------------------------------------- /frozen-requirements.txt: -------------------------------------------------------------------------------- 1 | # Requirements frozen by 'pip freeze' on 2021-08-02 12:53:17 2 | alabaster==0.7.12 3 | appdirs==1.4.4 4 | argh==0.26.2 5 | astroid==2.6.5 6 | attrs==19.3.0 7 | Babel==2.8.0 8 | bleach==3.1.5 9 | blessings==1.7 10 | bpython==0.21 11 | certifi==2020.6.20 12 | cffi==1.14.6 13 | chardet==3.0.4 14 | click==7.1.2 15 | colorama==0.4.4 16 | colorclass==2.2.0 17 | coverage==5.5 18 | coveralls==3.2.0 19 | cryptography==3.4.7 20 | curtsies==0.3.5 21 | cwcwidth==0.1.4 22 | distlib==0.3.0 23 | docopt==0.6.2 24 | docutils==0.16 25 | filelock==3.0.12 26 | greenlet==0.4.16 27 | idna==2.9 28 | imagesize==1.2.0 29 | importlib-metadata==4.6.3 30 | invoke==1.6.0 31 | isort==4.3.21 32 | jeepney==0.7.1 33 | Jinja2==2.11.2 34 | keyring==23.0.1 35 | lazy-object-proxy==1.4.3 36 | livereload==2.6.2 37 | MarkupSafe==1.1.1 38 | mccabe==0.6.1 39 | mock==4.0.2 40 | more-itertools==8.4.0 41 | munch==2.5.0 42 | packaging==20.4 43 | pathtools==0.1.2 44 | pex==2.1.44 45 | pip-upgrader==1.4.15 46 | pkginfo==1.5.0.1 47 | pluggy==0.13.1 48 | port-for==0.3.1 49 | py==1.9.0 50 | pycparser==2.20 51 | Pygments==2.6.1 52 | pylint==2.9.6 53 | pyparsing==2.4.7 54 | pytest==5.3.5 55 | pytest-cov==2.12.1 56 | pytest-spec==3.2.0 57 | pytz==2020.1 58 | pyxdg==0.27 59 | PyYAML==5.3.1 60 | readme-renderer==26.0 61 | requests==2.24.0 62 | requests-toolbelt==0.9.1 63 | rfc3986==1.5.0 64 | -e git+ssh://git@github.com/jhermann/rituals.git@8a9a49e39ff427cda66ad3f28d173549dc42a4ff#egg=rituals 65 | SecretStorage==3.3.1 66 | shiv==0.5.2 67 | six==1.15.0 68 | snowballstemmer==2.0.0 69 | Sphinx==3.5.4 70 | sphinx-autobuild==2021.3.14 71 | sphinx-rtd-theme==0.5.2 72 | sphinxcontrib-applehelp==1.0.2 73 | sphinxcontrib-devhelp==1.0.2 74 | sphinxcontrib-htmlhelp==1.0.3 75 | sphinxcontrib-jsmath==1.0.1 76 | sphinxcontrib-qthelp==1.0.3 77 | sphinxcontrib-serializinghtml==1.1.4 78 | terminaltables==3.1.0 79 | toml==0.10.1 80 | tornado==6.0.4 81 | tox==3.24.1 82 | tqdm==4.46.1 83 | twine==3.4.2 84 | urllib3==1.25.9 85 | virtualenv==20.0.25 86 | watchdog==0.10.3 87 | wcwidth==0.2.5 88 | webencodings==0.5.1 89 | wrapt==1.12.1 90 | yolk3k==0.9 91 | zipp==3.5.0 92 | -------------------------------------------------------------------------------- /invoke.yaml: -------------------------------------------------------------------------------- 1 | rituals: 2 | docs: 3 | build: ../build/_html 4 | -------------------------------------------------------------------------------- /project.d/classifiers.txt: -------------------------------------------------------------------------------- 1 | # Details at http://pypi.python.org/pypi?:action=list_classifiers 2 | Development Status :: 5 - Production/Stable 3 | Environment :: Console 4 | Intended Audience :: Developers 5 | License :: OSI Approved :: GNU General Public License v2 (GPLv2) 6 | Operating System :: OS Independent 7 | Programming Language :: Python :: 3.6 8 | Programming Language :: Python :: 3.7 9 | Programming Language :: Python :: 3.8 10 | Programming Language :: Python :: 3.9 11 | Topic :: Software Development 12 | Topic :: Software Development :: Build Tools 13 | Topic :: Utilities 14 | -------------------------------------------------------------------------------- /project.d/coverage.cfg: -------------------------------------------------------------------------------- 1 | # 2 | # Coveragerc configuration, for details see 3 | # 4 | # http://nedbatchelder.com/code/coverage/config.html 5 | # 6 | 7 | [run] 8 | branch = True 9 | ; data_file = build/coverage.db 10 | 11 | omit = 12 | */_compat.py 13 | */tests/*.py 14 | 15 | 16 | [report] 17 | ignore_errors = True 18 | 19 | # Regex 'find' matches for lines to exclude from consideration 20 | exclude_lines = 21 | # Have to re-enable the standard pragma 22 | pragma: no cover 23 | 24 | # Don't complain about missing debug-only code 25 | def __repr__ 26 | if self\.debug 27 | 28 | # Don't complain if tests don't hit defensive assertion code 29 | raise AssertionError 30 | raise NotImplementedError 31 | 32 | # Don't complain if non-runnable code isn't run 33 | if 0: 34 | if False: 35 | if __name__ == .__main__.: 36 | 37 | 38 | [xml] 39 | output = build/coverage.xml 40 | 41 | 42 | [html] 43 | directory = build/coverage_html_report 44 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Install requirements 3 | # 4 | 5 | munch>2,<3 6 | invoke>=1.5 7 | toml>=0.10.1 8 | #requests>=2.9 ; python_version >= '2.7.9' 9 | #requests[security]>=2.9 ; python_version < '2.7.9' 10 | requests>=2.9 11 | pathlib2 ; python_version == "2.7" 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration for setuptools 3 | # 4 | 5 | [egg_info] 6 | tag_build = dev 7 | tag_date = false 8 | 9 | [sdist] 10 | formats = zip 11 | 12 | [bdist_wheel] 13 | universal = 0 14 | 15 | [tool:pytest] 16 | norecursedirs = .* *.egg *.egg-info bin dist include lib local share static docs 17 | python_files = src/tests/test_*.py 18 | addopts = --spec 19 | 20 | [flake8] 21 | #ignore = E226,... 22 | max-line-length = 132 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # pylint: disable=attribute-defined-outside-init, invalid-name 4 | # pylint: disable=wrong-import-position 5 | """ rituals – Common tasks for 'Invoke' that are needed again and again. 6 | 7 | This setuptools script follows the DRY principle and tries to 8 | minimize repetition of project metadata by loading it from other 9 | places (like the package's `__init__.py`). Incidently, this makes 10 | the script almost identical between different projects. 11 | 12 | It is also importable (by using the usual `if __name__ == '__main__'` 13 | idiom), and exposes the project's setup data in a `project` dict. 14 | This allows other tools to exploit the data assembling code contained 15 | in here, and again supports the DRY principle. The `rituals` package 16 | uses that to provide Invoke tasks that work for any project, based on 17 | its project metadata. 18 | 19 | Copyright ⓒ 2015 - 2019 Jürgen Hermann 20 | 21 | This program is free software; you can redistribute it and/or modify 22 | it under the terms of the GNU General Public License version 2 as 23 | published by the Free Software Foundation. 24 | 25 | This program is distributed in the hope that it will be useful, 26 | but WITHOUT ANY WARRANTY; without even the implied warranty of 27 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 28 | GNU General Public License for more details. 29 | 30 | You should have received a copy of the GNU General Public License along 31 | with this program; if not, write to the Free Software Foundation, Inc., 32 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 33 | 34 | The full LICENSE file and source are available at 35 | https://github.com/jhermann/rituals 36 | """ 37 | from __future__ import absolute_import, print_function 38 | 39 | # Project data (the rest is parsed from __init__.py and other project files) 40 | name = 'rituals' 41 | package_name = 'rituals' 42 | 43 | # ~~~ BEGIN springerle/py-generic-project ~~~ 44 | # Stdlib imports 45 | import os 46 | import re 47 | import sys 48 | import textwrap 49 | from codecs import open # pylint: disable=redefined-builtin 50 | from collections import defaultdict 51 | 52 | # Import setuptools 53 | try: 54 | from setuptools import setup, find_packages 55 | from setuptools.command.test import test as TestCommand 56 | except ImportError as exc: 57 | raise RuntimeError("Cannot install '{0}', setuptools is missing ({1})".format(name, exc)) 58 | 59 | # Helpers 60 | project_root = os.path.abspath(os.path.dirname(__file__)) 61 | 62 | def srcfile(*args): 63 | "Helper for path building." 64 | return os.path.join(*((project_root,) + args)) 65 | 66 | class PyTest(TestCommand): 67 | """pytest integration into setuptool's `test` command.""" 68 | user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")] 69 | 70 | def initialize_options(self): 71 | TestCommand.initialize_options(self) 72 | self.pytest_args = [] 73 | 74 | def finalize_options(self): 75 | TestCommand.finalize_options(self) 76 | self.test_args = [] 77 | self.test_suite = True 78 | 79 | def run_tests(self): 80 | # import locally, cause outside the eggs aren't loaded 81 | import pytest 82 | errno = pytest.main(self.pytest_args) 83 | if errno: 84 | sys.exit(errno) 85 | 86 | def _build_metadata(): # pylint: disable=too-many-locals, too-many-branches 87 | "Return project's metadata as a dict." 88 | # Handle metadata in package source 89 | expected_keys = ('url', 'version', 'license', 'author', 'author_email', 'long_description', 'keywords') 90 | metadata = {} 91 | with open(srcfile('src', package_name, '__init__.py'), encoding='utf-8') as handle: 92 | pkg_init = handle.read() 93 | # Get default long description from docstring 94 | metadata['long_description'] = re.search(r'^"""(.+?)^"""$', pkg_init, re.DOTALL|re.MULTILINE).group(1) 95 | for line in pkg_init.splitlines(): 96 | match = re.match(r"""^__({0})__ += (?P['"])(.+?)(?P=q)$""".format('|'.join(expected_keys)), line) 97 | if match: 98 | metadata[match.group(1)] = match.group(3) 99 | 100 | if not all(i in metadata for i in expected_keys): 101 | raise RuntimeError("Missing or bad metadata in '{0}' package: {1}".format( 102 | name, ', '.join(sorted(set(expected_keys) - set(metadata.keys()))), 103 | )) 104 | 105 | text = metadata['long_description'].strip() 106 | if text: 107 | metadata['description'], text = text.split('.', 1) 108 | metadata['description'] = ' '.join(metadata['description'].split()).strip() + '.' # normalize whitespace 109 | metadata['long_description'] = textwrap.dedent(text).strip() 110 | metadata['keywords'] = metadata['keywords'].replace(',', ' ').strip().split() 111 | 112 | # Load requirements files 113 | requirements_files = dict( 114 | install = 'requirements.txt', 115 | setup = 'setup-requirements.txt', 116 | test = 'test-requirements.txt', 117 | ) 118 | requires = {} 119 | for key, filename in requirements_files.items(): 120 | requires[key] = [] 121 | if os.path.exists(srcfile(filename)): 122 | with open(srcfile(filename), encoding='utf-8') as handle: 123 | for line in handle: 124 | line = line.strip() 125 | if line and not line.startswith('#'): 126 | if line.startswith('-e'): 127 | line = line.split()[1].split('#egg=')[1] 128 | requires[key].append(line) 129 | if not any('pytest' == re.split('[\t ,<=>]', i.lower())[0] for i in requires['test']): 130 | requires['test'].append('pytest') # add missing requirement 131 | 132 | # CLI entry points 133 | console_scripts = [] 134 | for path, dirs, files in os.walk(srcfile('src', package_name)): 135 | dirs = [i for i in dirs if not i.startswith('.')] 136 | if '__main__.py' in files: 137 | path = path[len(srcfile('src') + os.sep):] 138 | appname = path.split(os.sep)[-1] 139 | with open(srcfile('src', path, '__main__.py'), encoding='utf-8') as handle: 140 | for line in handle.readlines(): 141 | match = re.match(r"""^__app_name__ += (?P['"])(.+?)(?P=q)$""", line) 142 | if match: 143 | appname = match.group(2) 144 | console_scripts.append('{0} = {1}.__main__:cli'.format(appname, path.replace(os.sep, '.'))) 145 | 146 | # Add some common files to EGG-INFO 147 | candidate_files = [ 148 | 'LICENSE', 'NOTICE', 149 | 'README', 'README.md', 'README.rst', 'README.txt', 150 | 'CHANGES', 'CHANGELOG', 'debian/changelog', 151 | ] 152 | data_files = defaultdict(list) 153 | for filename in candidate_files: 154 | if os.path.exists(srcfile(filename)): 155 | data_files['EGG-INFO'].append(filename) 156 | 157 | # Complete project metadata 158 | classifiers = [] 159 | for classifiers_txt in ('classifiers.txt', 'project.d/classifiers.txt'): 160 | classifiers_txt = srcfile(classifiers_txt) 161 | if os.path.exists(classifiers_txt): 162 | with open(classifiers_txt, encoding='utf-8') as handle: 163 | classifiers = [i.strip() for i in handle if i.strip() and not i.startswith('#')] 164 | break 165 | 166 | metadata.update(dict( 167 | name = name, 168 | package_dir = {'': 'src'}, 169 | packages = find_packages(srcfile('src'), exclude=['tests']), 170 | data_files = data_files.items(), 171 | zip_safe = False, 172 | include_package_data = True, 173 | install_requires = requires['install'], 174 | setup_requires = requires['setup'], 175 | tests_require = requires['test'], 176 | classifiers = classifiers, 177 | cmdclass = dict( 178 | test = PyTest, 179 | ), 180 | entry_points = dict( 181 | console_scripts = console_scripts, 182 | ), 183 | )) 184 | return metadata 185 | 186 | # Ensure "setup.py" is importable by other tools, to access the project's metadata 187 | project = _build_metadata() 188 | __all__ = ['project', 'project_root', 'package_name', 'srcfile'] 189 | if __name__ == '__main__': 190 | setup(**project) 191 | -------------------------------------------------------------------------------- /src/rituals/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable= 3 | """ 4 | Common tasks for 'Invoke' that are needed again and again. 5 | 6 | The ``rituals`` package provides PyInvoke tasks that work for any 7 | project, based on its project metadata, to automate common 8 | developer chores like 'clean', 'build', 'dist', 'test', 'check', 9 | and 'release-prep' (for the moment). 10 | 11 | The guiding principle for these tasks is to strictly separate 12 | low-level tasks for building and installing (via ``setup.py``) from 13 | high-level convenience tasks a developer uses (via ``tasks.py``). 14 | Invoke tasks can use Setuptools ones as building blocks, but 15 | never the other way 'round – this avoids bootstrapping head- 16 | aches during package installations using ``pip``. 17 | 18 | The easiest way to get a working project based on ``rituals`` is 19 | the ``py-generic-project`` cookiecutter template. That way you have 20 | a working project skeleton within minutes that is fully equipped, 21 | with all aspects of bootstrapping, building, testing, quality 22 | checks, continuous integration, documentation, and releasing 23 | covered. See here for more: 24 | 25 | https://github.com/Springerle/py-generic-project 26 | 27 | 28 | Copyright ⓒ 2015 - 2019 Jürgen Hermann 29 | 30 | This program is free software; you can redistribute it and/or modify 31 | it under the terms of the GNU General Public License version 2 as 32 | published by the Free Software Foundation. 33 | 34 | This program is distributed in the hope that it will be useful, 35 | but WITHOUT ANY WARRANTY; without even the implied warranty of 36 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 37 | GNU General Public License for more details. 38 | 39 | You should have received a copy of the GNU General Public License along 40 | with this program; if not, write to the Free Software Foundation, Inc., 41 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 42 | 43 | The full LICENSE file and source are available at 44 | 45 | https://github.com/jhermann/rituals 46 | """ 47 | from __future__ import absolute_import, unicode_literals, print_function 48 | 49 | try: 50 | import pathlib 51 | except ImportError: 52 | import pathlib2 as pathlib 53 | 54 | __url__ = 'https://github.com/jhermann/rituals' 55 | __version__ = '0.4.3' 56 | __license__ = 'GPL v2' 57 | __author__ = 'Jürgen Hermann' 58 | __author_email__ = 'jh@web.de' 59 | __keywords__ = 'invoke automation tasks release deploy distribute publish' 60 | -------------------------------------------------------------------------------- /src/rituals/acts/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable= 3 | """ rituals.acts – Task building blocks. 4 | """ 5 | # Copyright ⓒ 2015 Jürgen Hermann 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License version 2 as 9 | # published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | # 20 | # The full LICENSE file and source are available at 21 | # https://github.com/jhermann/rituals 22 | from __future__ import absolute_import, unicode_literals, print_function 23 | 24 | from invoke import Collection, exceptions 25 | try: 26 | from invoke import ctask as task 27 | except ImportError: 28 | from invoke import task # since 0.13.0; TODO: remove try block in 2017 or so 29 | -------------------------------------------------------------------------------- /src/rituals/acts/basic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=superfluous-parens, wildcard-import 3 | """ Basic tasks. 4 | """ 5 | # Copyright ⓒ 2015 Jürgen Hermann 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License version 2 as 9 | # published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | # 20 | # The full LICENSE file and source are available at 21 | # https://github.com/jhermann/rituals 22 | from __future__ import absolute_import, unicode_literals, print_function 23 | 24 | import io 25 | import os 26 | import shlex 27 | import shutil 28 | 29 | from . import task 30 | from .. import config 31 | from ..util import antglob, notify, shell, buildsys 32 | from ..util._compat import isodate 33 | 34 | 35 | __all__ = ['help', 'clean', 'build', 'freeze'] 36 | 37 | 38 | @task(default=True) 39 | def help(_dummy_ctx): # pylint: disable=redefined-builtin 40 | """Invoked with no arguments.""" 41 | shell.run("invoke --help") 42 | shell.run("invoke --list") 43 | notify.info("Use 'invoke -h ‹taskname›' to get detailed help.") 44 | 45 | 46 | @task(help=dict( 47 | docs="Also clean the documentation build area", 48 | backups="Also clean '*~' files etc.", 49 | bytecode="Also clean '.pyc', '.pyo', and package metadata", 50 | dist="Also clean the 'dist' dir", 51 | all="The same as --backups --bytecode --dist --docs", 52 | venv="Include an existing virtualenv (in '.' or in '.venv')", 53 | tox="Include '.tox' directory", 54 | extra="Any extra patterns, space-separated and possibly quoted", 55 | )) 56 | def clean(_dummy_ctx, docs=False, backups=False, bytecode=False, dist=False, # pylint: disable=redefined-outer-name 57 | all=False, venv=False, tox=False, extra=''): # pylint: disable=redefined-builtin 58 | """Perform house-keeping.""" 59 | cfg = config.load() 60 | notify.banner("Cleaning up project files") 61 | 62 | # Add patterns based on given parameters 63 | venv_dirs = ['bin', 'include', 'lib', 'share', 'local', '.venv'] 64 | patterns = ['build/', 'pip-selfcheck.json'] 65 | excludes = ['.git/', '.hg/', '.svn/', 'debian/*/'] 66 | if docs or all: 67 | patterns.extend(['docs/_build/', 'doc/_build/']) 68 | if dist or all: 69 | patterns.append('dist/') 70 | if backups or all: 71 | patterns.extend(['**/*~']) 72 | if bytecode or all: 73 | patterns.extend([ 74 | '**/*.py[co]', '**/__pycache__/', '*.egg-info/', 75 | cfg.srcjoin('*.egg-info/')[len(cfg.project_root)+1:], 76 | ]) 77 | if venv: 78 | patterns.extend([i + '/' for i in venv_dirs]) 79 | if tox: 80 | patterns.append('.tox/') 81 | else: 82 | excludes.append('.tox/') 83 | if extra: 84 | patterns.extend(shlex.split(extra)) 85 | 86 | # Build fileset 87 | patterns = [antglob.includes(i) for i in patterns] + [antglob.excludes(i) for i in excludes] 88 | if not venv: 89 | # Do not scan venv dirs when not cleaning them 90 | patterns.extend([antglob.excludes(i + '/') for i in venv_dirs]) 91 | fileset = antglob.FileSet(cfg.project_root, patterns) 92 | 93 | # Iterate over matches and remove them 94 | for name in fileset: 95 | notify.info('rm {0}'.format(name)) 96 | if name.endswith('/'): 97 | shutil.rmtree(os.path.join(cfg.project_root, name)) 98 | else: 99 | os.unlink(os.path.join(cfg.project_root, name)) 100 | 101 | 102 | @task(help=dict( 103 | docs="Also build the documentation (with Sphinx)", 104 | )) 105 | def build(ctx, docs=False): 106 | """Build the project.""" 107 | cfg = config.load() 108 | buildsys.build() 109 | 110 | if docs: 111 | for doc_path in ('docs', 'doc'): 112 | if os.path.exists(cfg.rootjoin(doc_path, 'conf.py')): 113 | break 114 | else: 115 | doc_path = None 116 | 117 | if doc_path: 118 | ctx.run("invoke docs") 119 | else: 120 | notify.warning("Cannot find either a 'docs' or 'doc' Sphinx directory!") 121 | 122 | 123 | @task(help=dict( 124 | local="If in a virtualenv that has global access, do not output globally installed packages", 125 | )) 126 | def freeze(ctx, local=False): 127 | """Freeze currently installed requirements.""" 128 | cmd = 'pip --disable-pip-version-check freeze{}'.format(' --local' if local else '') 129 | frozen = ctx.run(cmd, hide='out').stdout.replace('\x1b', '#') 130 | with io.open('frozen-requirements.txt', 'w', encoding='ascii') as out: 131 | out.write("# Requirements frozen by 'pip freeze' on {}\n".format(isodate())) 132 | out.write(frozen) 133 | notify.info("Frozen {} requirements.".format(len(frozen.splitlines()),)) 134 | -------------------------------------------------------------------------------- /src/rituals/acts/devpi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable= 3 | """ 'devpi' tasks. 4 | """ 5 | # Copyright ⓒ 2015 Jürgen Hermann 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License version 2 as 9 | # published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | # 20 | # The full LICENSE file and source are available at 21 | # https://github.com/jhermann/rituals 22 | from __future__ import absolute_import, unicode_literals, print_function 23 | 24 | #import os 25 | import sys 26 | #import warnings 27 | 28 | from . import Collection, task 29 | from ..util import notify 30 | 31 | 32 | DEFAULT_REQUIREMENTS = 'dev-requirements.txt' 33 | 34 | 35 | def get_devpi_url(ctx): 36 | """Get currently used 'devpi' base URL.""" 37 | cmd = 'devpi use --urls' 38 | lines = ctx.run(cmd, hide='out', echo=False).stdout.splitlines() 39 | for line in lines: 40 | try: 41 | line, base_url = line.split(':', 1) 42 | except ValueError: 43 | notify.warning('Ignoring "{}"!'.format(line)) 44 | else: 45 | if line.split()[-1].strip() == 'simpleindex': 46 | return base_url.split('\x1b')[0].strip().rstrip('/') 47 | 48 | raise LookupError("Cannot find simpleindex URL in '{}' output:\n {}".format( 49 | cmd, '\n '.join(lines), 50 | )) 51 | 52 | 53 | # XXX: broken due to internal pip changes, must be rewritten to command calls or other libs 54 | #@task(help=dict( 55 | # requirement="Refresh from the given requirements file (default: {})".format(DEFAULT_REQUIREMENTS), 56 | # name="Refresh a specific package", 57 | # installed="Refresh all installed packages", 58 | #)) # pylint: disable=too-many-locals 59 | #def refresh(ctx, requirement='', name='', installed=False): 60 | # """Refresh 'devpi' PyPI links.""" 61 | # import requests 62 | # from pip import get_installed_distributions 63 | # from pip.download import PipSession 64 | # from pip.req.req_file import parse_requirements 65 | 66 | # with warnings.catch_warnings(): 67 | # warnings.simplefilter('once') 68 | 69 | # # If no option at all is given, default to using 'dev-requirements.txt' 70 | # if not (requirement or name or installed): 71 | # requirement = ctx.rituals.devpi.requirements or DEFAULT_REQUIREMENTS 72 | # if not os.path.exists(requirement): 73 | # requirement = 'requirements.txt' 74 | 75 | # # Get 'devpi' URL 76 | # try: 77 | # base_url = get_devpi_url(ctx) 78 | # except LookupError as exc: 79 | # notify.failure(exc.args[0]) 80 | # notify.banner("Refreshing devpi links on {}".format(base_url)) 81 | 82 | # # Assemble requirements 83 | # reqs = set(('pip', 'setuptools', 'wheel')) # always refresh basics 84 | # if requirement: 85 | # reqs |= set(i.name for i in parse_requirements(requirement, session=PipSession())) 86 | # if name: 87 | # reqs |= set([name]) 88 | # if installed: 89 | # installed_pkgs = get_installed_distributions(local_only=True, skip=['python']) 90 | # reqs |= set(i.project_name for i in installed_pkgs) 91 | # reqs = [i for i in reqs if i] # catch flukes 92 | 93 | # for req in sorted(reqs): 94 | # url = "{}/{}/refresh".format(base_url, req) 95 | # response = requests.post(url) 96 | # if response.status_code not in (200, 302): 97 | # notify.warning("Failed to refresh {}: {} {}".format(url, response.status_code, response.reason)) 98 | # else: 99 | # notify.info("{:>{width}}: {} {}".format( 100 | # req, response.status_code, response.reason, width=4 + max(len(i) for i in reqs), 101 | # )) 102 | 103 | # lines = ctx.run('pip list --local --outdated', hide='out', echo=False).stdout.splitlines() 104 | # if lines: 105 | # notify.banner("Outdated packages") 106 | # notify.info(' ' + '\n '.join(sorted(lines))) 107 | 108 | 109 | namespace = Collection.from_module(sys.modules[__name__], config={'rituals': dict( 110 | devpi = dict( 111 | requirements = DEFAULT_REQUIREMENTS, 112 | ), 113 | )}) 114 | -------------------------------------------------------------------------------- /src/rituals/acts/github.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable= 3 | """ GitHub automation. 4 | """ 5 | # Copyright ⓒ 2015 Jürgen Hermann 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License version 2 as 9 | # published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | # 20 | # The full LICENSE file and source are available at 21 | # https://github.com/jhermann/rituals 22 | from __future__ import absolute_import, unicode_literals, print_function 23 | 24 | import sys 25 | 26 | from . import Collection, task 27 | from .. import config 28 | from ..util import notify 29 | 30 | 31 | @task(name='sync-readme') 32 | def sync_readme(_dummy_ctx): 33 | """Update GH pages from project's README.""" 34 | _ = config.load() 35 | notify.banner("Syncing GH pages with 'README.md'...") 36 | 37 | notify.failure("Not implemented yet!") 38 | 39 | 40 | namespace = Collection.from_module(sys.modules[__name__]) # pylint: disable=invalid-name 41 | -------------------------------------------------------------------------------- /src/rituals/acts/inspection.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable= 3 | """ 'docs' tasks. 4 | """ 5 | # Copyright ⓒ 2015 Jürgen Hermann 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License version 2 as 9 | # published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | # 20 | # The full LICENSE file and source are available at 21 | # https://github.com/jhermann/rituals 22 | from __future__ import absolute_import, unicode_literals, print_function 23 | 24 | import os 25 | import sys 26 | 27 | from . import Collection, task, exceptions 28 | from .. import config 29 | from ..util import antglob, notify, shell, add_dir2pypath 30 | 31 | 32 | @task(default=True, help=dict( 33 | skip_tests="Do not check test modules", 34 | skip_root="Do not check scripts in project root", 35 | reports="Create extended report?", 36 | )) 37 | def pylint(ctx, skip_tests=False, skip_root=False, reports=False): 38 | """Perform source code checks via pylint.""" 39 | cfg = config.load() 40 | add_dir2pypath(cfg.project_root) 41 | if not os.path.exists(cfg.testjoin('__init__.py')): 42 | add_dir2pypath(cfg.testjoin()) 43 | 44 | namelist = set() 45 | for package in cfg.project.get('packages', []): 46 | if '.' not in package: 47 | namelist.add(cfg.srcjoin(package)) 48 | for module in cfg.project.get('py_modules', []): 49 | namelist.add(module + '.py') 50 | 51 | if not skip_tests: 52 | test_py = antglob.FileSet(cfg.testdir, '**/*.py') 53 | test_py = [cfg.testjoin(i) for i in test_py] 54 | if test_py: 55 | namelist |= set(test_py) 56 | 57 | if not skip_root: 58 | root_py = antglob.FileSet('.', '*.py') 59 | if root_py: 60 | namelist |= set(root_py) 61 | 62 | namelist = set([i[len(os.getcwd())+1:] if i.startswith(os.getcwd() + os.sep) else i for i in namelist]) 63 | cmd = 'pylint' 64 | cmd += ' "{}"'.format('" "'.join(sorted(namelist))) 65 | cmd += ' --reports={0}'.format('y' if reports else 'n') 66 | for cfgfile in ('.pylintrc', 'pylint.rc', 'pylint.cfg', 'project.d/pylint.cfg'): 67 | if os.path.exists(cfgfile): 68 | cmd += ' --rcfile={0}'.format(cfgfile) 69 | break 70 | try: 71 | shell.run(cmd, report_error=False, runner=ctx.run) 72 | notify.info("OK - No problems found by pylint.") 73 | except exceptions.Failure as exc: 74 | # Check bit flags within pylint return code 75 | if exc.result.return_code & 32: 76 | # Usage error (internal error in this code) 77 | notify.error("Usage error, bad arguments in {}?!".format(repr(cmd))) 78 | raise 79 | else: 80 | bits = { 81 | 1: "fatal", 82 | 2: "error", 83 | 4: "warning", 84 | 8: "refactor", 85 | 16: "convention", 86 | } 87 | notify.warning("Some messages of type {} issued by pylint.".format( 88 | ", ".join([text for bit, text in bits.items() if exc.result.return_code & bit]) 89 | )) 90 | if exc.result.return_code & 3: 91 | notify.error("Exiting due to fatal / error message.") 92 | raise 93 | 94 | 95 | namespace = Collection.from_module(sys.modules[__name__], name='check') 96 | -------------------------------------------------------------------------------- /src/rituals/acts/jenkins.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable= 3 | """ Tasks specific to Jenkins. 4 | """ 5 | # Copyright ⓒ 2015 Jürgen Hermann 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License version 2 as 9 | # published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | # 20 | # The full LICENSE file and source are available at 21 | # https://github.com/jhermann/rituals 22 | from __future__ import absolute_import, unicode_literals, print_function 23 | 24 | import io 25 | import re 26 | import sys 27 | 28 | from . import Collection, task 29 | from .. import config 30 | from ..util import notify 31 | 32 | 33 | DESCRIPTION_TEMPLATES = dict( 34 | html="""

{project.name} v{project.version}

35 | {long_description_html} 36 |
37 |
Summary
{project.description}
38 |
URL
{project.url}
39 |
Author
{project.author} <{project.author_email}>
40 |
License
{project.license}
41 |
Keywords
{keywords}
42 |
Packages
{packages}
43 |
Classifiers
{classifiers}
44 |
45 | """, 46 | md="""## {project.name} v{project.version} 47 | 48 | {project.long_description} 49 | 50 | * **Summary**: {project.description} 51 | * **URL**: {project.url} 52 | * **Author**: {project.author} <{project.author_email}> 53 | * **License**: {project.license} 54 | * **Keywords**: {keywords} 55 | * **Packages**: {packages} 56 | 57 | **Classifiers** 58 | 59 | {classifiers_indented} 60 | """, 61 | ) 62 | 63 | 64 | @task(help=dict( 65 | markdown="Use Markdown instead of HTML", 66 | )) 67 | def description(_dummy_ctx, markdown=False): 68 | """Dump project metadata for Jenkins Description Setter Plugin.""" 69 | cfg = config.load() 70 | markup = 'md' if markdown else 'html' 71 | description_file = cfg.rootjoin("build/project.{}".format(markup)) 72 | notify.banner("Creating {} file for Jenkins...".format(description_file)) 73 | 74 | long_description = cfg.project.long_description 75 | long_description = long_description.replace('\n\n', '

\n

') 76 | long_description = re.sub(r'(\W)``([^`]+)``(\W)', r'\1\2\3', long_description) 77 | 78 | text = DESCRIPTION_TEMPLATES[markup].format( 79 | keywords=', '.join(cfg.project.keywords), 80 | classifiers='\n'.join(cfg.project.classifiers), 81 | classifiers_indented=' ' + '\n '.join(cfg.project.classifiers), 82 | packages=', '.join(cfg.project.packages), 83 | long_description_html='

{}

'.format(long_description), 84 | ##data='\n'.join(["%s=%r" % i for i in cfg.project.iteritems()]), 85 | **cfg) 86 | with io.open(description_file, 'w', encoding='utf-8') as handle: 87 | handle.write(text) 88 | 89 | 90 | namespace = Collection.from_module(sys.modules[__name__]) # pylint: disable=invalid-name 91 | -------------------------------------------------------------------------------- /src/rituals/acts/pkgdeb.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable= 3 | """ 'deb' tasks. 4 | """ 5 | # Copyright ⓒ 2015 Jürgen Hermann 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License version 2 as 9 | # published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | # 20 | # The full LICENSE file and source are available at 21 | # https://github.com/jhermann/rituals 22 | from __future__ import absolute_import, unicode_literals, print_function 23 | 24 | import io 25 | import os 26 | import re 27 | import sys 28 | import glob 29 | import shutil 30 | 31 | from . import Collection, task 32 | from ..util import notify 33 | 34 | 35 | @task(default=True, help=dict( 36 | dput="Host to upload to (use 'dput -H' to list them)", 37 | opts="Extra flags for package build", 38 | )) 39 | def build(ctx, dput='', opts=''): 40 | """Build a DEB package.""" 41 | # Get package metadata 42 | with io.open('debian/changelog', encoding='utf-8') as changes: 43 | metadata = re.match(r'^([^ ]+) \(([^)]+)\) ([^;]+); urgency=(.+)$', changes.readline().rstrip()) 44 | if not metadata: 45 | notify.failure('Badly formatted top entry in changelog') 46 | name, version, _, _ = metadata.groups() 47 | 48 | # Build package 49 | ctx.run('dpkg-buildpackage {} {}'.format(ctx.rituals.deb.build.opts, opts)) 50 | 51 | # Move created artifacts into "dist" 52 | if not os.path.exists('dist'): 53 | os.makedirs('dist') 54 | artifact_pattern = '{}?{}*'.format(name, re.sub(r'[^-_.a-zA-Z0-9]', '?', version)) 55 | changes_files = [] 56 | for debfile in glob.glob('../' + artifact_pattern): 57 | shutil.move(debfile, 'dist') 58 | if debfile.endswith('.changes'): 59 | changes_files.append(os.path.join('dist', os.path.basename(debfile))) 60 | ctx.run('ls -l dist/{}'.format(artifact_pattern)) 61 | 62 | if dput: 63 | ctx.run('dput {} {}'.format(dput, ' '.join(changes_files))) 64 | 65 | 66 | namespace = Collection.from_module(sys.modules[__name__], name='deb', config={'rituals': dict( 67 | deb = dict( 68 | build = dict(opts='-uc -us -b',), 69 | ), 70 | )}) 71 | -------------------------------------------------------------------------------- /src/rituals/acts/testing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=superfluous-parens, 3 | """ Testing tasks. 4 | """ 5 | # Copyright ⓒ 2015 Jürgen Hermann 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License version 2 as 9 | # published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | # 20 | # The full LICENSE file and source are available at 21 | # https://github.com/jhermann/rituals 22 | from __future__ import absolute_import, unicode_literals, print_function 23 | 24 | import os 25 | import sys 26 | import shutil 27 | import webbrowser 28 | 29 | from . import Collection, task 30 | from .. import config 31 | from ..util import notify, which, add_dir2pypath 32 | 33 | 34 | @task(default=True, help={ 35 | 'coverage': "Open coverage report in browser tab", 36 | 'opts': "Extra flags for test runner", 37 | }) 38 | def pytest(ctx, coverage=False, opts=''): 39 | """Perform standard unittests.""" 40 | cfg = config.load() 41 | setup_cfg = cfg.rootjoin('setup.cfg') 42 | add_dir2pypath(cfg.project_root) 43 | 44 | try: 45 | console = sys.stdin.isatty() 46 | except AttributeError: 47 | console = False 48 | 49 | try: 50 | pytest_cmd = which.which("py.test").replace(cfg.project_root + os.sep, '') 51 | except which.WhichError: 52 | pytest_cmd = None 53 | 54 | if pytest_cmd: 55 | cmd = [pytest_cmd, 56 | '-c', setup_cfg, 57 | '--color=yes' if console else '', 58 | ] 59 | 60 | try: 61 | import pytest_cov as _ 62 | except ImportError: 63 | pass 64 | else: 65 | for name in cfg.project.get('packages', []) + cfg.project.get('py_modules', []): 66 | if '.' not in name: 67 | cmd.extend(['--cov', name,]) 68 | for dirname in ('.', 'project.d'): 69 | if os.path.exists(cfg.rootjoin(dirname, 'coverage.cfg')): 70 | cmd.extend(['--cov-config', cfg.rootjoin(dirname, 'coverage.cfg'),]) 71 | break 72 | cmd.extend(['--cov-report=term', '--cov-report=html', '--cov-report=xml',]) 73 | 74 | if opts: 75 | cmd.append(opts) 76 | cmd.append(cfg.testdir) 77 | ctx.run(' '.join(cmd)) 78 | else: 79 | ctx.run('python setup.py test' + (' ' + opts if opts else '')) 80 | 81 | if coverage: 82 | # TODO: Read from "coverage.cfg [html] directory" 83 | cov_html = os.path.join("build/coverage_html_report", "index.html") 84 | if os.path.exists(cov_html): 85 | webbrowser.open_new_tab(cov_html) 86 | else: 87 | notify.warning('No coverage report found at "{}"!'.format(cov_html)) 88 | 89 | 90 | #_PROJECT_ROOT = config.get_project_root() 91 | # Keep 'tox' tasks? 92 | #if _PROJECT_ROOT and not os.path.exists(os.path.join(_PROJECT_ROOT, 'tox.ini')): 93 | # del tox 94 | 95 | @task(help={ 96 | 'verbose': "Make 'tox' more talkative", 97 | 'clean': "Remove '.tox' first", 98 | 'env-list': "Override list of environments to use (e.g. 'py27,py34')", 99 | 'opts': "Extra flags for tox", 100 | }) 101 | def tox(ctx, verbose=False, clean=False, env_list='', opts=''): 102 | """Perform multi-environment tests.""" 103 | cfg = config.load() 104 | add_dir2pypath(cfg.project_root) 105 | snakepits = ctx.rituals.snakepits.split(os.pathsep) 106 | cmd = [] 107 | 108 | snakepits = [i for i in snakepits if os.path.isdir(i)] 109 | if snakepits: 110 | cmd += ['PATH="{}:$PATH"'.format(os.pathsep.join(snakepits),)] 111 | 112 | if clean and os.path.exists(cfg.rootjoin('.tox')): 113 | shutil.rmtree(cfg.rootjoin('.tox')) 114 | 115 | cmd += ['tox'] 116 | if verbose: 117 | cmd += ['-v'] 118 | if env_list: 119 | cmd += ['-e', env_list] 120 | cmd += opts 121 | ctx.run(' '.join(cmd)) 122 | 123 | 124 | namespace = Collection.from_module(sys.modules[__name__], name='test', config={'rituals': dict( 125 | #test = dict( 126 | #), 127 | snakepits = '/opt/pyenv/bin:/opt/pyrun/bin', 128 | )}) 129 | -------------------------------------------------------------------------------- /src/rituals/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable= 3 | """ Project configuration and layout. 4 | """ 5 | # Copyright ⓒ 2015 Jürgen Hermann 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License version 2 as 9 | # published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | # 20 | # The full LICENSE file and source are available at 21 | # https://github.com/jhermann/rituals 22 | from __future__ import absolute_import, unicode_literals, print_function 23 | 24 | import os 25 | import sys 26 | from pathlib import Path 27 | 28 | from munch import Munch as Bunch 29 | 30 | 31 | DEFAULTS = dict( 32 | srcdir = 'src', 33 | testdir = 'src/tests', 34 | project_root = None, 35 | project = {}, 36 | cwd = None, 37 | rootjoin = None, 38 | srcjoin = None, 39 | testjoin = None, 40 | ) 41 | 42 | 43 | def get_project_root(): 44 | """ Determine location of `tasks.py`.""" 45 | try: 46 | tasks_py = sys.modules['tasks'] 47 | except KeyError: 48 | return None 49 | else: 50 | return os.path.abspath(os.path.dirname(tasks_py.__file__)) 51 | 52 | 53 | def load(): 54 | """ Load and return configuration as a ``Bunch``. 55 | 56 | Values are based on ``DEFAULTS``, and metadata from ``setup.py``. 57 | """ 58 | from .util import buildsys 59 | 60 | cfg = Bunch(DEFAULTS) 61 | # TODO: override with contents of [rituals] section in setup.cfg 62 | 63 | cfg.project_root = get_project_root() 64 | if not cfg.project_root: 65 | raise RuntimeError("No tasks module is imported, cannot determine project root") 66 | 67 | cfg.rootjoin = lambda *names: os.path.join(cfg.project_root, *names) 68 | cfg.srcjoin = lambda *names: cfg.rootjoin(cfg.srcdir, *names) 69 | cfg.testjoin = lambda *names: cfg.rootjoin(cfg.testdir, *names) 70 | cfg.cwd = os.getcwd() 71 | os.chdir(cfg.project_root) 72 | cfg.project = buildsys.project_meta() 73 | 74 | return cfg 75 | 76 | 77 | def is_maven_layout(project_dir): 78 | """Apply heuristics to check if the given path is a Maven project.""" 79 | project_dir = Path(project_dir) 80 | result = ((project_dir / 'src/main/tests').exists() 81 | and (project_dir / 'src/main/python').exists() 82 | ) 83 | return result 84 | 85 | 86 | def set_maven_layout(): 87 | """Switch default project layout to Maven-like.""" 88 | DEFAULTS.update( 89 | srcdir = 'src/main/python', 90 | testdir = 'src/test/python', 91 | ) 92 | 93 | 94 | def is_flat_layout(project_dir): 95 | """Apply heuristics to check if the given path is a 'flat' project.""" 96 | # Right now, we only take care of projects where repo name and package name are equivalent 97 | project_dir = Path(project_dir) 98 | result = ((project_dir / 'tests').exists() 99 | and (project_dir / project_dir.name.replace('-', '_') / '__init__.py').exists() 100 | ) 101 | return result 102 | 103 | 104 | def set_flat_layout(): 105 | """Switch default project layout to everything top-level.""" 106 | DEFAULTS.update( 107 | srcdir = '.', 108 | testdir = 'tests', 109 | ) 110 | -------------------------------------------------------------------------------- /src/rituals/easy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=wrong-import-position 3 | """ Default namespace for convenient wildcard import in task definition modules. 4 | """ 5 | # Copyright ⓒ 2015 Jürgen Hermann 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License version 2 as 9 | # published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | # 20 | # The full LICENSE file and source are available at 21 | # https://github.com/jhermann/rituals 22 | from __future__ import absolute_import, unicode_literals, print_function 23 | 24 | import os 25 | import sys 26 | 27 | from .acts import Collection, task 28 | from .util.filesys import pushd 29 | 30 | # Project layout detection 31 | from . import config 32 | if config.is_flat_layout(os.getcwd()): 33 | config.set_flat_layout() 34 | elif config.is_maven_layout(os.getcwd()): 35 | config.set_maven_layout() 36 | 37 | # Build root namespace 38 | from .acts import basic 39 | namespace = Collection.from_module(basic, name='') # pylint: disable=invalid-name 40 | 41 | from .acts.testing import namespace as _ 42 | namespace.add_collection(_) 43 | 44 | from .acts.documentation import namespace as _ 45 | namespace.add_collection(_) 46 | 47 | from .acts.inspection import namespace as _ 48 | namespace.add_collection(_) 49 | 50 | from .acts.releasing import namespace as _ 51 | namespace.add_collection(_) 52 | 53 | # Activate Jekins tasks? 54 | if os.environ.get('JENKINS_URL'): 55 | from .acts.jenkins import namespace as _ 56 | namespace.add_collection(_) 57 | 58 | # Activate devpi tasks by default? 59 | if os.path.exists(os.path.expanduser('~/.devpi/client/current.json')): 60 | from .acts.devpi import namespace as _ 61 | namespace.add_collection(_) 62 | 63 | # Activate dpkg tasks? 64 | if os.path.exists('debian/rules'): 65 | from .acts.pkgdeb import namespace as _ 66 | namespace.add_collection(_) 67 | 68 | 69 | def fail(message, exitcode=1): 70 | """Exit with error code and message.""" 71 | sys.stderr.write('ERROR: {}\n'.format(message)) 72 | sys.stderr.flush() 73 | sys.exit(exitcode) 74 | 75 | 76 | __all__ = ['Collection', 'task', 'namespace', 'pushd', 'fail'] 77 | 78 | for _ in namespace.task_names: 79 | _name = _.replace('-', '_').replace('.', '_') 80 | __all__.append(_name) 81 | globals()[_name] = namespace.task_with_config(_)[0] 82 | -------------------------------------------------------------------------------- /src/rituals/util/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ rituals.util – Helper modules. 3 | """ 4 | # Copyright ⓒ 2015 Jürgen Hermann 5 | # 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License version 2 as 8 | # published by the Free Software Foundation. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License along 16 | # with this program; if not, write to the Free Software Foundation, Inc., 17 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 18 | # 19 | # The full LICENSE file and source are available at 20 | # https://github.com/jhermann/rituals 21 | from __future__ import absolute_import, unicode_literals, print_function 22 | 23 | import os 24 | 25 | 26 | def search_file_upwards(name, base=None): 27 | """ Search for a file named `name` from cwd or given directory to root. 28 | Return None if nothing's found. 29 | """ 30 | base = base or os.getcwd() 31 | while base != os.path.dirname(base): 32 | if os.path.exists(os.path.join(base, name)): 33 | return base 34 | base = os.path.dirname(base) 35 | 36 | return None 37 | 38 | 39 | def add_dir2pypath(path): 40 | """Add given directory to PYTHONPATH, e.g. for pylint.""" 41 | py_path = os.environ.get('PYTHONPATH', '') 42 | if path not in py_path.split(os.pathsep): 43 | py_path = ''.join([path, os.pathsep if py_path else '', py_path]) 44 | os.environ['PYTHONPATH'] = py_path 45 | # print('*** PYTHONPATH={}'.format(os.environ.get('PYTHONPATH', None))) 46 | -------------------------------------------------------------------------------- /src/rituals/util/_compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name, unused-import, missing-docstring, exec-used 3 | # pylint: disable=unused-argument, too-few-public-methods, redefined-builtin 4 | # pylint: disable=no-name-in-module, no-member, undefined-variable 5 | # pylint: disable=import-error, reimported 6 | """ 7 | rituals.util._compat 8 | ~~~~~~~~~~~~~~~~~~~~ 9 | 10 | Some py2/py3 compatibility support based on a stripped down 11 | version of six so there is no dependency on a specific version 12 | of it. 13 | 14 | Copied from Jinja2 (2015-03-22, #9bf5fcb). See also 15 | 16 | http://lucumr.pocoo.org/2013/5/21/porting-to-python-3-redux/ 17 | 18 | :copyright: Copyright (c) 2013 by the Jinja team, see their AUTHORS. 19 | :license: BSD, see the module's source for details. 20 | """ 21 | # Some rights reserved. 22 | # 23 | # Redistribution and use in source and binary forms, with or without 24 | # modification, are permitted provided that the following conditions are 25 | # met: 26 | # 27 | # * Redistributions of source code must retain the above copyright 28 | # notice, this list of conditions and the following disclaimer. 29 | # 30 | # * Redistributions in binary form must reproduce the above 31 | # copyright notice, this list of conditions and the following 32 | # disclaimer in the documentation and/or other materials provided 33 | # with the distribution. 34 | # 35 | # * The names of the contributors may not be used to endorse or 36 | # promote products derived from this software without specific 37 | # prior written permission. 38 | # 39 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 40 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 41 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 42 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 43 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 44 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 45 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 46 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 47 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 48 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 49 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 50 | 51 | import sys 52 | import datetime 53 | 54 | PY2 = sys.version_info[0] == 2 55 | PYPY = hasattr(sys, 'pypy_translation_info') 56 | _identity = lambda x: x 57 | 58 | 59 | if not PY2: 60 | unichr = chr 61 | range_type = range 62 | text_type = str 63 | string_types = (str,) 64 | integer_types = (int,) 65 | 66 | iterkeys = lambda d: iter(d.keys()) 67 | itervalues = lambda d: iter(d.values()) 68 | iteritems = lambda d: iter(d.items()) 69 | 70 | import pickle 71 | from io import BytesIO, StringIO 72 | NativeStringIO = StringIO 73 | 74 | def reraise(tp, value, tb=None): 75 | if value.__traceback__ is not tb: 76 | raise value.with_traceback(tb) 77 | raise value 78 | 79 | ifilter = filter 80 | imap = map 81 | izip = zip 82 | intern = sys.intern 83 | 84 | implements_iterator = _identity 85 | implements_to_string = _identity 86 | encode_filename = _identity 87 | get_next = lambda x: x.__next__ 88 | 89 | def decode_filename(filename): 90 | if isinstance(filename, bytes): 91 | filename = filename.decode(sys.getfilesystemencoding(), 'replace') 92 | else: 93 | filename = filename.encode('utf-8', 'surrogateescape').decode('utf-8', 'replace') 94 | return filename 95 | 96 | else: # PY2 97 | unichr = unichr 98 | text_type = unicode 99 | range_type = xrange 100 | string_types = (str, unicode) 101 | integer_types = (int, long) 102 | 103 | iterkeys = lambda d: d.iterkeys() 104 | itervalues = lambda d: d.itervalues() 105 | iteritems = lambda d: d.iteritems() 106 | 107 | import cPickle as pickle 108 | from cStringIO import StringIO as BytesIO, StringIO 109 | NativeStringIO = BytesIO 110 | 111 | exec('def reraise(tp, value, tb=None):\n raise tp, value, tb') 112 | 113 | from itertools import imap, izip, ifilter 114 | intern = intern 115 | 116 | def implements_iterator(cls): 117 | cls.next = cls.__next__ 118 | del cls.__next__ 119 | return cls 120 | 121 | def implements_to_string(cls): 122 | cls.__unicode__ = cls.__str__ 123 | cls.__str__ = lambda x: x.__unicode__().encode('utf-8') 124 | return cls 125 | 126 | get_next = lambda x: x.next 127 | 128 | def encode_filename(filename): 129 | if isinstance(filename, unicode): 130 | return filename.encode('utf-8') 131 | return filename 132 | 133 | def decode_filename(filename): 134 | if isinstance(filename, bytes): 135 | filename = filename.decode(sys.getfilesystemencoding(), 'replace') 136 | return filename 137 | 138 | 139 | def with_metaclass(meta, *bases): 140 | # This requires a bit of explanation: the basic idea is to make a 141 | # dummy metaclass for one level of class instanciation that replaces 142 | # itself with the actual metaclass. Because of internal type checks 143 | # we also need to make sure that we downgrade the custom metaclass 144 | # for one level to something closer to type (that's why __call__ and 145 | # __init__ comes back from type etc.). 146 | # 147 | # This has the advantage over six.with_metaclass in that it does not 148 | # introduce dummy classes into the final MRO. 149 | class metaclass(meta): 150 | __call__ = type.__call__ 151 | __init__ = type.__init__ 152 | def __new__(cls, name, this_bases, d): 153 | if this_bases is None: 154 | return type.__new__(cls, name, (), d) 155 | return meta(name, bases, d) 156 | return metaclass('temporary_class', None, {}) 157 | 158 | 159 | try: 160 | from urllib.parse import quote_from_bytes as url_quote 161 | from urllib.parse import urlparse, parse_qs, parse_qsl 162 | except ImportError: 163 | from urllib import quote as url_quote 164 | from urlparse import urlparse, parse_qs, parse_qsl 165 | 166 | 167 | def isodate(datestamp=None, microseconds=False): 168 | """Return current or given time formatted according to ISO-8601.""" 169 | datestamp = datestamp or datetime.datetime.now() 170 | if not microseconds: 171 | usecs = datetime.timedelta(microseconds=datestamp.microsecond) 172 | datestamp = datestamp - usecs 173 | return datestamp.isoformat(b' ' if PY2 else u' ') # pylint: disable=no-member 174 | -------------------------------------------------------------------------------- /src/rituals/util/antglob.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=too-few-public-methods 3 | """ Recursive globbing with ant-style syntax. 4 | """ 5 | # 6 | # The MIT License (MIT) 7 | # 8 | # Original source (2014-02-17) from https://github.com/zacherates/fileset.py 9 | # Copyright (c) 2012 Aaron Maenpaa 10 | # 11 | # Modifications at https://github.com/jhermann/rituals 12 | # Copyright ⓒ 2015 Jürgen Hermann 13 | # 14 | # Permission is hereby granted, free of charge, to any person obtaining a copy 15 | # of this software and associated documentation files (the "Software"), to deal 16 | # in the Software without restriction, including without limitation the rights 17 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | # copies of the Software, and to permit persons to whom the Software is 19 | # furnished to do so, subject to the following conditions: 20 | # 21 | # The above copyright notice and this permission notice shall be included in all 22 | # copies or substantial portions of the Software. 23 | # 24 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 30 | # SOFTWARE. 31 | from __future__ import absolute_import, unicode_literals, print_function 32 | 33 | import os 34 | import re 35 | 36 | from ._compat import string_types 37 | 38 | # TODO: allow '?' 39 | # TODO: matching for Windows? (need to canonize to forward slashes in 'root') 40 | 41 | __all__ = ['FileSet', 'includes', 'excludes'] 42 | 43 | 44 | def glob2re(part): 45 | """Convert a path part to regex syntax.""" 46 | return "[^/]*".join( 47 | re.escape(bit).replace(r'\[\^', '[^').replace(r'\[', '[').replace(r'\]', ']') 48 | for bit in part.split("*") 49 | ) 50 | 51 | 52 | def parse_glob(pattern): 53 | """Generate parts of regex transformed from glob pattern.""" 54 | if not pattern: 55 | return 56 | 57 | bits = pattern.split("/") 58 | dirs, filename = bits[:-1], bits[-1] 59 | 60 | for dirname in dirs: 61 | if dirname == "**": 62 | yield "(|.+/)" 63 | else: 64 | yield glob2re(dirname) + "/" 65 | 66 | yield glob2re(filename) 67 | 68 | 69 | def compile_glob(spec): 70 | """Convert the given glob `spec` to a compiled regex.""" 71 | parsed = "".join(parse_glob(spec)) 72 | regex = "^{0}$".format(parsed) 73 | return re.compile(regex) 74 | 75 | 76 | class Pattern(): 77 | """A single pattern for either inclusion or exclusion.""" 78 | 79 | def __init__(self, spec, inclusive): 80 | """Create regex-based pattern matcher from glob `spec`.""" 81 | self.compiled = compile_glob(spec.rstrip('/')) 82 | self.inclusive = inclusive 83 | self.is_dir = spec.endswith('/') 84 | 85 | def __str__(self): 86 | """Return inclusiveness indicator and original glob pattern.""" 87 | return ('+' if self.inclusive else '-') + self.compiled.pattern 88 | 89 | def matches(self, path): 90 | """Check this pattern against given `path`.""" 91 | return bool(self.compiled.match(path)) 92 | 93 | 94 | class FileSet(): 95 | """ Ant-style file and directory matching. 96 | 97 | Produces an iterator of all of the files that match the provided patterns. 98 | Note that directory matches must end with a slash, and if they're exclusions, 99 | they won't be scanned (which prunes anything in that directory that would 100 | otherwise match). 101 | 102 | Directory specifiers: 103 | ** matches zero or more directories. 104 | / path separator. 105 | 106 | File specifiers: 107 | * glob style wildcard. 108 | [chars] inclusive character sets. 109 | [^chars] exclusive character sets. 110 | 111 | Examples: 112 | **/*.py recursively match all python files. 113 | foo/**/*.py recursively match all python files in the 'foo' directory. 114 | *.py match all the python files in the current directory. 115 | */*.txt match all the text files in top-level directories. 116 | foo/**/* all files under directory 'foo'. 117 | */ top-level directories. 118 | foo/ the directory 'foo' itself. 119 | **/foo/ any directory named 'foo'. 120 | **/.* hidden files. 121 | **/.*/ hidden directories. 122 | """ 123 | 124 | def __init__(self, root, patterns): 125 | if isinstance(patterns, string_types): 126 | patterns = [patterns] 127 | 128 | self.root = root 129 | self.patterns = [i if hasattr(i, 'inclusive') else includes(i) for i in patterns] 130 | 131 | def __repr__(self): 132 | return "".format(repr(self.root), ' '.join(str(i) for i in self. patterns)) 133 | 134 | def included(self, path, is_dir=False): 135 | """Check patterns in order, last match that includes or excludes `path` wins. Return `None` on undecided.""" 136 | inclusive = None 137 | for pattern in self.patterns: 138 | if pattern.is_dir == is_dir and pattern.matches(path): 139 | inclusive = pattern.inclusive 140 | 141 | #print('+++' if inclusive else '---', path, pattern) 142 | return inclusive 143 | 144 | def __iter__(self): 145 | for path in self.walk(): 146 | yield path 147 | 148 | def __or__(self, other): 149 | return set(self) | set(other) 150 | 151 | def __ror__(self, other): 152 | return self | other 153 | 154 | def __and__(self, other): 155 | return set(self) & set(other) 156 | 157 | def __rand__(self, other): 158 | return self & other 159 | 160 | def walk(self, **kwargs): 161 | """ Like `os.walk` and taking the same keyword arguments, 162 | but generating paths relative to the root. 163 | 164 | Starts in the fileset's root and filters based on its patterns. 165 | If ``with_root=True`` is passed in, the generated paths include 166 | the root path. 167 | """ 168 | lead = '' 169 | if 'with_root' in kwargs and kwargs.pop('with_root'): 170 | lead = self.root.rstrip(os.sep) + os.sep 171 | 172 | for base, dirs, files in os.walk(self.root, **kwargs): 173 | prefix = base[len(self.root):].lstrip(os.sep) 174 | bits = prefix.split(os.sep) if prefix else [] 175 | 176 | for dirname in dirs[:]: 177 | path = '/'.join(bits + [dirname]) 178 | inclusive = self.included(path, is_dir=True) 179 | if inclusive: 180 | yield lead + path + '/' 181 | elif inclusive is False: 182 | dirs.remove(dirname) 183 | 184 | for filename in files: 185 | path = '/'.join(bits + [filename]) 186 | if self.included(path): 187 | yield lead + path 188 | 189 | 190 | def includes(pattern): 191 | """A single inclusive glob pattern.""" 192 | return Pattern(pattern, inclusive=True) 193 | 194 | 195 | def excludes(pattern): 196 | """A single exclusive glob pattern.""" 197 | return Pattern(pattern, inclusive=False) 198 | -------------------------------------------------------------------------------- /src/rituals/util/buildsys.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable= 3 | """ Helpers for handling the build system and project metdata. 4 | 5 | This module is supposed to handle (most of) the differences 6 | between different build tools and config files used in projects. 7 | """ 8 | # Copyright ⓒ 2021 Jürgen Hermann 9 | # 10 | # This program is free software; you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License version 2 as 12 | # published by the Free Software Foundation. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License along 20 | # with this program; if not, write to the Free Software Foundation, Inc., 21 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 22 | # 23 | # The full LICENSE file and source are available at 24 | # https://github.com/jhermann/rituals 25 | import re 26 | import sys 27 | import subprocess 28 | from pathlib import Path 29 | from collections import defaultdict 30 | 31 | from munch import Munch as Bunch 32 | 33 | from . import notify, shell 34 | from .. import config 35 | 36 | 37 | def project_meta(project_root=None): 38 | """ Read and return all project metadata. 39 | """ 40 | root_dir = Path(project_root or config.get_project_root() or '.') 41 | if (root_dir / 'setup.py').exists(): 42 | # this assumes an importable setup.py 43 | if root_dir not in sys.path: 44 | sys.path.append(root_dir) 45 | try: 46 | from setup import project # pylint: disable=no-name-in-module 47 | except ImportError: 48 | # Support another common metadata name found in real-world projects 49 | from setup import setup_args as project # pylint: disable=no-name-in-module 50 | elif (root_dir / 'pyproject.toml').exists(): 51 | import toml 52 | 53 | pyproject = toml.load(root_dir / 'pyproject.toml') 54 | backend = pyproject['build-system']['build-backend'] 55 | if backend.startswith('poetry.'): 56 | poetry = defaultdict(str) 57 | poetry.update(pyproject['tool']['poetry']) 58 | author = (poetry['authors'] or [''])[0] 59 | project = dict( 60 | name=poetry['name'], 61 | version=poetry['version'], 62 | author=author.split('<')[0].strip(), 63 | author_email=(re.findall(r'<([^>]*?)>', author or '<>') or [''])[0], 64 | license=poetry['license'], 65 | packages=[poetry['name'].replace('-', '_')], 66 | url=poetry['homepage'], 67 | description=poetry['description'], 68 | ) 69 | else: 70 | raise NotImplementedError('Unknown build system') 71 | else: 72 | raise NotImplementedError('Unknown project setup') 73 | 74 | def parse_copyright(text): 75 | 'Helper' 76 | line = [x for x in text.splitlines() if 'Copyright' in x][0] 77 | return line.replace('Copyright', '').strip() 78 | 79 | project = Bunch(project) 80 | if 'long_description' in project: 81 | project.copyright = parse_copyright(project.long_description) 82 | elif (root_dir / 'LICENSE').exists(): 83 | text = (root_dir / 'LICENSE').read_text(encoding='utf8', errors='replace') 84 | project.copyright = parse_copyright(text) 85 | if 'copyright' not in project: 86 | raise NotImplementedError('Cannot determine copyright for this project') 87 | 88 | return project 89 | 90 | 91 | def project_version(): 92 | """ Determine project version. 93 | """ 94 | root_dir = Path(config.get_project_root() or '.') 95 | if (root_dir / 'setup.py').exists(): 96 | return shell.capture('python setup.py --version') 97 | else: 98 | raise NotImplementedError() 99 | 100 | 101 | def build(run=subprocess.check_call): 102 | """ Build a project. 103 | """ 104 | root_dir = Path(config.get_project_root() or '.') 105 | if (root_dir / 'setup.py').exists(): 106 | run("python setup.py build", shell=True) 107 | else: 108 | raise NotImplementedError() 109 | -------------------------------------------------------------------------------- /src/rituals/util/filesys.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable= 3 | """ File system helpers. 4 | """ 5 | # Copyright ⓒ 2015 Jürgen Hermann 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License version 2 as 9 | # published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | # 20 | # The full LICENSE file and source are available at 21 | # https://github.com/jhermann/rituals 22 | from __future__ import absolute_import, unicode_literals, print_function 23 | 24 | import os 25 | import re 26 | import tempfile 27 | from contextlib import contextmanager 28 | 29 | import requests 30 | 31 | from ._compat import urlparse, decode_filename 32 | 33 | 34 | 35 | def pretty_path(path, _home_re=re.compile('^' + re.escape(os.path.expanduser('~') + os.sep))): 36 | """Prettify path for humans, and make it Unicode.""" 37 | path = decode_filename(path) 38 | path = _home_re.sub('~' + os.sep, path) 39 | return path 40 | 41 | 42 | @contextmanager 43 | def pushd(path): 44 | """ A context that enters a given directory and restores the old state on exit. 45 | 46 | The original directory is returned as the context variable. 47 | """ 48 | saved = os.getcwd() 49 | os.chdir(path) 50 | try: 51 | yield saved 52 | finally: 53 | os.chdir(saved) 54 | 55 | 56 | # Copied from "rudiments.www" 57 | @contextmanager 58 | def url_as_file(url, ext=None): 59 | """ 60 | Context manager that GETs a given `url` and provides it as a local file. 61 | 62 | The file is in a closed state upon entering the context, 63 | and removed when leaving it, if still there. 64 | 65 | To give the file name a specific extension, use `ext`; 66 | the extension can optionally include a separating dot, 67 | otherwise it will be added. 68 | 69 | Parameters: 70 | url (str): URL to retrieve. 71 | ext (str, optional): Extension for the generated filename. 72 | 73 | Yields: 74 | str: The path to a temporary file with the content of the URL. 75 | 76 | Raises: 77 | requests.RequestException: Base exception of ``requests``, see its 78 | docs for more detailed ones. 79 | 80 | Example: 81 | >>> import io, re, json 82 | >>> with url_as_file('https://api.github.com/meta', ext='json') as meta: 83 | ... meta, json.load(io.open(meta, encoding='ascii'))['hooks'] 84 | (u'/tmp/www-api.github.com-Ba5OhD.json', [u'192.30.252.0/22']) 85 | """ 86 | if ext: 87 | ext = '.' + ext.strip('.') # normalize extension 88 | url_hint = 'www-{}-'.format(urlparse(url).hostname or 'any') 89 | 90 | if url.startswith('file://'): 91 | url = os.path.abspath(url[len('file://'):]) 92 | if os.path.isabs(url): 93 | with open(url, 'rb') as handle: 94 | content = handle.read() 95 | else: 96 | content = requests.get(url).content 97 | 98 | with tempfile.NamedTemporaryFile(suffix=ext or '', prefix=url_hint, delete=False) as handle: 99 | handle.write(content) 100 | 101 | try: 102 | yield handle.name 103 | finally: 104 | if os.path.exists(handle.name): 105 | os.remove(handle.name) 106 | -------------------------------------------------------------------------------- /src/rituals/util/notify.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable= 3 | """ Log notification messages to console. 4 | """ 5 | # Copyright ⓒ 2015 Jürgen Hermann 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License version 2 as 9 | # published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | # 20 | # The full LICENSE file and source are available at 21 | # https://github.com/jhermann/rituals 22 | from __future__ import absolute_import, unicode_literals, print_function 23 | 24 | import sys 25 | 26 | ECHO = True 27 | 28 | 29 | def _flush(): 30 | """Flush all console output.""" 31 | sys.stdout.flush() 32 | sys.stderr.flush() 33 | 34 | 35 | def banner(msg): 36 | """Emit a banner just like Invoke's `run(…, echo=True)`.""" 37 | if ECHO: 38 | _flush() 39 | sys.stderr.write("\033[1;7;32;40m{}\033[0m\n".format(msg)) 40 | sys.stderr.flush() 41 | 42 | 43 | def info(msg): 44 | """Emit a normal message.""" 45 | _flush() 46 | sys.stdout.write(msg + '\n') 47 | sys.stdout.flush() 48 | 49 | 50 | def warning(msg): 51 | """Emit a warning message.""" 52 | _flush() 53 | sys.stderr.write("\033[1;7;33;40mWARNING: {}\033[0m\n".format(msg)) 54 | sys.stderr.flush() 55 | 56 | 57 | def error(msg): 58 | """Emit an error message to stderr.""" 59 | _flush() 60 | sys.stderr.write("\033[1;37;41mERROR: {}\033[0m\n".format(msg)) 61 | sys.stderr.flush() 62 | 63 | 64 | def failure(msg): 65 | """Emit a fatal message and exit.""" 66 | error(msg) 67 | sys.exit(1) 68 | -------------------------------------------------------------------------------- /src/rituals/util/scm/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ rituals.util.scm – Source Code Management support. 3 | """ 4 | # Copyright ⓒ 2015 Jürgen Hermann 5 | # 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License version 2 as 8 | # published by the Free Software Foundation. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License along 16 | # with this program; if not, write to the Free Software Foundation, Inc., 17 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 18 | # 19 | # The full LICENSE file and source are available at 20 | # https://github.com/jhermann/rituals 21 | from __future__ import absolute_import, unicode_literals, print_function 22 | 23 | import os 24 | 25 | from .git import GitProvider 26 | from .null import NullProvider 27 | 28 | 29 | # See "null.NullProvider" for the provider interface reference 30 | SCM_PROVIDER = dict((i.key, i) for i in (GitProvider, NullProvider,)) 31 | 32 | 33 | def auto_detect(workdir): 34 | """ Return string signifying the SCM used in the given directory. 35 | 36 | Currently, 'git' is supported. Anything else returns 'unknown'. 37 | """ 38 | # Any additions here also need a change to `SCM_PROVIDERS`! 39 | if os.path.isdir(os.path.join(workdir, '.git')) and os.path.isfile(os.path.join(workdir, '.git', 'HEAD')): 40 | return 'git' 41 | 42 | return 'unknown' 43 | 44 | 45 | def provider(workdir, commit=True, **kwargs): 46 | """Factory for the correct SCM provider in `workdir`.""" 47 | return SCM_PROVIDER[auto_detect(workdir)](workdir, commit=commit, **kwargs) 48 | -------------------------------------------------------------------------------- /src/rituals/util/scm/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=too-few-public-methods 3 | """ Provider base class. 4 | """ 5 | # Copyright ⓒ 2015 Jürgen Hermann 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License version 2 as 9 | # published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | # 20 | # The full LICENSE file and source are available at 21 | # https://github.com/jhermann/rituals 22 | from __future__ import absolute_import, unicode_literals, print_function 23 | 24 | from .. import notify 25 | from ..shell import run 26 | 27 | 28 | class ProviderBase(): 29 | """Base class for SCM providers.""" 30 | 31 | 32 | def __init__(self, workdir, commit=True, **kwargs): # pylint: disable=unused-argument 33 | self.ctx = kwargs.pop('ctx', None) 34 | self.workdir = workdir 35 | self._commit = commit 36 | 37 | 38 | def run(self, cmd, *args, **kwargs): 39 | """Run a command.""" 40 | runner = self.ctx.run if self.ctx else None 41 | return run(cmd, runner=runner, *args, **kwargs) 42 | 43 | 44 | def run_elective(self, cmd, *args, **kwargs): 45 | """Run a command, or just echo it, depending on `commit`.""" 46 | if self._commit: 47 | return self.run(cmd, *args, **kwargs) 48 | else: 49 | notify.warning("WOULD RUN: {}".format(cmd)) 50 | kwargs = kwargs.copy() 51 | kwargs['echo'] = False 52 | return self.run('true', *args, **kwargs) 53 | -------------------------------------------------------------------------------- /src/rituals/util/scm/git.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=no-self-use 3 | """ git SCM provider. 4 | """ 5 | # Copyright ⓒ 2015 Jürgen Hermann 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License version 2 as 9 | # published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | # 20 | # The full LICENSE file and source are available at 21 | # https://github.com/jhermann/rituals 22 | from __future__ import absolute_import, unicode_literals, print_function 23 | 24 | import os 25 | import re 26 | from datetime import datetime 27 | 28 | from invoke import exceptions 29 | 30 | from .. import notify, buildsys 31 | from .base import ProviderBase 32 | from ..shell import capture 33 | 34 | 35 | RUN_KWARGS = dict() 36 | 37 | 38 | class GitProvider(ProviderBase): 39 | """ git SCM provider. 40 | 41 | Expects a working `git` executable in the path, 42 | having a reasonably current version. 43 | """ 44 | key = 'git' 45 | 46 | 47 | def workdir_is_clean(self, quiet=False): 48 | """ Check for uncommitted changes, return `True` if everything is clean. 49 | 50 | Inspired by http://stackoverflow.com/questions/3878624/. 51 | """ 52 | # Update the index 53 | self.run('git update-index -q --ignore-submodules --refresh', **RUN_KWARGS) 54 | unchanged = True 55 | 56 | # Disallow unstaged changes in the working tree 57 | try: 58 | self.run('git diff-files --quiet --ignore-submodules --', report_error=False, **RUN_KWARGS) 59 | except exceptions.Failure: 60 | unchanged = False 61 | if not quiet: 62 | notify.warning('You have unstaged changes!') 63 | self.run('git diff-files --name-status -r --ignore-submodules -- >&2', **RUN_KWARGS) 64 | 65 | # Disallow uncommitted changes in the index 66 | try: 67 | self.run('git diff-index --cached --quiet HEAD --ignore-submodules --', report_error=False, **RUN_KWARGS) 68 | except exceptions.Failure: 69 | unchanged = False 70 | if not quiet: 71 | notify.warning('Your index contains uncommitted changes!') 72 | self.run('git diff-index --cached --name-status -r --ignore-submodules HEAD -- >&2', **RUN_KWARGS) 73 | 74 | return unchanged 75 | 76 | 77 | def add_file(self, filename): 78 | """Stage a file for committing.""" 79 | self.run('git add "{}"'.format(filename), **RUN_KWARGS) 80 | 81 | 82 | def commit(self, message): 83 | """Commit pending changes.""" 84 | self.run_elective('git commit -m "{}"'.format(message)) 85 | 86 | 87 | def tag(self, label, message=None): 88 | """Tag the current workdir state.""" 89 | options = ' -m "{}" -a'.format(message) if message else '' 90 | self.run_elective('git tag{} "{}"'.format(options, label)) 91 | 92 | 93 | def pep440_dev_version(self, verbose=False, non_local=False): 94 | """ Return a PEP-440 dev version appendix to the main version number. 95 | 96 | Result is ``None`` if the workdir is in a release-ready state 97 | (i.e. clean and properly tagged). 98 | """ 99 | version = buildsys.project_version() 100 | if verbose: 101 | notify.info("project version = '{}'".format(version)) 102 | 103 | now = '{:%Y%m%d!%H%M}'.format(datetime.now()) 104 | tag = capture("git describe --long --exclude stable --dirty='!{}'".format(now), echo=verbose) 105 | if verbose: 106 | notify.info("git describe = '{}'".format(tag)) 107 | try: 108 | tag, date, time = tag.split('!') 109 | except ValueError: 110 | date = time = '' 111 | try: 112 | tag, commits, short_hash = tag.rsplit('-', 3) 113 | except ValueError as cause: 114 | raise ValueError("Cannot split tag {!r}: {}".format(tag, cause)) 115 | label = tag 116 | if re.match(r"v[0-9]+(\.[0-9]+)*", label): 117 | label = label[1:] 118 | 119 | # Make a PEP-440 version appendix, the format is: 120 | # [N!]N(.N)*[{a|b|rc}N][.postN][.devN][+] 121 | if commits == '0' and label == version: 122 | pep440 = None 123 | else: 124 | local_part = [ 125 | re.sub(r"[^a-zA-Z0-9]+", '.', label).strip('.'), # reduce to alphanum and dots 126 | short_hash, 127 | date + ('T' + time if time else ''), 128 | ] 129 | build_number = os.environ.get('BUILD_NUMBER', 'n/a') 130 | if build_number.isdigit(): 131 | local_part.extend(['ci', build_number]) 132 | if verbose: 133 | notify.info("Adding CI build ID #{} to version".format(build_number)) 134 | 135 | local_part = [i for i in local_part if i] 136 | pep440 = '.dev{}+{}'.format(commits, '.'.join(local_part).strip('.')) 137 | if non_local: 138 | pep440, _ = pep440.split('+', 1) 139 | 140 | return pep440 141 | -------------------------------------------------------------------------------- /src/rituals/util/scm/null.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=no-self-use 3 | """ Provider for unknown SCM systems. 4 | """ 5 | # Copyright ⓒ 2015 Jürgen Hermann 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License version 2 as 9 | # published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | # 20 | # The full LICENSE file and source are available at 21 | # https://github.com/jhermann/rituals 22 | from __future__ import absolute_import, unicode_literals, print_function 23 | 24 | import os 25 | from datetime import datetime 26 | 27 | from .. import notify 28 | from .base import ProviderBase 29 | 30 | 31 | class NullProvider(ProviderBase): 32 | """ Stub provider for unknown SCM systems. 33 | 34 | This implements the provider interface, mostly emitting warnings. 35 | """ 36 | key = 'unknown' 37 | 38 | 39 | def workdir_is_clean(self, quiet=False): 40 | """Check for uncommitted changes, return `True` if everything is clean.""" 41 | if not quiet: 42 | notify.warning('Unsupported SCM: Cannot check for uncommitted changes,' 43 | ' assuming you did it!') 44 | 45 | return True 46 | 47 | 48 | def add_file(self, filename): 49 | """Stage a file for committing, or commit it directly (depending on the SCM).""" 50 | notify.warning('Unsupported SCM: Please commit the file "{}"'.format(filename)) 51 | 52 | 53 | def commit(self, message): 54 | """Commit pending changes.""" 55 | notify.warning('Unsupported SCM: Make sure you commit pending changes for "{}"!'.format(message)) 56 | 57 | 58 | def tag(self, label, message=None): 59 | """Tag the current workdir state.""" 60 | notify.warning('Unsupported SCM: Make sure you apply the "{}" tag after commit!{}'.format( 61 | label, ' [message={}]'.format(message) if message else '', 62 | )) 63 | 64 | 65 | def pep440_dev_version(self, verbose=False, non_local=False): 66 | """Return a PEP-440 dev version appendix to the main version number.""" 67 | # Always return a timestamp 68 | pep440 = '.dev{:%Y%m%d%H%M}'.format(datetime.now()) 69 | 70 | if not non_local: 71 | build_number = os.environ.get('BUILD_NUMBER', 'n/a') 72 | if build_number.isdigit(): 73 | pep440 += '+ci.{}'.format(build_number) 74 | if verbose: 75 | notify.info("Adding CI build ID #{} to version".format(build_number)) 76 | 77 | return pep440 78 | -------------------------------------------------------------------------------- /src/rituals/util/shell.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable= 3 | """ Shell command calls. 4 | """ 5 | # Copyright ⓒ 2015 Jürgen Hermann 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License version 2 as 9 | # published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | # 20 | # The full LICENSE file and source are available at 21 | # https://github.com/jhermann/rituals 22 | from __future__ import absolute_import, unicode_literals, print_function 23 | 24 | import sys 25 | 26 | from invoke import run as invoke_run 27 | from invoke import exceptions 28 | 29 | from . import notify 30 | 31 | 32 | def capture(cmd, **kw): 33 | """Run a command and return its stripped captured output.""" 34 | kw = kw.copy() 35 | kw['hide'] = 'out' 36 | if not kw.get('echo', False): 37 | kw['echo'] = False 38 | ignore_failures = kw.pop('ignore_failures', False) 39 | try: 40 | return invoke_run(cmd, **kw).stdout.strip() 41 | except exceptions.Failure as exc: 42 | if not ignore_failures: 43 | notify.error("Command `{}` failed with RC={}!".format(cmd, exc.result.return_code,)) 44 | raise 45 | return None 46 | 47 | 48 | def run(cmd, **kw): 49 | """Run a command and flush its output.""" 50 | kw = kw.copy() 51 | kw.setdefault('warn', False) # make extra sure errors don't get silenced 52 | 53 | report_error = kw.pop('report_error', True) 54 | runner = kw.pop('runner', invoke_run) 55 | 56 | try: 57 | return runner(cmd, **kw) 58 | except exceptions.Failure as exc: 59 | sys.stdout.flush() 60 | sys.stderr.flush() 61 | if report_error: 62 | notify.error("Command `{}` failed with RC={}!".format(cmd, exc.result.return_code,)) 63 | raise 64 | finally: 65 | sys.stdout.flush() 66 | sys.stderr.flush() 67 | -------------------------------------------------------------------------------- /src/tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable= 3 | """ py.test dynamic configuration. 4 | 5 | For details needed to understand these tests, refer to: 6 | https://pytest.org/ 7 | http://pythontesting.net/start-here/ 8 | """ 9 | # Copyright ⓒ 2015 Jürgen Hermann 10 | # 11 | # This program is free software; you can redistribute it and/or modify 12 | # it under the terms of the GNU General Public License version 2 as 13 | # published by the Free Software Foundation. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License along 21 | # with this program; if not, write to the Free Software Foundation, Inc., 22 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 23 | # 24 | # The full LICENSE file and source are available at 25 | # https://github.com/jhermann/rituals 26 | from __future__ import absolute_import, unicode_literals, print_function 27 | 28 | import logging 29 | 30 | import pytest 31 | 32 | 33 | # Globally available fixtures 34 | @pytest.fixture(scope='session') 35 | def logger(): 36 | """Test logger instance as a fixture.""" 37 | logging.basicConfig(level=logging.DEBUG) 38 | return logging.getLogger('tests') 39 | -------------------------------------------------------------------------------- /src/tests/test_acts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=wildcard-import, unused-wildcard-import, missing-docstring 3 | # pylint: disable=redefined-outer-name, invalid-name, no-self-use 4 | """ Tests for `rituals.acts`. 5 | """ 6 | # Copyright ⓒ 2015 Jürgen Hermann 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License version 2 as 10 | # published by the Free Software Foundation. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License along 18 | # with this program; if not, write to the Free Software Foundation, Inc., 19 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | # 21 | # The full LICENSE file and source are available at 22 | # https://github.com/jhermann/rituals 23 | from __future__ import absolute_import, unicode_literals, print_function 24 | 25 | #import unittest 26 | 27 | #import pytest 28 | 29 | import tasks # pylint: disable=unused-import 30 | from rituals.acts import * 31 | -------------------------------------------------------------------------------- /src/tests/test_acts_basic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=wildcard-import, unused-wildcard-import, missing-docstring 3 | # pylint: disable=redefined-outer-name, invalid-name, no-self-use, unused-import 4 | """ Tests for `rituals.acts.basic`. 5 | """ 6 | # Copyright ⓒ 2015 Jürgen Hermann 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License version 2 as 10 | # published by the Free Software Foundation. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License along 18 | # with this program; if not, write to the Free Software Foundation, Inc., 19 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | # 21 | # The full LICENSE file and source are available at 22 | # https://github.com/jhermann/rituals 23 | from __future__ import absolute_import, unicode_literals, print_function 24 | 25 | #import unittest 26 | 27 | #import pytest 28 | 29 | import tasks # pylint: disable=unused-import 30 | from rituals.acts import basic 31 | -------------------------------------------------------------------------------- /src/tests/test_acts_devpi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=wildcard-import, unused-wildcard-import, missing-docstring 3 | # pylint: disable=redefined-outer-name, invalid-name, no-self-use 4 | """ Tests for `rituals.acts.devpi`. 5 | """ 6 | # Copyright ⓒ 2015 Jürgen Hermann 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License version 2 as 10 | # published by the Free Software Foundation. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License along 18 | # with this program; if not, write to the Free Software Foundation, Inc., 19 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | # 21 | # The full LICENSE file and source are available at 22 | # https://github.com/jhermann/rituals 23 | from __future__ import absolute_import, unicode_literals, print_function 24 | 25 | import unittest 26 | 27 | from munch import Munch as Bunch 28 | import pytest 29 | 30 | import tasks # pylint: disable=unused-import 31 | from rituals.acts import devpi 32 | 33 | 34 | class GetDevpiUrlTest(unittest.TestCase): 35 | 36 | def test_get_devpi_url_delivers_simpleindex_url(self): 37 | ctx = Bunch(run=lambda cmd, **_: Bunch(stdout="first: line\n simpleindex: THE-URL/\n other: foobar\n")) 38 | url = devpi.get_devpi_url(ctx) 39 | assert url == "THE-URL", "get_devpi_url() extracted the URL" 40 | 41 | 42 | def test_get_devpi_url_ignores_ansi_codes(self): 43 | ctx = Bunch(run=lambda cmd, **_: Bunch(stdout="first: line\n\x1b1m simpleindex: THE-URL/\x1b0m\n other: foobar\n")) 44 | url = devpi.get_devpi_url(ctx) 45 | assert url == "THE-URL", "get_devpi_url() extracted the URL" 46 | 47 | 48 | def test_get_devpi_url_detects_missing_simpleindex_url(self): 49 | ctx = Bunch(run=lambda cmd, **_: Bunch(stdout=" notsosimpleindex: THE-URL/")) 50 | with pytest.raises(LookupError): 51 | _ = devpi.get_devpi_url(ctx) 52 | -------------------------------------------------------------------------------- /src/tests/test_acts_documentation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=wildcard-import, unused-wildcard-import, missing-docstring 3 | # pylint: disable=redefined-outer-name, invalid-name, no-self-use, unused-import 4 | """ Tests for `rituals.acts.documentation`. 5 | """ 6 | # Copyright ⓒ 2015 Jürgen Hermann 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License version 2 as 10 | # published by the Free Software Foundation. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License along 18 | # with this program; if not, write to the Free Software Foundation, Inc., 19 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | # 21 | # The full LICENSE file and source are available at 22 | # https://github.com/jhermann/rituals 23 | from __future__ import absolute_import, unicode_literals, print_function 24 | 25 | #import unittest 26 | 27 | #import pytest 28 | 29 | import tasks # pylint: disable=unused-import 30 | from rituals.acts import documentation 31 | -------------------------------------------------------------------------------- /src/tests/test_acts_github.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=wildcard-import, unused-wildcard-import, missing-docstring 3 | # pylint: disable=redefined-outer-name, invalid-name, no-self-use, unused-import 4 | """ Tests for `rituals.acts.github`. 5 | """ 6 | # Copyright ⓒ 2015 Jürgen Hermann 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License version 2 as 10 | # published by the Free Software Foundation. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License along 18 | # with this program; if not, write to the Free Software Foundation, Inc., 19 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | # 21 | # The full LICENSE file and source are available at 22 | # https://github.com/jhermann/rituals 23 | from __future__ import absolute_import, unicode_literals, print_function 24 | 25 | #import unittest 26 | 27 | #import pytest 28 | 29 | import tasks # pylint: disable=unused-import 30 | from rituals.acts import github 31 | -------------------------------------------------------------------------------- /src/tests/test_acts_inspection.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=wildcard-import, unused-wildcard-import, missing-docstring 3 | # pylint: disable=redefined-outer-name, invalid-name, no-self-use 4 | """ Tests for `rituals.acts.inspection`. 5 | """ 6 | # Copyright ⓒ 2015 Jürgen Hermann 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License version 2 as 10 | # published by the Free Software Foundation. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License along 18 | # with this program; if not, write to the Free Software Foundation, Inc., 19 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | # 21 | # The full LICENSE file and source are available at 22 | # https://github.com/jhermann/rituals 23 | from __future__ import absolute_import, unicode_literals, print_function 24 | 25 | import unittest 26 | 27 | #import pytest 28 | from munch import Munch as Bunch 29 | from invoke.context import Context 30 | 31 | import tasks # pylint: disable=unused-import 32 | from rituals.acts import inspection 33 | 34 | 35 | class FakeContext(Context): 36 | def __init__(self, taskname): 37 | Context.__init__(self, config=inspection.namespace.configuration(taskname)) 38 | self.memo = Bunch(cmd='') 39 | 40 | def run(self, command, **kwargs): 41 | self.memo.cmd = command 42 | 43 | def _modify(self, keypath, key, value): 44 | pass 45 | 46 | 47 | class PylintTest(unittest.TestCase): 48 | 49 | def call_pylint(self, **kwargs): 50 | ctx = FakeContext('pylint') 51 | inspection.pylint(ctx, **kwargs) 52 | return ctx.memo.cmd.split() 53 | 54 | def test_pylint_command_is_called(self): 55 | parts = self.call_pylint() 56 | assert parts, "checking command is not empty" 57 | assert parts[0] == 'pylint', "pylint is actually called" 58 | assert '--reports=n' in parts, "no pylint reports by default" 59 | assert '--rcfile=project.d/pylint.cfg' in parts, "pylint config is loaded" 60 | assert '"src/tests/conftest.py"' in parts, "test files in pylint command: " + repr(parts) 61 | assert '"setup.py"' in parts, "root files in pylint command: " + repr(parts) 62 | 63 | def test_pylint_can_skip_test_files(self): 64 | parts = self.call_pylint(skip_tests=True) 65 | assert not any(i.endswith('/src/tests/conftest.py"') for i in parts), "no test files in pylint command: " + repr(parts) 66 | 67 | def test_pylint_can_skip_root_files(self): 68 | parts = self.call_pylint(skip_root=True) 69 | assert '"setup.py"' not in parts, "no root files in pylint command: " + repr(parts) 70 | 71 | def test_pylint_report_can_be_activated(self): 72 | parts = self.call_pylint(reports=True) 73 | assert '--reports=y' in parts, "no pylint reports by default" 74 | -------------------------------------------------------------------------------- /src/tests/test_acts_releasing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=wildcard-import, unused-wildcard-import, missing-docstring 3 | # pylint: disable=redefined-outer-name, invalid-name, no-self-use, unused-import 4 | """ Tests for `rituals.acts.releasing`. 5 | """ 6 | # Copyright ⓒ 2015 Jürgen Hermann 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License version 2 as 10 | # published by the Free Software Foundation. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License along 18 | # with this program; if not, write to the Free Software Foundation, Inc., 19 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | # 21 | # The full LICENSE file and source are available at 22 | # https://github.com/jhermann/rituals 23 | from __future__ import absolute_import, unicode_literals, print_function 24 | 25 | #import unittest 26 | 27 | #import pytest 28 | 29 | import tasks # pylint: disable=unused-import 30 | from rituals.acts import releasing 31 | -------------------------------------------------------------------------------- /src/tests/test_acts_testing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=wildcard-import, unused-wildcard-import, missing-docstring 3 | # pylint: disable=redefined-outer-name, invalid-name, no-self-use 4 | """ Tests for `rituals.acts.testing`. 5 | """ 6 | # Copyright ⓒ 2015 Jürgen Hermann 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License version 2 as 10 | # published by the Free Software Foundation. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License along 18 | # with this program; if not, write to the Free Software Foundation, Inc., 19 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | # 21 | # The full LICENSE file and source are available at 22 | # https://github.com/jhermann/rituals 23 | from __future__ import absolute_import, unicode_literals, print_function 24 | 25 | import unittest 26 | 27 | from invoke.context import Context 28 | from munch import Munch as Bunch 29 | #import pytest 30 | 31 | import tasks # pylint: disable=unused-import 32 | from rituals.acts import testing 33 | 34 | 35 | class FakeContext(Context): 36 | def __init__(self, taskname): 37 | Context.__init__(self, config=testing.namespace.configuration(taskname)) 38 | self.memo = Bunch(cmd='') 39 | 40 | def run(self, command, **kwargs): 41 | self.memo.cmd = command 42 | 43 | def _modify(self, keypath, key, value): 44 | pass 45 | 46 | 47 | class PytestTest(unittest.TestCase): 48 | 49 | def call_pytest(self, **kwargs): 50 | ctx = FakeContext('pytest') 51 | testing.pytest(ctx, **kwargs) 52 | return ctx.memo.cmd.split() 53 | 54 | def test_pytest_command_is_called(self): 55 | parts = self.call_pytest() 56 | assert parts, "testing command is not empty" 57 | assert parts[0].endswith('py.test'), "py.test is actually called" 58 | 59 | def test_pytest_task_takes_options(self): 60 | parts = self.call_pytest(opts='--verbose') 61 | assert '--verbose' in parts[-2:], "py.test option is added" 62 | 63 | def test_pytest_task_detects_coverage(self): 64 | parts = self.call_pytest(opts='--verbose') 65 | assert '--cov' in parts, "py.test option is added" 66 | 67 | 68 | class ToxTest(unittest.TestCase): 69 | 70 | def test_tox_command_is_called(self): 71 | ctx = FakeContext('tox') 72 | testing.tox(ctx) 73 | parts = ctx.memo.cmd.split() 74 | assert parts, "tox command is not empty" 75 | assert 'tox' in parts[:2], "tox is actually called" 76 | 77 | def test_tox_task_takes_environment(self): 78 | ctx = FakeContext('tox') 79 | testing.tox(ctx, env_list='py34') 80 | assert '-e py34' in ctx.memo.cmd, "tox environment is selected" 81 | -------------------------------------------------------------------------------- /src/tests/test_antglob.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=wildcard-import, missing-docstring, redefined-outer-name, invalid-name, no-self-use 3 | # pylint: disable=unused-wildcard-import, ungrouped-imports 4 | """ Tests for `rituals.util.antglob`. 5 | """ 6 | # 7 | # The MIT License (MIT) 8 | # 9 | # Original source (2014-02-17) from https://github.com/zacherates/fileset.py 10 | # Copyright (c) 2012 Aaron Maenpaa 11 | # 12 | # Modifications at https://github.com/jhermann/rituals 13 | # Copyright ⓒ 2015 Jürgen Hermann 14 | # 15 | # Permission is hereby granted, free of charge, to any person obtaining a copy 16 | # of this software and associated documentation files (the "Software"), to deal 17 | # in the Software without restriction, including without limitation the rights 18 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | # copies of the Software, and to permit persons to whom the Software is 20 | # furnished to do so, subject to the following conditions: 21 | # 22 | # The above copyright notice and this permission notice shall be included in all 23 | # copies or substantial portions of the Software. 24 | # 25 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | # SOFTWARE. 32 | from __future__ import absolute_import, unicode_literals, print_function 33 | 34 | import os 35 | import shutil 36 | import tempfile 37 | import unittest 38 | from os.path import join 39 | 40 | import pytest 41 | 42 | from rituals.util import antglob 43 | from rituals.util.antglob import * 44 | 45 | 46 | class Glob2reTest(unittest.TestCase): 47 | 48 | def test_antglob_star_import_does_not_include_glob2re(self): 49 | assert 'glob2re' not in globals() 50 | 51 | def test_star_converts_to_non_slash_sequences(self): 52 | assert antglob.glob2re('a*b') == 'a[^/]*b' 53 | assert antglob.glob2re('a*b*') == 'a[^/]*b[^/]*' 54 | 55 | def test_charsets_are_left_intact(self): 56 | assert antglob.glob2re('[abc]') == '[abc]' 57 | 58 | def test_backslash_and_plus_are_properly_escaped(self): 59 | assert antglob.glob2re('\\') == r'\\' 60 | assert antglob.glob2re('+') == r'\+' 61 | 62 | 63 | class ParseGlobTest(unittest.TestCase): 64 | 65 | def test_antglob_star_import_does_not_include_parse_glob(self): 66 | assert 'parse_glob' not in globals() 67 | 68 | def test_empty_inputs_create_an_empty_output(self): 69 | assert not list(antglob.parse_glob(None)) 70 | assert not list(antglob.parse_glob('')) 71 | 72 | def test_twin_star_translates_to_null_match_or_non_empty_sequence_plus_slash(self): 73 | assert list(antglob.parse_glob('a/**/b')) == ['a/', '(|.+/)', 'b'] 74 | 75 | def test_path_parts_have_a_trailing_slash(self): 76 | assert list(antglob.parse_glob('a/b/')) == ['a/', 'b/', ''] 77 | 78 | def test_embedded_star_in_path_part_matches_non_slash_middle(self): 79 | assert list(antglob.parse_glob('a*b/c')) == ['a[^/]*b/', 'c'] 80 | 81 | def test_absolute_paths_emit_a_starting_slash(self): 82 | assert list(antglob.parse_glob('/root')) == ['/', 'root'] 83 | 84 | 85 | @pytest.fixture(scope='module') 86 | def root(request): 87 | """ Root of filesystem layout for tests. 88 | 89 | ./foo/ 90 | ./foo/bar/ 91 | ./foo/bar/.hidden/ 92 | ./foo/bar/baz/ 93 | ./foo/bar/baz/... 94 | ./foo/bar/baz/three 95 | ./foo/bar/baz/three.py 96 | ./foo/bar/two 97 | ./foo/bar/two.py 98 | ./foo/one 99 | ./foo/one.py 100 | ./zero 101 | ./zero.py 102 | """ 103 | rootpath = tempfile.mkdtemp() 104 | request.addfinalizer(lambda: shutil.rmtree(rootpath)) 105 | 106 | dir_foo = join(rootpath, "foo") 107 | dir_bar = join(dir_foo, "bar") 108 | dir_baz = join(dir_bar, "baz") 109 | hidden = join(dir_bar, ".hidden") 110 | os.makedirs(dir_baz) 111 | os.makedirs(hidden) 112 | 113 | new = ( 114 | join(rootpath, "zero.py"), 115 | join(rootpath, "zero"), 116 | join(dir_foo, "one.py"), 117 | join(dir_foo, "one"), 118 | join(dir_bar, "two.py"), 119 | join(dir_bar, "two"), 120 | join(dir_baz, "three.py"), 121 | join(dir_baz, "three"), 122 | join(dir_baz, "..."), 123 | ) 124 | 125 | for path in new: 126 | open(path, "a").close() 127 | 128 | return rootpath 129 | 130 | 131 | def assert_sets_equal(s1, s2): 132 | """Helper to compare sets.""" 133 | assert list(sorted(s1)) == list(sorted(s2)) 134 | 135 | 136 | def check_glob(root, pattern, expected): 137 | assert_sets_equal(antglob.FileSet(root, [antglob.includes(pattern)]), expected) 138 | 139 | 140 | ALL_THE_PIES = ["zero.py", "foo/one.py", "foo/bar/two.py", "foo/bar/baz/three.py"] 141 | 142 | def test_patterns_that_cause_an_empty_result(root): 143 | cases = ( 144 | ("foo/blah/*.py", []), 145 | ("*.blah", []), 146 | ("**/hree.py", []), 147 | ("bar/foo/two.py", []), 148 | ) 149 | 150 | for pattern, results in cases: 151 | check_glob(root, pattern, results) 152 | 153 | 154 | def test_patterns_with_stars_and_twin_stars(root): 155 | cases = [ 156 | ("*.py", ["zero.py"]), 157 | ("foo/*.py", ["foo/one.py"]), 158 | ("*/*", ["foo/one.py", "foo/one"]), 159 | ("**/*a*/**/*.py", ["foo/bar/two.py", "foo/bar/baz/three.py"]) 160 | ] 161 | 162 | for pattern, results in cases: 163 | check_glob(root, pattern, results) 164 | 165 | 166 | def test_twin_star_pattern_matches_root_files(root): 167 | root_py = set(antglob.FileSet(root, [antglob.includes('*.py')])) 168 | all_py = set(antglob.FileSet(root, [antglob.includes('**/*.py')])) 169 | assert len(root_py) == 1 170 | assert len(all_py) > 1 171 | assert root_py <= all_py 172 | 173 | 174 | def test_direct_matches_for_plain_filenames_and_paths(root): 175 | cases = [ 176 | ("zero.py", ["zero.py"]), 177 | ("foo/bar/baz/three.py", ["foo/bar/baz/three.py"]), 178 | ] 179 | 180 | for pattern, results in cases: 181 | check_glob(root, pattern, results) 182 | 183 | 184 | def test_recursive_patterns_using_twin_stars_once_or_twice(root): 185 | cases = ( 186 | ("**/...", ["foo/bar/baz/..."]), 187 | ("**/*.py", ALL_THE_PIES), 188 | ("**/baz/**/*.py", ["foo/bar/baz/three.py"]), 189 | ) 190 | 191 | for pattern, results in cases: 192 | check_glob(root, pattern, results) 193 | 194 | 195 | def test_multiple_combinations_of_includes_and_excludes(root): 196 | a = FileSet(root, [ 197 | includes("*.py"), 198 | includes("*/*.py"), 199 | ]) 200 | 201 | b = FileSet(root, [ 202 | includes("**/zero*"), 203 | includes("**/one*"), 204 | ]) 205 | 206 | c = FileSet(root, [ 207 | includes("**/*"), 208 | excludes("**/*.py"), 209 | excludes("**/baz/*"), 210 | ]) 211 | 212 | d = FileSet(root, [ 213 | includes("**/*.py"), 214 | excludes("**/foo/**/*"), 215 | includes("**/baz/**/*.py"), 216 | ]) 217 | 218 | d2 = FileSet(root, [ 219 | includes("**/*.py"), 220 | excludes("**/foo/"), 221 | includes("**/baz/**/*.py"), 222 | ]) 223 | 224 | e = FileSet(root, [ 225 | includes("**/*.py"), 226 | excludes("**/two.py"), 227 | excludes("**/three.py"), 228 | ]) 229 | 230 | cases = ( 231 | (a, ["zero.py", "foo/one.py"]), 232 | (b, ["zero.py", "zero", "foo/one.py", "foo/one"]), 233 | (c, ["zero", "foo/one", "foo/bar/two"]), 234 | (d, ["zero.py", "foo/bar/baz/three.py"]), 235 | (d2, ["zero.py"]), 236 | (e, ["zero.py", "foo/one.py"]), 237 | ) 238 | 239 | for result, expected in cases: 240 | assert_sets_equal(result, expected) 241 | 242 | 243 | def test_drivers_fileset_operator_combinations(root): 244 | a = FileSet(root, [ 245 | includes("**/*.py") 246 | ]) 247 | 248 | b = FileSet(root, [ 249 | includes("**/*"), 250 | excludes("**/bar/**/*"), 251 | ]) 252 | 253 | c = FileSet(root, []) 254 | 255 | cases = ( 256 | (a | b, ALL_THE_PIES + ["zero", "foo/one"]), 257 | (a & b, ["zero.py", "foo/one.py"]), 258 | (a | c, a), 259 | (a & c, []), 260 | (a | b | c, ALL_THE_PIES + ["zero", "foo/one"]), 261 | ((a | b) & c, []), 262 | (a & b & c, []), 263 | ) 264 | 265 | for result, expected in cases: 266 | assert_sets_equal(result, expected) 267 | 268 | 269 | def test_directory_patterns_return_matches(root): 270 | assert_sets_equal(antglob.FileSet(root, "foo/"), ["foo/"]) 271 | assert_sets_equal(antglob.FileSet(root, "**/baz/"), ["foo/bar/baz/"]) 272 | assert_sets_equal(antglob.FileSet(root, "**/b*/"), ["foo/bar/", "foo/bar/baz/"]) 273 | assert_sets_equal(antglob.FileSet(root, "**/.*/"), ["foo/bar/.hidden/"]) 274 | assert_sets_equal(antglob.FileSet(root, "**/.*"), ["foo/bar/baz/..."]) 275 | assert_sets_equal(antglob.FileSet(root, "*/"), ["foo/"]) 276 | assert_sets_equal(antglob.FileSet(root, "*/*/*/"), ["foo/bar/.hidden/", "foo/bar/baz/"]) 277 | 278 | 279 | def test_glob_patterns_with_normal_char_sets(root): 280 | assert_sets_equal(antglob.FileSet(root, "**/bar/[.b]*/"), ["foo/bar/.hidden/", "foo/bar/baz/"]) 281 | 282 | 283 | def test_glob_patterns_with_inverted_char_sets(root): 284 | assert_sets_equal(antglob.FileSet(root, "**/baz/[^.]*"), ["foo/bar/baz/three", "foo/bar/baz/three.py"]) 285 | assert_sets_equal(antglob.FileSet(root, "**/baz/[^t]*"), ["foo/bar/baz/..."]) 286 | 287 | 288 | def test_string_patterns_are_inclusive_by_default(root): 289 | assert list(antglob.FileSet(root, "*.py")) == ["zero.py"] 290 | 291 | 292 | def test_lists_of_string_patterns_are_combined(root): 293 | patterns = ["*.py", "foo/bar/tw*", "foo/bar/*.py"] 294 | assert set(antglob.FileSet(root, patterns[:1])) == set(["zero.py"]) 295 | assert set(antglob.FileSet(root, patterns)) == set(["zero.py", "foo/bar/two", "foo/bar/two.py"]) 296 | -------------------------------------------------------------------------------- /src/tests/test_compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=wildcard-import, unused-wildcard-import, missing-docstring 3 | # pylint: disable=invalid-name, no-self-use, redefined-outer-name 4 | """ Tests for `rituals.util._compat`. 5 | """ 6 | # Copyright ⓒ 2015 Jürgen Hermann 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License version 2 as 10 | # published by the Free Software Foundation. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License along 18 | # with this program; if not, write to the Free Software Foundation, Inc., 19 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | # 21 | # The full LICENSE file and source are available at 22 | # https://github.com/jhermann/rituals 23 | from __future__ import absolute_import, unicode_literals, print_function 24 | 25 | import re 26 | import datetime 27 | #import unittest 28 | 29 | #import pytest 30 | 31 | from rituals.util._compat import * # pylint: disable=redefined-builtin 32 | 33 | 34 | def test_python_version_flags_are_consistent(): 35 | if PYPY: 36 | assert not PY2 37 | if PY2: 38 | assert not PYPY 39 | 40 | 41 | def test_isodate_is_formatted_as_expected(): 42 | result = isodate() 43 | assert re.match(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}", result) 44 | 45 | 46 | def test_isodate_with_microseconds_is_formatted_as_expected(): 47 | result = isodate(microseconds=True) 48 | assert re.match(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{6}", result) 49 | 50 | 51 | def test_isodate_with_specific_date_works(): 52 | result = isodate(datetime.datetime(1998, 1, 23, 12, 34, 56)) 53 | assert result == "1998-01-23 12:34:56" 54 | -------------------------------------------------------------------------------- /src/tests/test_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=wildcard-import, unused-wildcard-import, missing-docstring 3 | # pylint: disable=invalid-name, no-self-use, redefined-outer-name 4 | """ Tests for `rituals.config`. 5 | """ 6 | # Copyright ⓒ 2015 Jürgen Hermann 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License version 2 as 10 | # published by the Free Software Foundation. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License along 18 | # with this program; if not, write to the Free Software Foundation, Inc., 19 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | # 21 | # The full LICENSE file and source are available at 22 | # https://github.com/jhermann/rituals 23 | from __future__ import absolute_import, unicode_literals, print_function 24 | 25 | #import unittest 26 | 27 | #import pytest 28 | 29 | from rituals import config 30 | 31 | 32 | def test_set_maven_layout(): 33 | config.DEFAULTS, saved = config.DEFAULTS.copy(), config.DEFAULTS 34 | assert saved['srcdir'] == 'src' and saved['testdir'] == 'src/tests' 35 | try: 36 | config.set_maven_layout() 37 | assert config.DEFAULTS['srcdir'] == 'src/main/python' 38 | assert config.DEFAULTS['testdir'] == 'src/test/python' 39 | finally: 40 | config.DEFAULTS = saved 41 | 42 | 43 | def test_set_flat_layout(): 44 | config.DEFAULTS, saved = config.DEFAULTS.copy(), config.DEFAULTS 45 | assert saved['srcdir'] == 'src' and saved['testdir'] == 'src/tests' 46 | try: 47 | config.set_flat_layout() 48 | assert config.DEFAULTS['srcdir'] == '.' 49 | assert config.DEFAULTS['testdir'] == 'tests' 50 | finally: 51 | config.DEFAULTS = saved 52 | -------------------------------------------------------------------------------- /src/tests/test_easy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=wildcard-import, missing-docstring, redefined-outer-name, invalid-name, no-self-use, 3 | """ Tests for `rituals.easy`. 4 | """ 5 | # Copyright ⓒ 2015 Jürgen Hermann 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License version 2 as 9 | # published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | # 20 | # The full LICENSE file and source are available at 21 | # https://github.com/jhermann/rituals 22 | from __future__ import absolute_import, unicode_literals, print_function 23 | 24 | import os 25 | #import unittest 26 | 27 | #import pytest 28 | 29 | import tasks 30 | 31 | from rituals import easy 32 | 33 | 34 | def test_tasks_module_directory_contains_a_setup_sibling(): 35 | """Check that imported `tasks` module is the correct one.""" 36 | assert os.path.exists(os.path.join(os.path.dirname(tasks.__file__), "setup.py")) 37 | 38 | 39 | def test_rituals_easy_exports_essential_names(): 40 | for needed in ('Collection', 'task', 'namespace', 'pushd'): 41 | assert needed in easy.__all__, "{} missing from easy exports".format(needed) 42 | assert needed in dir(easy), "{} missing from easy namespace".format(needed) 43 | 44 | 45 | def test_rituals_easy_exports_task_names(): 46 | # Check some sample names mentioned in the docs, that's enough 47 | for needed in ('help', 'clean', 'test_tox', 'check_pylint', 'release_bump'): 48 | assert needed in easy.__all__, "{} missing from easy exports".format(needed) 49 | assert needed in dir(easy), "{} missing from easy namespace".format(needed) 50 | -------------------------------------------------------------------------------- /src/tests/test_filesys.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=wildcard-import, missing-docstring, redefined-outer-name, invalid-name, no-self-use, 3 | """ Tests for `rituals.util.filesys`. 4 | """ 5 | # Copyright ⓒ 2015 Jürgen Hermann 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License version 2 as 9 | # published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | # 20 | # The full LICENSE file and source are available at 21 | # https://github.com/jhermann/rituals 22 | from __future__ import absolute_import, unicode_literals, print_function 23 | 24 | import os 25 | import tempfile 26 | #import unittest 27 | 28 | import pytest 29 | 30 | from rituals.util.filesys import pushd 31 | 32 | 33 | @pytest.fixture(scope='module') 34 | def tmpdir(): 35 | return tempfile.gettempdir() 36 | 37 | 38 | def test_pushd_changes_cwd_to_passed_parameter(tmpdir): 39 | with pushd(tmpdir): 40 | assert os.getcwd() == tmpdir 41 | 42 | 43 | def test_pushd_puts_old_cwd_into_context_var(tmpdir): 44 | cwd = os.getcwd() 45 | with pushd(tmpdir) as saved: 46 | assert saved == cwd 47 | 48 | 49 | def test_pushd_restores_old_cwd(tmpdir): 50 | cwd = os.getcwd() 51 | with pushd(tmpdir): 52 | assert os.getcwd() != cwd 53 | assert os.getcwd() == cwd 54 | -------------------------------------------------------------------------------- /src/tests/test_notify.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=wildcard-import, unused-wildcard-import, missing-docstring 3 | # pylint: disable=redefined-outer-name, invalid-name, no-self-use 4 | """ Tests for `rituals.util.notify`. 5 | """ 6 | # Copyright ⓒ 2015 Jürgen Hermann 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License version 2 as 10 | # published by the Free Software Foundation. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License along 18 | # with this program; if not, write to the Free Software Foundation, Inc., 19 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | # 21 | # The full LICENSE file and source are available at 22 | # https://github.com/jhermann/rituals 23 | from __future__ import absolute_import, unicode_literals, print_function 24 | 25 | import re 26 | #import unittest 27 | 28 | import pytest 29 | 30 | from rituals.util.notify import * 31 | 32 | 33 | SAID = "I've got this terrible pain in all the diodes down my left hand side!" 34 | 35 | 36 | def no_ansi(text): 37 | """Kill any ANSI escape sequences.""" 38 | return re.sub(r"\x1b.+?m", "", text) 39 | 40 | 41 | def test_output_a_banner_to_stderr(capsys): 42 | banner(SAID) 43 | silence, text = capsys.readouterr() 44 | assert repr(no_ansi(text.replace(SAID, '~'))) == repr('~\n') 45 | assert silence == '' 46 | 47 | 48 | def test_output_an_info_message_to_stdout(capsys): 49 | info(SAID) 50 | text, silence = capsys.readouterr() 51 | assert repr(text.replace(SAID, '~')) == repr('~\n') 52 | assert silence == '' 53 | 54 | 55 | def test_output_a_warning_to_stderr(capsys): 56 | warning(SAID) 57 | silence, text = capsys.readouterr() 58 | assert repr(no_ansi(text.replace(SAID, '~'))) == repr('WARNING: ~\n') 59 | assert silence == '' 60 | 61 | 62 | def test_output_an_error_to_stderr(capsys): 63 | error(SAID) 64 | silence, text = capsys.readouterr() 65 | assert repr(no_ansi(text.replace(SAID, '~'))) == repr('ERROR: ~\n') 66 | assert silence == '' 67 | 68 | 69 | def test_output_a_failure_message_to_stderr_and_exit(capsys): 70 | with pytest.raises(SystemExit): 71 | failure(SAID) 72 | silence, text = capsys.readouterr() 73 | assert repr(no_ansi(text.replace(SAID, '~'))) == repr('ERROR: ~\n') 74 | assert silence == '' 75 | -------------------------------------------------------------------------------- /src/tests/test_util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=wildcard-import, missing-docstring, redefined-outer-name, invalid-name, no-self-use, 3 | """ Tests for `rituals.util`. 4 | """ 5 | # Copyright ⓒ 2015 Jürgen Hermann 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License version 2 as 9 | # published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | # 20 | # The full LICENSE file and source are available at 21 | # https://github.com/jhermann/rituals 22 | from __future__ import absolute_import, unicode_literals, print_function 23 | 24 | import os 25 | import unittest 26 | 27 | #import pytest 28 | 29 | from rituals import util 30 | 31 | 32 | class SearchFileUpwardsTest(unittest.TestCase): 33 | 34 | def test_searching_for_a_missing_file_terminates(self): 35 | assert util.search_file_upwards('only_crazies_would_name_a_file_like_this') is None 36 | 37 | def test_we_can_find_our_own_setup_and_tasks_modules(self): 38 | home = os.path.dirname(__file__) 39 | root = util.search_file_upwards('setup.py', base=home) 40 | assert os.path.exists(os.path.join(root, 'tasks.py')) 41 | 42 | def test_file_is_found_when_located_in_the_base_dir(self): 43 | home = os.path.dirname(__file__) 44 | root = util.search_file_upwards('conftest.py', base=home) 45 | assert root == home 46 | 47 | def test_search_starts_in_cwd_by_default(self): 48 | home = os.path.dirname(__file__) 49 | cwd = os.getcwd() 50 | try: 51 | os.chdir(home) 52 | root = util.search_file_upwards('conftest.py') 53 | assert root == home 54 | finally: 55 | os.chdir(cwd) 56 | -------------------------------------------------------------------------------- /src/tests/test_which_py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable= 3 | """Test suite for which.py.""" 4 | # Imported v1.1.0 from https://code.google.com/p/which/ (2015-03-15, ZIP download) 5 | # Originally developed by Trent Mick 6 | # 7 | # Copyright (c) 2002-2003 ActiveState Corp. 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a 10 | # copy of this software and associated documentation files (the 11 | # "Software"), to deal in the Software without restriction, including 12 | # without limitation the rights to use, copy, modify, merge, publish, 13 | # distribute, sublicense, and/or sell copies of the Software, and to 14 | # permit persons to whom the Software is furnished to do so, subject to 15 | # the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included 18 | # in all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 21 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 23 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 24 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 25 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 26 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 27 | from __future__ import absolute_import, unicode_literals, print_function 28 | 29 | import sys 30 | import os 31 | import re 32 | import tempfile 33 | import unittest 34 | 35 | import testsupport 36 | 37 | #XXX:TODO 38 | # - def test_registry_success(self): ...App Paths setting 39 | # - def test_registry_noexist(self): 40 | # - test all the other options 41 | # - test on linux 42 | # - test the module API 43 | 44 | class WhichTestCase(unittest.TestCase): 45 | def setUp(self): 46 | """Create a temp directory with a couple test "commands". 47 | The temp dir can be added to the PATH, etc, for testing purposes. 48 | """ 49 | # Find the which.py to call. 50 | whichPy = os.path.join(os.path.dirname(__file__), 51 | os.pardir, "which.py") 52 | self.which = sys.executable + " " + whichPy 53 | 54 | # Setup the test environment. 55 | self.tmpdir = tempfile.mktemp() 56 | os.makedirs(self.tmpdir) 57 | if sys.platform.startswith("win"): 58 | self.testapps = ['whichtestapp1.exe', 59 | 'whichtestapp2.exe', 60 | 'whichtestapp3.wta'] 61 | else: 62 | self.testapps = ['whichtestapp1', 'whichtestapp2'] 63 | for app in self.testapps: 64 | path = os.path.join(self.tmpdir, app) 65 | open(path, 'wb').write('\n') 66 | os.chmod(path, 0755) 67 | 68 | def tearDown(self): 69 | testsupport.rmtree(self.tmpdir) 70 | 71 | def test_opt_h(self): 72 | output, error, retval = testsupport.run(self.which+' --h') 73 | token = 'Usage:' 74 | self.failUnless(output.find(token) != -1, 75 | "'%s' was not found in 'which -h' output: '%s' "\ 76 | % (token, output)) 77 | self.failUnless(retval == 0, 78 | "'which -h' did not return 0: retval=%d" % retval) 79 | 80 | def test_opt_help(self): 81 | output, error, retval = testsupport.run(self.which+' --help') 82 | token = 'Usage:' 83 | self.failUnless(output.find(token) != -1, 84 | "'%s' was not found in 'which --help' output: '%s' "\ 85 | % (token, output)) 86 | self.failUnless(retval == 0, 87 | "'which --help' did not return 0: retval=%d" % retval) 88 | 89 | def test_opt_version(self): 90 | output, error, retval = testsupport.run(self.which+' --version') 91 | versionRe = re.compile("^which \d+\.\d+\.\d+$") 92 | versionMatch = versionRe.search(output.strip()) 93 | self.failUnless(versionMatch, 94 | "Version, '%s', from 'which --version' does not "\ 95 | "match pattern, '%s'."\ 96 | % (output.strip(), versionRe.pattern)) 97 | self.failUnless(retval == 0, 98 | "'which --version' did not return 0: retval=%d"\ 99 | % retval) 100 | 101 | def test_no_args(self): 102 | output, error, retval = testsupport.run(self.which) 103 | self.failUnless(retval == -1, 104 | "'which' with no args should return -1: retval=%d"\ 105 | % retval) 106 | 107 | def test_one_failure(self): 108 | output, error, retval = testsupport.run( 109 | self.which+' whichtestapp1') 110 | self.failUnless(retval == 1, 111 | "One failure did not return 1: retval=%d" % retval) 112 | 113 | def test_two_failures(self): 114 | output, error, retval = testsupport.run( 115 | self.which+' whichtestapp1 whichtestapp2') 116 | self.failUnless(retval == 2, 117 | "Two failures did not return 2: retval=%d" % retval) 118 | 119 | def _match(self, path1, path2): 120 | #print("_match: %r =?= %r" % (path1, path2)) 121 | if sys.platform.startswith('win'): 122 | path1 = os.path.normpath(os.path.normcase(path1)) 123 | path2 = os.path.normpath(os.path.normcase(path2)) 124 | path1 = os.path.splitext(path1)[0] 125 | path2 = os.path.splitext(path2)[0] 126 | return path1 == path2 127 | else: 128 | return os.path.samefile(path1, path2) 129 | 130 | def test_one_success(self): 131 | os.environ["PATH"] += os.pathsep + self.tmpdir 132 | output, error, retval = testsupport.run(self.which+' -q whichtestapp1') 133 | expectedOutput = os.path.join(self.tmpdir, "whichtestapp1") 134 | self.failUnless(self._match(output.strip(), expectedOutput), 135 | "Output, %r, and expected output, %r, do not match."\ 136 | % (output.strip(), expectedOutput)) 137 | self.failUnless(retval == 0, 138 | "'which ...' should have returned 0: retval=%d" % retval) 139 | 140 | def test_two_successes(self): 141 | os.environ["PATH"] += os.pathsep + self.tmpdir 142 | apps = ['whichtestapp1', 'whichtestapp2'] 143 | output, error, retval = testsupport.run( 144 | self.which + ' -q ' + ' '.join(apps)) 145 | lines = output.strip().split("\n") 146 | for app, line in zip(apps, lines): 147 | expected = os.path.join(self.tmpdir, app) 148 | self.failUnless(self._match(line, expected), 149 | "Output, %r, and expected output, %r, do not match."\ 150 | % (line, expected)) 151 | self.failUnless(retval == 0, 152 | "'which ...' should have returned 0: retval=%d" % retval) 153 | 154 | if sys.platform.startswith("win"): 155 | def test_PATHEXT_failure(self): 156 | os.environ["PATH"] += os.pathsep + self.tmpdir 157 | output, error, retval = testsupport.run(self.which+' whichtestapp3') 158 | self.failUnless(retval == 1, 159 | "'which ...' should have returned 1: retval=%d" % retval) 160 | 161 | def test_PATHEXT_success(self): 162 | os.environ["PATH"] += os.pathsep + self.tmpdir 163 | os.environ["PATHEXT"] += os.pathsep + '.wta' 164 | output, error, retval = testsupport.run(self.which+' whichtestapp3') 165 | expectedOutput = os.path.join(self.tmpdir, "whichtestapp3") 166 | self.failUnless(self._match(output.strip(), expectedOutput), 167 | "Output, %r, and expected output, %r, do not match."\ 168 | % (output.strip(), expectedOutput)) 169 | self.failUnless(retval == 0, 170 | "'which ...' should have returned 0: retval=%d" % retval) 171 | 172 | def test_exts(self): 173 | os.environ["PATH"] += os.pathsep + self.tmpdir 174 | output, error, retval = testsupport.run(self.which+' -e .wta whichtestapp3') 175 | expectedOutput = os.path.join(self.tmpdir, "whichtestapp3") 176 | self.failUnless(self._match(output.strip(), expectedOutput), 177 | "Output, %r, and expected output, %r, do not match."\ 178 | % (output.strip(), expectedOutput)) 179 | self.failUnless(retval == 0, 180 | "'which ...' should have returned 0: retval=%d" % retval) 181 | 182 | 183 | 184 | def suite(): 185 | """Return a unittest.TestSuite to be used by test.py.""" 186 | return unittest.makeSuite(WhichTestCase) 187 | 188 | if __name__ == "__main__": 189 | unittest.main() 190 | -------------------------------------------------------------------------------- /task-requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Task requirements 3 | # 4 | 5 | # devpi / docs 6 | #requests==2.9.1 # already in requirements! 7 | 8 | # check 9 | pylint==2.9.6 10 | 11 | # test 12 | pytest==4.6.4 ; python_version < "3.5" 13 | pytest==5.3.5 ; python_version >= "3.5" 14 | 15 | pytest-spec==3.2.0 16 | pytest-cov==2.12.1 17 | py>=1.5.0 18 | pluggy>=0.12 19 | coveralls==3.2.0 20 | 21 | # docs 22 | sphinx==1.8.5 ; python_version < "3.5" 23 | sphinx==3.5.4 ; python_version >= "3.5" 24 | sphinx-autobuild==2021.3.14 25 | 26 | # release 27 | shiv==0.5.2 28 | pex==2.1.44 29 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=wildcard-import, unused-wildcard-import, unused-import 3 | """ Project automation for Invoke. 4 | """ 5 | 6 | from invoke.tasks import call 7 | from rituals.easy import * 8 | 9 | # Example for selective import 10 | #from rituals.acts.documentation import namespace as _ 11 | #namespace.add_collection(_) 12 | 13 | 14 | @task(pre=[ 15 | clean, build, test_pytest, check_pylint, # pylint: disable=undefined-variable 16 | #call(clean, all=True), 17 | #call(build, docs=True), 18 | #call(test), 19 | #call(check, reports=True), 20 | ]) # pylint: disable=invalid-name 21 | def ci(_): 22 | """Perform continuous integration tasks.""" 23 | 24 | namespace.add_task(ci) 25 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox configuration, for details see 2 | # 3 | # http://tox.testrun.org/ 4 | # 5 | # $ . .env "--yes" "--develop" 6 | # $ tox 7 | 8 | [tox] 9 | envlist = py36, py38, flake8 10 | 11 | 12 | [testenv] 13 | deps = 14 | -r./task-requirements.txt 15 | -r./requirements.txt 16 | whitelist_externals = 17 | cp 18 | ln 19 | commands_pre = 20 | cp {toxinidir}/setup.py {toxinidir}/tasks.py {envsitepackagesdir} 21 | ln -nfs {toxinidir}/src {toxinidir}/project.d {envsitepackagesdir} 22 | commands = 23 | py.test -c {toxinidir}/setup.cfg --color=yes --cov=rituals \ 24 | --cov-config=project.d/coverage.cfg --cov-report=term --cov-report=html --cov-report=xml \ 25 | {posargs} 26 | 27 | 28 | [testenv:flake8] 29 | deps = 30 | flake8==2.3.0 31 | pep8==1.6.2 32 | 33 | ; for now just informational 34 | commands = 35 | flake8 --count --statistics --exit-zero src/rituals 36 | --------------------------------------------------------------------------------