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.
- invoke_tasklib/__init__.py +37 -0
- invoke_tasklib/config.py +109 -0
- invoke_tasklib/doc.py +59 -0
- invoke_tasklib/env.py +95 -0
- invoke_tasklib/format.py +89 -0
- invoke_tasklib/lint.py +21 -0
- invoke_tasklib/py.typed +0 -0
- invoke_tasklib/release.py +47 -0
- invoke_tasklib/test.py +124 -0
- invoke_tasklib/types.py +25 -0
- invoke_tasklib-0.0.1.dist-info/METADATA +163 -0
- invoke_tasklib-0.0.1.dist-info/RECORD +14 -0
- invoke_tasklib-0.0.1.dist-info/WHEEL +4 -0
- invoke_tasklib-0.0.1.dist-info/licenses/LICENSE +28 -0
|
@@ -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")
|
invoke_tasklib/config.py
ADDED
|
@@ -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)
|
invoke_tasklib/format.py
ADDED
|
@@ -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")
|
invoke_tasklib/py.typed
ADDED
|
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")
|
invoke_tasklib/types.py
ADDED
|
@@ -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,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.
|