├── .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 |  … and again and again.
6 |
7 | [](https://groups.google.com/forum/#!forum/rituals-dev)
8 | [](https://travis-ci.org/jhermann/rituals)
9 | [](https://coveralls.io/r/jhermann/rituals)
10 | [](https://github.com/jhermann/rituals/issues)
11 | [](https://github.com/jhermann/rituals/blob/master/LICENSE)
12 | [](https://pypi.python.org/pypi/rituals/)
13 | [](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 | [](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 | 
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 |
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="""
'.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 |
--------------------------------------------------------------------------------