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.
@@ -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"