invoke-tasklib 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- invoke_tasklib-0.0.1/.gitignore +218 -0
- invoke_tasklib-0.0.1/LICENSE +28 -0
- invoke_tasklib-0.0.1/PKG-INFO +163 -0
- invoke_tasklib-0.0.1/README.md +135 -0
- invoke_tasklib-0.0.1/invoke_tasklib/__init__.py +37 -0
- invoke_tasklib-0.0.1/invoke_tasklib/config.py +109 -0
- invoke_tasklib-0.0.1/invoke_tasklib/doc.py +59 -0
- invoke_tasklib-0.0.1/invoke_tasklib/env.py +95 -0
- invoke_tasklib-0.0.1/invoke_tasklib/format.py +89 -0
- invoke_tasklib-0.0.1/invoke_tasklib/lint.py +21 -0
- invoke_tasklib-0.0.1/invoke_tasklib/py.typed +0 -0
- invoke_tasklib-0.0.1/invoke_tasklib/release.py +47 -0
- invoke_tasklib-0.0.1/invoke_tasklib/test.py +124 -0
- invoke_tasklib-0.0.1/invoke_tasklib/types.py +25 -0
- invoke_tasklib-0.0.1/pyproject.toml +80 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
# Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
# poetry.lock
|
|
109
|
+
# poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
# pdm.lock
|
|
116
|
+
# pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
# pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# Redis
|
|
135
|
+
*.rdb
|
|
136
|
+
*.aof
|
|
137
|
+
*.pid
|
|
138
|
+
|
|
139
|
+
# RabbitMQ
|
|
140
|
+
mnesia/
|
|
141
|
+
rabbitmq/
|
|
142
|
+
rabbitmq-data/
|
|
143
|
+
|
|
144
|
+
# ActiveMQ
|
|
145
|
+
activemq-data/
|
|
146
|
+
|
|
147
|
+
# SageMath parsed files
|
|
148
|
+
*.sage.py
|
|
149
|
+
|
|
150
|
+
# Environments
|
|
151
|
+
.env
|
|
152
|
+
.envrc
|
|
153
|
+
.venv
|
|
154
|
+
env/
|
|
155
|
+
venv/
|
|
156
|
+
ENV/
|
|
157
|
+
env.bak/
|
|
158
|
+
venv.bak/
|
|
159
|
+
|
|
160
|
+
# Spyder project settings
|
|
161
|
+
.spyderproject
|
|
162
|
+
.spyproject
|
|
163
|
+
|
|
164
|
+
# Rope project settings
|
|
165
|
+
.ropeproject
|
|
166
|
+
|
|
167
|
+
# mkdocs documentation
|
|
168
|
+
/site
|
|
169
|
+
|
|
170
|
+
# mypy
|
|
171
|
+
.mypy_cache/
|
|
172
|
+
.dmypy.json
|
|
173
|
+
dmypy.json
|
|
174
|
+
|
|
175
|
+
# Pyre type checker
|
|
176
|
+
.pyre/
|
|
177
|
+
|
|
178
|
+
# pytype static type analyzer
|
|
179
|
+
.pytype/
|
|
180
|
+
|
|
181
|
+
# Cython debug symbols
|
|
182
|
+
cython_debug/
|
|
183
|
+
|
|
184
|
+
# PyCharm
|
|
185
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
186
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
188
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
189
|
+
# .idea/
|
|
190
|
+
|
|
191
|
+
# Abstra
|
|
192
|
+
# Abstra is an AI-powered process automation framework.
|
|
193
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
194
|
+
# Learn more at https://abstra.io/docs
|
|
195
|
+
.abstra/
|
|
196
|
+
|
|
197
|
+
# Visual Studio Code
|
|
198
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
199
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
200
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
201
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
202
|
+
# .vscode/
|
|
203
|
+
# Temporary file for partial code execution
|
|
204
|
+
tempCodeRunnerFile.py
|
|
205
|
+
|
|
206
|
+
# Ruff stuff:
|
|
207
|
+
.ruff_cache/
|
|
208
|
+
|
|
209
|
+
# PyPI configuration file
|
|
210
|
+
.pypirc
|
|
211
|
+
|
|
212
|
+
# Marimo
|
|
213
|
+
marimo/_static/
|
|
214
|
+
marimo/_lsp/
|
|
215
|
+
__marimo__/
|
|
216
|
+
|
|
217
|
+
# Streamlit
|
|
218
|
+
.streamlit/secrets.toml
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Thibaut Durand
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: invoke-tasklib
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Reusable Invoke tasks shared across Python projects.
|
|
5
|
+
Project-URL: Homepage, https://github.com/durandtibo/invoke-tasklib
|
|
6
|
+
Project-URL: Repository, https://github.com/durandtibo/invoke-tasklib
|
|
7
|
+
Project-URL: Documentation, https://durandtibo.github.io/invoke-tasklib/
|
|
8
|
+
Project-URL: Changelog, https://github.com/durandtibo/invoke-tasklib/releases
|
|
9
|
+
Project-URL: Issues, https://github.com/durandtibo/invoke-tasklib/issues
|
|
10
|
+
Author-email: Thibaut Durand <durand.tibo+gh@gmail.com>
|
|
11
|
+
License-Expression: BSD-3-Clause
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: automation,devtools,invoke,tasks
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
17
|
+
Classifier: Operating System :: MacOS
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: invoke>=3.0
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# invoke-tasklib
|
|
30
|
+
|
|
31
|
+
Reusable [Invoke](https://www.pyinvoke.org/) tasks shared across Python
|
|
32
|
+
projects.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
Add `invoke-tasklib` as a dev dependency.
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
In your project's `tasks.py`:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from invoke_tasklib import ns
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Set the required config in `invoke.yaml` at the project root:
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
tasklib:
|
|
50
|
+
package:
|
|
51
|
+
name: my_package
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Then list the available tasks:
|
|
55
|
+
|
|
56
|
+
```shell
|
|
57
|
+
invoke --list
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Tasks
|
|
61
|
+
|
|
62
|
+
Tasks are organized into namespaces: `format.*`, `lint.*`, `types.*`,
|
|
63
|
+
`test.*`, `env.*`, `release.*`, `doc.*`.
|
|
64
|
+
|
|
65
|
+
### `format.*` and `lint.*`
|
|
66
|
+
|
|
67
|
+
Tasks that check or verify something (never modify files, exit non-zero on
|
|
68
|
+
violations) are named `check_<target>`. Tasks that modify files in place are
|
|
69
|
+
named `fix_<target>`. Both share the same `<target>` (e.g. `python`, `shell`,
|
|
70
|
+
`docstrings`) so the read-only/mutating counterpart of a task is easy to find:
|
|
71
|
+
|
|
72
|
+
| Task | Behavior |
|
|
73
|
+
| ------------------------- | --------------------------------------------------------- |
|
|
74
|
+
| `format.check-python` | Checks Python formatting with ruff (read-only) |
|
|
75
|
+
| `format.check-docstrings` | Checks docstring formatting with docformatter (read-only) |
|
|
76
|
+
| `format.check-shell` | Checks shell scripts with shellcheck (read-only) |
|
|
77
|
+
| `format.fix-python` | Formats Python code with ruff (in place) |
|
|
78
|
+
| `format.fix-docstrings` | Formats docstrings with docformatter (in place) |
|
|
79
|
+
| `format.fix-shell` | Formats shell scripts with shfmt (in place) |
|
|
80
|
+
| `lint.check-lint` | Checks linting with ruff (read-only) |
|
|
81
|
+
|
|
82
|
+
When adding a new task to these namespaces, follow this convention: pick
|
|
83
|
+
`check_` or `fix_` based on whether the task mutates files, and use a
|
|
84
|
+
`<target>` name that matches its read-only/mutating counterpart if one
|
|
85
|
+
exists.
|
|
86
|
+
|
|
87
|
+
### `types.*`
|
|
88
|
+
|
|
89
|
+
| Task | Behavior |
|
|
90
|
+
| ------------- | ------------------------------------------ |
|
|
91
|
+
| `types.check` | Checks type hints with pyright (read-only) |
|
|
92
|
+
|
|
93
|
+
### `test.*`
|
|
94
|
+
|
|
95
|
+
| Task | Behavior |
|
|
96
|
+
| ------------------ | -------------------------------------------------- |
|
|
97
|
+
| `test.doctest` | Runs doctests on source code |
|
|
98
|
+
| `test.unit` | Runs unit tests |
|
|
99
|
+
| `test.integration` | Runs integration tests |
|
|
100
|
+
| `test.functional` | Runs functional tests |
|
|
101
|
+
| `test.all` | Runs all tests (unit, integration, and functional) |
|
|
102
|
+
| `test.benchmark` | Runs performance benchmarks |
|
|
103
|
+
|
|
104
|
+
### `env.*`
|
|
105
|
+
|
|
106
|
+
| Task | Behavior |
|
|
107
|
+
| ----------------------------- | -------------------------------------------------------- |
|
|
108
|
+
| `env.create-venv` | Creates a virtual environment and installs invoke |
|
|
109
|
+
| `env.install` | Installs project dependencies and the package (editable) |
|
|
110
|
+
| `env.update` | Updates dependencies and pre-commit hooks |
|
|
111
|
+
| `env.show-installed-packages` | Shows the installed packages |
|
|
112
|
+
| `env.show-python-config` | Shows the Python configuration |
|
|
113
|
+
|
|
114
|
+
### `release.*`
|
|
115
|
+
|
|
116
|
+
| Task | Behavior |
|
|
117
|
+
| --------------- | ------------------------------------------------------------------------------------------- |
|
|
118
|
+
| `release.build` | Builds the package and verifies installation (`--check` also validates metadata with twine) |
|
|
119
|
+
| `release.pypi` | Builds and publishes the package to PyPI |
|
|
120
|
+
|
|
121
|
+
### `doc.*`
|
|
122
|
+
|
|
123
|
+
| Task | Behavior |
|
|
124
|
+
| -------------------- | ------------------------------------- |
|
|
125
|
+
| `doc.publish-dev` | Publishes development (unstable) docs |
|
|
126
|
+
| `doc.publish-latest` | Publishes latest (stable) docs |
|
|
127
|
+
|
|
128
|
+
## Config
|
|
129
|
+
|
|
130
|
+
Only `tasklib.package.name` is required. Everything else has a default
|
|
131
|
+
derived from it. Full schema:
|
|
132
|
+
|
|
133
|
+
```yaml
|
|
134
|
+
tasklib:
|
|
135
|
+
package:
|
|
136
|
+
name: my_package # required
|
|
137
|
+
python_version: "3.14" # used by env.create-venv
|
|
138
|
+
paths:
|
|
139
|
+
src: src/my_package # default: src/<package.name>
|
|
140
|
+
tests: tests
|
|
141
|
+
unit_tests: tests/unit # default: <tests>/unit
|
|
142
|
+
integration_tests: tests/integration # default: <tests>/integration
|
|
143
|
+
functional_tests: tests/functional # default: <tests>/functional
|
|
144
|
+
benchmarks: tests/benchmarks # default: <tests>/benchmarks
|
|
145
|
+
docs_config: docs/mkdocs.yml
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Composing a custom subset of tasks
|
|
149
|
+
|
|
150
|
+
If a project needs a different set of tasks, or a one-off task alongside the
|
|
151
|
+
shared ones, import individual task modules instead of the pre-built `ns`:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from invoke import Collection
|
|
155
|
+
from invoke_tasklib import lint, test
|
|
156
|
+
|
|
157
|
+
from . import my_custom_task
|
|
158
|
+
|
|
159
|
+
ns = Collection(lint, test, my_custom_task)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Prefer adding a config knob to a shared task over forking it; reserve custom
|
|
163
|
+
composition for things that are genuinely one-off to a single project.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# invoke-tasklib
|
|
2
|
+
|
|
3
|
+
Reusable [Invoke](https://www.pyinvoke.org/) tasks shared across Python
|
|
4
|
+
projects.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
Add `invoke-tasklib` as a dev dependency.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
In your project's `tasks.py`:
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from invoke_tasklib import ns
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Set the required config in `invoke.yaml` at the project root:
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
tasklib:
|
|
22
|
+
package:
|
|
23
|
+
name: my_package
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Then list the available tasks:
|
|
27
|
+
|
|
28
|
+
```shell
|
|
29
|
+
invoke --list
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Tasks
|
|
33
|
+
|
|
34
|
+
Tasks are organized into namespaces: `format.*`, `lint.*`, `types.*`,
|
|
35
|
+
`test.*`, `env.*`, `release.*`, `doc.*`.
|
|
36
|
+
|
|
37
|
+
### `format.*` and `lint.*`
|
|
38
|
+
|
|
39
|
+
Tasks that check or verify something (never modify files, exit non-zero on
|
|
40
|
+
violations) are named `check_<target>`. Tasks that modify files in place are
|
|
41
|
+
named `fix_<target>`. Both share the same `<target>` (e.g. `python`, `shell`,
|
|
42
|
+
`docstrings`) so the read-only/mutating counterpart of a task is easy to find:
|
|
43
|
+
|
|
44
|
+
| Task | Behavior |
|
|
45
|
+
| ------------------------- | --------------------------------------------------------- |
|
|
46
|
+
| `format.check-python` | Checks Python formatting with ruff (read-only) |
|
|
47
|
+
| `format.check-docstrings` | Checks docstring formatting with docformatter (read-only) |
|
|
48
|
+
| `format.check-shell` | Checks shell scripts with shellcheck (read-only) |
|
|
49
|
+
| `format.fix-python` | Formats Python code with ruff (in place) |
|
|
50
|
+
| `format.fix-docstrings` | Formats docstrings with docformatter (in place) |
|
|
51
|
+
| `format.fix-shell` | Formats shell scripts with shfmt (in place) |
|
|
52
|
+
| `lint.check-lint` | Checks linting with ruff (read-only) |
|
|
53
|
+
|
|
54
|
+
When adding a new task to these namespaces, follow this convention: pick
|
|
55
|
+
`check_` or `fix_` based on whether the task mutates files, and use a
|
|
56
|
+
`<target>` name that matches its read-only/mutating counterpart if one
|
|
57
|
+
exists.
|
|
58
|
+
|
|
59
|
+
### `types.*`
|
|
60
|
+
|
|
61
|
+
| Task | Behavior |
|
|
62
|
+
| ------------- | ------------------------------------------ |
|
|
63
|
+
| `types.check` | Checks type hints with pyright (read-only) |
|
|
64
|
+
|
|
65
|
+
### `test.*`
|
|
66
|
+
|
|
67
|
+
| Task | Behavior |
|
|
68
|
+
| ------------------ | -------------------------------------------------- |
|
|
69
|
+
| `test.doctest` | Runs doctests on source code |
|
|
70
|
+
| `test.unit` | Runs unit tests |
|
|
71
|
+
| `test.integration` | Runs integration tests |
|
|
72
|
+
| `test.functional` | Runs functional tests |
|
|
73
|
+
| `test.all` | Runs all tests (unit, integration, and functional) |
|
|
74
|
+
| `test.benchmark` | Runs performance benchmarks |
|
|
75
|
+
|
|
76
|
+
### `env.*`
|
|
77
|
+
|
|
78
|
+
| Task | Behavior |
|
|
79
|
+
| ----------------------------- | -------------------------------------------------------- |
|
|
80
|
+
| `env.create-venv` | Creates a virtual environment and installs invoke |
|
|
81
|
+
| `env.install` | Installs project dependencies and the package (editable) |
|
|
82
|
+
| `env.update` | Updates dependencies and pre-commit hooks |
|
|
83
|
+
| `env.show-installed-packages` | Shows the installed packages |
|
|
84
|
+
| `env.show-python-config` | Shows the Python configuration |
|
|
85
|
+
|
|
86
|
+
### `release.*`
|
|
87
|
+
|
|
88
|
+
| Task | Behavior |
|
|
89
|
+
| --------------- | ------------------------------------------------------------------------------------------- |
|
|
90
|
+
| `release.build` | Builds the package and verifies installation (`--check` also validates metadata with twine) |
|
|
91
|
+
| `release.pypi` | Builds and publishes the package to PyPI |
|
|
92
|
+
|
|
93
|
+
### `doc.*`
|
|
94
|
+
|
|
95
|
+
| Task | Behavior |
|
|
96
|
+
| -------------------- | ------------------------------------- |
|
|
97
|
+
| `doc.publish-dev` | Publishes development (unstable) docs |
|
|
98
|
+
| `doc.publish-latest` | Publishes latest (stable) docs |
|
|
99
|
+
|
|
100
|
+
## Config
|
|
101
|
+
|
|
102
|
+
Only `tasklib.package.name` is required. Everything else has a default
|
|
103
|
+
derived from it. Full schema:
|
|
104
|
+
|
|
105
|
+
```yaml
|
|
106
|
+
tasklib:
|
|
107
|
+
package:
|
|
108
|
+
name: my_package # required
|
|
109
|
+
python_version: "3.14" # used by env.create-venv
|
|
110
|
+
paths:
|
|
111
|
+
src: src/my_package # default: src/<package.name>
|
|
112
|
+
tests: tests
|
|
113
|
+
unit_tests: tests/unit # default: <tests>/unit
|
|
114
|
+
integration_tests: tests/integration # default: <tests>/integration
|
|
115
|
+
functional_tests: tests/functional # default: <tests>/functional
|
|
116
|
+
benchmarks: tests/benchmarks # default: <tests>/benchmarks
|
|
117
|
+
docs_config: docs/mkdocs.yml
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Composing a custom subset of tasks
|
|
121
|
+
|
|
122
|
+
If a project needs a different set of tasks, or a one-off task alongside the
|
|
123
|
+
shared ones, import individual task modules instead of the pre-built `ns`:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from invoke import Collection
|
|
127
|
+
from invoke_tasklib import lint, test
|
|
128
|
+
|
|
129
|
+
from . import my_custom_task
|
|
130
|
+
|
|
131
|
+
ns = Collection(lint, test, my_custom_task)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Prefer adding a config knob to a shared task over forking it; reserve custom
|
|
135
|
+
composition for things that are genuinely one-off to a single project.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
r"""Reusable Invoke tasks shared across Python projects.
|
|
2
|
+
|
|
3
|
+
Typical usage in a consuming project's ``tasks.py``::
|
|
4
|
+
|
|
5
|
+
from invoke_tasklib import ns
|
|
6
|
+
|
|
7
|
+
and an ``invoke.yaml`` at the project root::
|
|
8
|
+
|
|
9
|
+
tasklib:
|
|
10
|
+
package:
|
|
11
|
+
name: my_package
|
|
12
|
+
|
|
13
|
+
To compose a custom subset of tasks instead of using the default
|
|
14
|
+
``ns``, import individual task modules::
|
|
15
|
+
|
|
16
|
+
from invoke import Collection
|
|
17
|
+
from invoke_tasklib import lint, test
|
|
18
|
+
|
|
19
|
+
ns = Collection(lint, test)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from invoke.collection import Collection
|
|
25
|
+
|
|
26
|
+
from invoke_tasklib import doc, env, format, lint, release, test, types
|
|
27
|
+
|
|
28
|
+
__all__ = ["doc", "env", "format", "lint", "ns", "release", "test", "types"]
|
|
29
|
+
|
|
30
|
+
ns: Collection = Collection()
|
|
31
|
+
ns.add_collection(Collection.from_module(format), name="format")
|
|
32
|
+
ns.add_collection(Collection.from_module(lint), name="lint")
|
|
33
|
+
ns.add_collection(Collection.from_module(types), name="types")
|
|
34
|
+
ns.add_collection(Collection.from_module(test), name="test")
|
|
35
|
+
ns.add_collection(Collection.from_module(env), name="env")
|
|
36
|
+
ns.add_collection(Collection.from_module(release), name="release")
|
|
37
|
+
ns.add_collection(Collection.from_module(doc), name="doc")
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
r"""Config resolution for invoke-tasklib tasks.
|
|
2
|
+
|
|
3
|
+
Consuming projects set project-specific values under a ``tasklib`` key in
|
|
4
|
+
their ``invoke.yaml``, e.g.::
|
|
5
|
+
|
|
6
|
+
tasklib:
|
|
7
|
+
package:
|
|
8
|
+
name: coola
|
|
9
|
+
paths:
|
|
10
|
+
docs_config: docs/mkdocs.yml
|
|
11
|
+
|
|
12
|
+
Only ``package.name`` is required; everything else has a default derived
|
|
13
|
+
from it.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import TYPE_CHECKING, TypedDict
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from invoke.context import Context
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PackageConfig(TypedDict):
|
|
25
|
+
r"""Resolved ``package`` config section."""
|
|
26
|
+
|
|
27
|
+
name: str
|
|
28
|
+
python_version: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PathsConfig(TypedDict):
|
|
32
|
+
r"""Resolved ``paths`` config section."""
|
|
33
|
+
|
|
34
|
+
src: str
|
|
35
|
+
tests: str
|
|
36
|
+
unit_tests: str
|
|
37
|
+
integration_tests: str
|
|
38
|
+
functional_tests: str
|
|
39
|
+
benchmarks: str
|
|
40
|
+
docs_config: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TasklibConfig(TypedDict):
|
|
44
|
+
r"""Resolved tasklib config."""
|
|
45
|
+
|
|
46
|
+
package: PackageConfig
|
|
47
|
+
paths: PathsConfig
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
DEFAULT_PACKAGE: dict[str, str | None] = {
|
|
51
|
+
"name": None,
|
|
52
|
+
"python_version": "3.14",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
DEFAULT_PATHS: dict[str, str | None] = {
|
|
56
|
+
"src": None,
|
|
57
|
+
"tests": "tests",
|
|
58
|
+
"unit_tests": None,
|
|
59
|
+
"integration_tests": None,
|
|
60
|
+
"functional_tests": None,
|
|
61
|
+
"benchmarks": None,
|
|
62
|
+
"docs_config": "docs/mkdocs.yml",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_config(c: Context) -> TasklibConfig:
|
|
67
|
+
r"""Return the effective tasklib config, merging user overrides from
|
|
68
|
+
``invoke.yaml`` (under the ``tasklib`` key) on top of the defaults.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
c: The invoke context.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
A dict with resolved ``package`` and ``paths`` sections.
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
ValueError: If ``tasklib.package.name`` is not set.
|
|
78
|
+
"""
|
|
79
|
+
user = dict(c.config.get("tasklib", {}))
|
|
80
|
+
package = {**DEFAULT_PACKAGE, **user.get("package", {})}
|
|
81
|
+
paths = {**DEFAULT_PATHS, **user.get("paths", {})}
|
|
82
|
+
|
|
83
|
+
if not package["name"]:
|
|
84
|
+
msg = "'tasklib.package.name' must be set in invoke.yaml"
|
|
85
|
+
raise ValueError(msg)
|
|
86
|
+
|
|
87
|
+
if not paths["src"]:
|
|
88
|
+
paths["src"] = f"src/{package['name']}"
|
|
89
|
+
if not paths["unit_tests"]:
|
|
90
|
+
paths["unit_tests"] = f"{paths['tests']}/unit"
|
|
91
|
+
if not paths["integration_tests"]:
|
|
92
|
+
paths["integration_tests"] = f"{paths['tests']}/integration"
|
|
93
|
+
if not paths["functional_tests"]:
|
|
94
|
+
paths["functional_tests"] = f"{paths['tests']}/functional"
|
|
95
|
+
if not paths["benchmarks"]:
|
|
96
|
+
paths["benchmarks"] = f"{paths['tests']}/benchmarks"
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
"package": PackageConfig(name=package["name"], python_version=package["python_version"]),
|
|
100
|
+
"paths": PathsConfig(
|
|
101
|
+
src=paths["src"],
|
|
102
|
+
tests=paths["tests"],
|
|
103
|
+
unit_tests=paths["unit_tests"],
|
|
104
|
+
integration_tests=paths["integration_tests"],
|
|
105
|
+
functional_tests=paths["functional_tests"],
|
|
106
|
+
benchmarks=paths["benchmarks"],
|
|
107
|
+
docs_config=paths["docs_config"],
|
|
108
|
+
),
|
|
109
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
r"""Documentation publishing tasks (versioned docs via mike)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from invoke.tasks import task
|
|
9
|
+
|
|
10
|
+
from invoke_tasklib.config import get_config
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from invoke.context import Context
|
|
14
|
+
|
|
15
|
+
logger: logging.Logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@task
|
|
19
|
+
def publish_dev(c: Context) -> None:
|
|
20
|
+
r"""Publish development (e.g. unstable) docs."""
|
|
21
|
+
cfg = get_config(c)
|
|
22
|
+
docs_config = cfg["paths"]["docs_config"]
|
|
23
|
+
logger.info("๐ Publishing development documentation...")
|
|
24
|
+
logger.info("๐๏ธ Deleting previous 'main' version if it exists...")
|
|
25
|
+
c.run(f"mike delete --config-file {docs_config} main", pty=True, warn=True)
|
|
26
|
+
logger.info("๐ Deploying 'main' and 'dev' aliases...")
|
|
27
|
+
c.run(f"mike deploy --config-file {docs_config} --push --update-aliases main dev", pty=True)
|
|
28
|
+
logger.info("โ
Development documentation published")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@task
|
|
32
|
+
def publish_latest(c: Context) -> None:
|
|
33
|
+
r"""Publish latest (e.g. stable) docs.
|
|
34
|
+
|
|
35
|
+
Requires the ``feu`` and ``packaging`` packages to determine the
|
|
36
|
+
latest version tag.
|
|
37
|
+
"""
|
|
38
|
+
from feu.local_git import get_last_version_tag_name
|
|
39
|
+
from packaging.version import Version
|
|
40
|
+
|
|
41
|
+
cfg = get_config(c)
|
|
42
|
+
docs_config = cfg["paths"]["docs_config"]
|
|
43
|
+
logger.info("๐ Publishing latest documentation...")
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
version = Version(get_last_version_tag_name())
|
|
47
|
+
tag = f"{version.major}.{version.minor}"
|
|
48
|
+
logger.info(f"๐ Using version tag: {tag}")
|
|
49
|
+
except RuntimeError:
|
|
50
|
+
tag = "0.0"
|
|
51
|
+
logger.warning("โ ๏ธ No version tag found, using default: 0.0")
|
|
52
|
+
|
|
53
|
+
logger.info(f"๐๏ธ Deleting previous '{tag}' version if it exists...")
|
|
54
|
+
c.run(f"mike delete --config-file {docs_config} {tag}", pty=True, warn=True)
|
|
55
|
+
logger.info(f"๐ Deploying '{tag}' and 'latest' aliases...")
|
|
56
|
+
c.run(f"mike deploy --config-file {docs_config} --push --update-aliases {tag} latest", pty=True)
|
|
57
|
+
logger.info("๐ฏ Setting 'latest' as default...")
|
|
58
|
+
c.run(f"mike set-default --config-file {docs_config} --push --allow-empty latest", pty=True)
|
|
59
|
+
logger.info("โ
Latest documentation published")
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
r"""Environment and dependency management tasks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from invoke.tasks import task
|
|
9
|
+
|
|
10
|
+
from invoke_tasklib.config import get_config
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from invoke.context import Context
|
|
14
|
+
|
|
15
|
+
logger: logging.Logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@task
|
|
19
|
+
def create_venv(c: Context) -> None:
|
|
20
|
+
r"""Create a virtual environment and install invoke.
|
|
21
|
+
|
|
22
|
+
Note:
|
|
23
|
+
The virtual environment will be created in the .venv directory and any
|
|
24
|
+
existing environment will be cleared.
|
|
25
|
+
"""
|
|
26
|
+
cfg = get_config(c)
|
|
27
|
+
python_version = cfg["package"]["python_version"]
|
|
28
|
+
logger.info(f"๐ Creating virtual environment with Python {python_version}...")
|
|
29
|
+
c.run(f"uv venv --python {python_version} --clear", pty=True)
|
|
30
|
+
logger.info("๐ฆ Installing invoke...")
|
|
31
|
+
c.run("uv tool install invoke", pty=True)
|
|
32
|
+
logger.info("โ
Virtual environment created successfully")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@task
|
|
36
|
+
def install(
|
|
37
|
+
c: Context, optional_deps: bool = True, dev_deps: bool = True, docs_deps: bool = False
|
|
38
|
+
) -> None:
|
|
39
|
+
r"""Install project dependencies and the package in editable mode.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
c: The invoke context.
|
|
43
|
+
optional_deps: If True, install all optional dependencies defined in
|
|
44
|
+
the project extras. Default is True.
|
|
45
|
+
dev_deps: If True, install development dependencies. Default is True.
|
|
46
|
+
docs_deps: If True, install documentation generation dependencies.
|
|
47
|
+
Default is False.
|
|
48
|
+
"""
|
|
49
|
+
logger.info("๐ฆ Installing project dependencies...")
|
|
50
|
+
cmd = ["uv sync --frozen"]
|
|
51
|
+
if optional_deps:
|
|
52
|
+
cmd.append("--all-extras")
|
|
53
|
+
if dev_deps:
|
|
54
|
+
cmd.append("--group dev")
|
|
55
|
+
if docs_deps:
|
|
56
|
+
cmd.append("--group docs")
|
|
57
|
+
c.run(" ".join(cmd), pty=True)
|
|
58
|
+
logger.info("๐ง Installing package in editable mode...")
|
|
59
|
+
c.run("uv pip install -e .", pty=True)
|
|
60
|
+
logger.info("โ
Installation complete")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@task
|
|
64
|
+
def update(c: Context) -> None:
|
|
65
|
+
r"""Update dependencies and pre-commit hooks to their latest versions.
|
|
66
|
+
|
|
67
|
+
Warning:
|
|
68
|
+
This may introduce breaking changes. Review the changes and run tests
|
|
69
|
+
after updating.
|
|
70
|
+
"""
|
|
71
|
+
logger.info("๐ Updating dependencies...")
|
|
72
|
+
c.run("uv sync --upgrade", pty=True)
|
|
73
|
+
logger.info("๐ ๏ธ Upgrading uv tools...")
|
|
74
|
+
c.run("uv tool upgrade --all", pty=True)
|
|
75
|
+
logger.info("๐ช Updating pre-commit hooks...")
|
|
76
|
+
c.run("pre-commit autoupdate", pty=True)
|
|
77
|
+
logger.info("๐ฆ Reinstalling with docs dependencies...")
|
|
78
|
+
install(c, docs_deps=True)
|
|
79
|
+
logger.info("โ
Update complete")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@task
|
|
83
|
+
def show_installed_packages(c: Context) -> None:
|
|
84
|
+
r"""Show the installed packages."""
|
|
85
|
+
logger.info("๐ฆ Listing installed packages...")
|
|
86
|
+
c.run("uv pip list", pty=True)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@task
|
|
90
|
+
def show_python_config(c: Context) -> None:
|
|
91
|
+
r"""Show the python configuration."""
|
|
92
|
+
logger.info("๐ Python configuration:")
|
|
93
|
+
c.run("uv python list --only-installed", pty=True)
|
|
94
|
+
c.run("uv python find", pty=True)
|
|
95
|
+
c.run("which python", pty=True)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
r"""Code and docstring formatting tasks.
|
|
2
|
+
|
|
3
|
+
Naming convention: ``check_<target>`` tasks are read-only (they fail
|
|
4
|
+
without modifying files); ``fix_<target>`` tasks modify files in place.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from invoke.tasks import task
|
|
13
|
+
|
|
14
|
+
from invoke_tasklib.config import get_config
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from invoke.context import Context
|
|
18
|
+
|
|
19
|
+
logger: logging.Logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
_FIND_SH = "find . -name '*.sh' -type f -not -path './.git/*'"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@task
|
|
25
|
+
def check_python(c: Context) -> None:
|
|
26
|
+
r"""Check code format with ruff without modifying files."""
|
|
27
|
+
logger.info("๐จ Checking code format with ruff...")
|
|
28
|
+
c.run("ruff format --check .", pty=True)
|
|
29
|
+
logger.info("โ
Code format check passed")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@task
|
|
33
|
+
def check_docstrings(c: Context) -> None:
|
|
34
|
+
r"""Check docstring formatting with docformatter without modifying
|
|
35
|
+
files."""
|
|
36
|
+
cfg = get_config(c)
|
|
37
|
+
src = cfg["paths"]["src"]
|
|
38
|
+
logger.info("๐ Checking docstring formatting...")
|
|
39
|
+
c.run(f"docformatter --config ./pyproject.toml --check {src}", pty=True)
|
|
40
|
+
logger.info("โ
Docstring format check passed")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@task
|
|
44
|
+
def fix_python(c: Context) -> None:
|
|
45
|
+
r"""Format code in place with ruff.
|
|
46
|
+
|
|
47
|
+
Note:
|
|
48
|
+
This modifies files in place. Ensure your work is committed before
|
|
49
|
+
running this task.
|
|
50
|
+
"""
|
|
51
|
+
logger.info("๐จ Formatting code with ruff...")
|
|
52
|
+
c.run("ruff format .", pty=True)
|
|
53
|
+
logger.info("โ
Code formatting complete")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@task
|
|
57
|
+
def fix_docstrings(c: Context) -> None:
|
|
58
|
+
r"""Format docstrings in source code with docformatter.
|
|
59
|
+
|
|
60
|
+
Note:
|
|
61
|
+
This modifies files in place. Ensure your work is committed before
|
|
62
|
+
running this task.
|
|
63
|
+
"""
|
|
64
|
+
cfg = get_config(c)
|
|
65
|
+
src = cfg["paths"]["src"]
|
|
66
|
+
logger.info("๐ Formatting docstrings...")
|
|
67
|
+
c.run(f"docformatter --config ./pyproject.toml --in-place {src}", pty=True)
|
|
68
|
+
logger.info("โ
Docstring formatting complete")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@task
|
|
72
|
+
def check_shell(c: Context) -> None:
|
|
73
|
+
r"""Check shell scripts with shellcheck."""
|
|
74
|
+
logger.info("๐ Running shellcheck on shell scripts...")
|
|
75
|
+
c.run(f"{_FIND_SH} -print0 | xargs -0 -r shellcheck --", pty=True)
|
|
76
|
+
logger.info("โ
Shellcheck passed")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@task
|
|
80
|
+
def fix_shell(c: Context) -> None:
|
|
81
|
+
r"""Format shell scripts in place with shfmt.
|
|
82
|
+
|
|
83
|
+
Note:
|
|
84
|
+
This modifies files in place. Ensure your work is committed before
|
|
85
|
+
running this task.
|
|
86
|
+
"""
|
|
87
|
+
logger.info("๐ง Running shfmt to format shell scripts...")
|
|
88
|
+
c.run(f"{_FIND_SH} -print0 | xargs -0 -r shfmt -l -w --", pty=True)
|
|
89
|
+
logger.info("โ
Shell formatting complete")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
r"""Lint tasks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from invoke.tasks import task
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from invoke.context import Context
|
|
12
|
+
|
|
13
|
+
logger: logging.Logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@task
|
|
17
|
+
def check_lint(c: Context) -> None:
|
|
18
|
+
r"""Check code linting with ruff."""
|
|
19
|
+
logger.info("๐ Checking code linting with ruff...")
|
|
20
|
+
c.run("ruff check --output-format=github .", pty=True)
|
|
21
|
+
logger.info("โ
Linting check passed")
|
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
r"""Build and publish tasks (PyPI package)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from invoke.tasks import task
|
|
9
|
+
|
|
10
|
+
from invoke_tasklib.config import get_config
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from invoke.context import Context
|
|
14
|
+
|
|
15
|
+
logger: logging.Logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@task
|
|
19
|
+
def build(c: Context, check: bool = False) -> None:
|
|
20
|
+
r"""Build the package and verify it can be installed.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
c: The invoke context.
|
|
24
|
+
check: If True, also check the package's PyPI metadata with
|
|
25
|
+
twine. Default is False.
|
|
26
|
+
"""
|
|
27
|
+
cfg = get_config(c)
|
|
28
|
+
name = cfg["package"]["name"]
|
|
29
|
+
logger.info("๐ฆ Building package...")
|
|
30
|
+
c.run("uv build", pty=True)
|
|
31
|
+
logger.info("๐ Verifying package installation...")
|
|
32
|
+
c.run(
|
|
33
|
+
f'uv run --with {name} --refresh-package {name} --no-project -- python -c "import {name}"',
|
|
34
|
+
pty=True,
|
|
35
|
+
)
|
|
36
|
+
if check:
|
|
37
|
+
logger.info("๐ Checking package metadata with twine...")
|
|
38
|
+
c.run("uvx twine check dist/*", pty=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@task
|
|
42
|
+
def pypi(c: Context) -> None:
|
|
43
|
+
r"""Build and publish the package to PyPI."""
|
|
44
|
+
build(c)
|
|
45
|
+
logger.info("๐ Publishing to PyPI...")
|
|
46
|
+
c.run("uv publish --token ${PYPI_TOKEN}", pty=True)
|
|
47
|
+
logger.info("โ
Package published successfully")
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
r"""Test and benchmark tasks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from invoke.tasks import task
|
|
9
|
+
|
|
10
|
+
from invoke_tasklib.config import get_config
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from invoke.context import Context
|
|
14
|
+
|
|
15
|
+
logger: logging.Logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@task
|
|
19
|
+
def doctest(c: Context) -> None:
|
|
20
|
+
r"""Run doctests on source code."""
|
|
21
|
+
cfg = get_config(c)
|
|
22
|
+
src = cfg["paths"]["src"]
|
|
23
|
+
logger.info("๐ Running doctests on source code...")
|
|
24
|
+
c.run(f"python -m pytest --xdoctest {src}", pty=True)
|
|
25
|
+
logger.info("โ
Doctest validation complete")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@task
|
|
29
|
+
def all(c: Context, cov: bool = False) -> None:
|
|
30
|
+
r"""Run all tests (unit, integration, and functional).
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
c: The invoke context.
|
|
34
|
+
cov: If True, generate coverage reports in HTML, XML, and terminal
|
|
35
|
+
formats. Default is False.
|
|
36
|
+
"""
|
|
37
|
+
cfg = get_config(c)
|
|
38
|
+
name = cfg["package"]["name"]
|
|
39
|
+
tests = cfg["paths"]["tests"]
|
|
40
|
+
logger.info("๐งช Running all tests...")
|
|
41
|
+
cmd = ["python -m pytest --xdoctest --timeout 10"]
|
|
42
|
+
if cov:
|
|
43
|
+
cmd.append(f"--cov-report html --cov-report xml --cov-report term --cov={name}")
|
|
44
|
+
logger.info("๐ Coverage reports will be generated")
|
|
45
|
+
cmd.append(tests)
|
|
46
|
+
c.run(" ".join(cmd), pty=True)
|
|
47
|
+
logger.info("โ
All tests complete")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@task
|
|
51
|
+
def unit(c: Context, cov: bool = False) -> None:
|
|
52
|
+
r"""Run unit tests.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
c: The invoke context.
|
|
56
|
+
cov: If True, generate coverage reports. Default is False.
|
|
57
|
+
"""
|
|
58
|
+
cfg = get_config(c)
|
|
59
|
+
name = cfg["package"]["name"]
|
|
60
|
+
unit_tests = cfg["paths"]["unit_tests"]
|
|
61
|
+
logger.info("๐งช Running unit tests...")
|
|
62
|
+
cmd = ["python -m pytest --xdoctest --timeout 10"]
|
|
63
|
+
if cov:
|
|
64
|
+
cmd.append(f"--cov-report html --cov-report xml --cov-report term --cov={name}")
|
|
65
|
+
logger.info("๐ Coverage reports will be generated")
|
|
66
|
+
cmd.append(unit_tests)
|
|
67
|
+
c.run(" ".join(cmd), pty=True)
|
|
68
|
+
logger.info("โ
Unit tests complete")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@task
|
|
72
|
+
def integration(c: Context, cov: bool = False) -> None:
|
|
73
|
+
r"""Run integration tests.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
c: The invoke context.
|
|
77
|
+
cov: If True, generate coverage reports (appended). Default is False.
|
|
78
|
+
"""
|
|
79
|
+
cfg = get_config(c)
|
|
80
|
+
name = cfg["package"]["name"]
|
|
81
|
+
integration_tests = cfg["paths"]["integration_tests"]
|
|
82
|
+
logger.info("๐งช Running integration tests...")
|
|
83
|
+
cmd = ["python -m pytest --xdoctest --timeout 60"]
|
|
84
|
+
if cov:
|
|
85
|
+
cmd.append(
|
|
86
|
+
f"--cov-report html --cov-report xml --cov-report term --cov-append --cov={name}"
|
|
87
|
+
)
|
|
88
|
+
logger.info("๐ Coverage reports will be generated (appending)")
|
|
89
|
+
cmd.append(integration_tests)
|
|
90
|
+
c.run(" ".join(cmd), pty=True)
|
|
91
|
+
logger.info("โ
Integration tests complete")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@task
|
|
95
|
+
def functional(c: Context, cov: bool = False) -> None:
|
|
96
|
+
r"""Run functional tests.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
c: The invoke context.
|
|
100
|
+
cov: If True, generate coverage reports (appended). Default is False.
|
|
101
|
+
"""
|
|
102
|
+
cfg = get_config(c)
|
|
103
|
+
name = cfg["package"]["name"]
|
|
104
|
+
functional_tests = cfg["paths"]["functional_tests"]
|
|
105
|
+
logger.info("๐งช Running functional tests...")
|
|
106
|
+
cmd = ["python -m pytest --xdoctest --timeout 60"]
|
|
107
|
+
if cov:
|
|
108
|
+
cmd.append(
|
|
109
|
+
f"--cov-report html --cov-report xml --cov-report term --cov-append --cov={name}"
|
|
110
|
+
)
|
|
111
|
+
logger.info("๐ Coverage reports will be generated (appending)")
|
|
112
|
+
cmd.append(functional_tests)
|
|
113
|
+
c.run(" ".join(cmd), pty=True)
|
|
114
|
+
logger.info("โ
Functional tests complete")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@task
|
|
118
|
+
def benchmark(c: Context) -> None:
|
|
119
|
+
r"""Run performance benchmarks."""
|
|
120
|
+
cfg = get_config(c)
|
|
121
|
+
benchmarks = cfg["paths"]["benchmarks"]
|
|
122
|
+
logger.info("โฑ๏ธ Running benchmarks...")
|
|
123
|
+
c.run(f"python -m pytest {benchmarks}/ --benchmark-only", pty=True)
|
|
124
|
+
logger.info("โ
Benchmarks complete")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
r"""Type-checking tasks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from invoke.tasks import task
|
|
9
|
+
|
|
10
|
+
from invoke_tasklib.config import get_config
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from invoke.context import Context
|
|
14
|
+
|
|
15
|
+
logger: logging.Logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@task
|
|
19
|
+
def check(c: Context) -> None:
|
|
20
|
+
r"""Check type hints with pyright."""
|
|
21
|
+
cfg = get_config(c)
|
|
22
|
+
name = cfg["package"]["name"]
|
|
23
|
+
logger.info("๐ฌ Checking type hints with pyright...")
|
|
24
|
+
c.run(f"pyright --verifytypes {name} --ignoreexternal", pty=True)
|
|
25
|
+
logger.info("โ
Type check passed")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling >= 1.32"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[tool.hatch.build]
|
|
6
|
+
sources = ["src"]
|
|
7
|
+
|
|
8
|
+
[tool.hatch.build.targets.sdist]
|
|
9
|
+
only-include = ["src"]
|
|
10
|
+
|
|
11
|
+
[project]
|
|
12
|
+
name = "invoke-tasklib"
|
|
13
|
+
version = "0.0.1"
|
|
14
|
+
description = "Reusable Invoke tasks shared across Python projects."
|
|
15
|
+
readme = "README.md"
|
|
16
|
+
authors = [
|
|
17
|
+
{ name = "Thibaut Durand", email = "durand.tibo+gh@gmail.com" },
|
|
18
|
+
]
|
|
19
|
+
keywords = ["invoke", "tasks", "automation", "devtools"]
|
|
20
|
+
license = "BSD-3-Clause"
|
|
21
|
+
license-files = ["LICENSE"]
|
|
22
|
+
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 3 - Alpha",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"License :: OSI Approved :: BSD License",
|
|
27
|
+
"Operating System :: POSIX :: Linux",
|
|
28
|
+
"Operating System :: MacOS",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
33
|
+
"Programming Language :: Python :: 3.14",
|
|
34
|
+
"Topic :: Software Development :: Build Tools",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
requires-python = ">=3.10"
|
|
38
|
+
dependencies = ["invoke>=3.0"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
[dependency-groups]
|
|
43
|
+
dev = [
|
|
44
|
+
"coverage[toml]>=7.15,<8.0",
|
|
45
|
+
"docformatter[tomli]>=1.7.8,<2.0",
|
|
46
|
+
"feu[all]>=0.8.1,<1.0",
|
|
47
|
+
"invoke>=3.0,<4.0",
|
|
48
|
+
"pre-commit>=4.6,<5.0",
|
|
49
|
+
"pygments>=2.20,<3.0",
|
|
50
|
+
"pyright>=1.1.411,<2.0",
|
|
51
|
+
"pytest>=9.1,<10.0",
|
|
52
|
+
"pytest-cov>=7.1,<8.0",
|
|
53
|
+
"pytest-timeout>=2.4,<3.0",
|
|
54
|
+
"ruff>=0.16,<1.0",
|
|
55
|
+
"xdoctest>=1.3,<2.0",
|
|
56
|
+
]
|
|
57
|
+
docs = [
|
|
58
|
+
"mike >=2.2,<3.0",
|
|
59
|
+
"mkdocs-material >=9.7,<10.0",
|
|
60
|
+
"mkdocstrings[python]>=1.0,<2.0",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[project.urls]
|
|
64
|
+
Homepage = "https://github.com/durandtibo/invoke-tasklib"
|
|
65
|
+
Repository = "https://github.com/durandtibo/invoke-tasklib"
|
|
66
|
+
Documentation = "https://durandtibo.github.io/invoke-tasklib/"
|
|
67
|
+
Changelog = "https://github.com/durandtibo/invoke-tasklib/releases"
|
|
68
|
+
Issues = "https://github.com/durandtibo/invoke-tasklib/issues"
|
|
69
|
+
|
|
70
|
+
[tool.ruff]
|
|
71
|
+
line-length = 100
|
|
72
|
+
target-version = "py310"
|
|
73
|
+
|
|
74
|
+
[tool.pytest.ini_options]
|
|
75
|
+
testpaths = ["tests"]
|
|
76
|
+
|
|
77
|
+
[tool.pyright]
|
|
78
|
+
include = ["src"]
|
|
79
|
+
venvPath = "."
|
|
80
|
+
venv = ".venv"
|