invoke-tasklib 0.0.1__py3-none-any.whl

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,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
+ }
invoke_tasklib/doc.py ADDED
@@ -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")
invoke_tasklib/env.py ADDED
@@ -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")
invoke_tasklib/lint.py ADDED
@@ -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")
invoke_tasklib/test.py ADDED
@@ -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,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,14 @@
1
+ invoke_tasklib/__init__.py,sha256=Hd-mmmIvfnAx43Zu9aQSIwdIrsK8OJ2v6W1Glk2kQoo,1163
2
+ invoke_tasklib/config.py,sha256=L-lGrvaSzBnordp_4DFC1lYRxkfqCHt5m8lt0ddU-lU,2884
3
+ invoke_tasklib/doc.py,sha256=ddEaXbaoZsaL-zspDESjPbME-s-8pHNtOTQ_CPz7MOg,2190
4
+ invoke_tasklib/env.py,sha256=ZFy3VjpIGDTmVwVSfirHgtlys2csBTnPKO3odddGDpw,3057
5
+ invoke_tasklib/format.py,sha256=NvLJQxPPsQ9GhrOmxkM5nvyE57pgLCfHns44LUQQzFI,2638
6
+ invoke_tasklib/lint.py,sha256=ojAPP6G16C2sLEk3JIrjeaIqIelEegyDrNgwv_lOe6c,494
7
+ invoke_tasklib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ invoke_tasklib/release.py,sha256=_J2lrVlbFZksfVd5lsDOjhb3odL_xXRtTkui6z-9FKg,1310
9
+ invoke_tasklib/test.py,sha256=HdcDyqGB91eQAEGGBiqKZXmg9VrqvPo-MbbDgah5YjQ,3875
10
+ invoke_tasklib/types.py,sha256=k01Yx781neHaftciTTLE1M-fWeNf0G29QLSvqTf4P9o,612
11
+ invoke_tasklib-0.0.1.dist-info/METADATA,sha256=m6MDkqqTjHA1CI2SPw6tDt0UKqw9prFGNcU-qcLwvNw,6521
12
+ invoke_tasklib-0.0.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
13
+ invoke_tasklib-0.0.1.dist-info/licenses/LICENSE,sha256=ZvV2ToDI3ZjTHHMRFQpG9WG1zV-OwBJCJRe_FZM6lBw,1501
14
+ invoke_tasklib-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.