minchin.tasks 1.0.0__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 William Minchin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: minchin.tasks
3
+ Version: 1.0.0
4
+ Summary: Common development tasks (lint, format, test, ...) for use with Invoke.
5
+ Author-email: William Minchin <w_minchin@hotmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Bug Tracker, https://github.com/MinchinWeb/minchin.tasks/issues
8
+ Project-URL: Repository, https://github.com/MinchinWeb/minchin.tasks
9
+ Project-URL: Changelog, https://github.com/MinchinWeb/minchin.tasks/blob/master/CHANGELOG.rst
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Natural Language :: English
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Environment :: Console
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/x-rst
19
+ License-File: LICENSE.txt
20
+ Requires-Dist: invoke>=2.0.0
21
+ Requires-Dist: taplo
22
+ Requires-Dist: validate-pyproject
23
+ Dynamic: license-file
24
+
25
+ Minchin.Tasks
26
+ =============
27
+
28
+ Common development tasks for use with `Invoke <https://www.pyinvoke.org/>`_.
29
+ Defaults to a ``uv`` and ``ruff`` setup, but should continue to work with
30
+ ``black``, ``isort``, and ``pip-tools``. Useful for applying my in-house Python
31
+ style, see https://github.com/minchinweb/python-style-guide
32
+
33
+ In your ``tasks.py`` (import each for clarity)::
34
+
35
+ from minchin.tasks import check, format_code, lint, test, update, upgrade # noqa: F401
36
+
37
+ The tasks are defined in ``minchin.tasks.dev``; ``minchin.tasks`` re-exports them.
38
+
39
+ This gives you:
40
+
41
+ - ``invoke update`` -- update requirements to the current versions
42
+ - ``invoke upgrade`` -- install the locked requirements
43
+ - ``invoke lint`` -- report only format check, including ``pyproject.toml``
44
+ - ``invoke format`` -- fix formatting issues
45
+ - ``invoke test`` -- run ``pytest``.
46
+ - ``invoke check`` -- ``lint`` and ``test``.
@@ -0,0 +1,22 @@
1
+ Minchin.Tasks
2
+ =============
3
+
4
+ Common development tasks for use with `Invoke <https://www.pyinvoke.org/>`_.
5
+ Defaults to a ``uv`` and ``ruff`` setup, but should continue to work with
6
+ ``black``, ``isort``, and ``pip-tools``. Useful for applying my in-house Python
7
+ style, see https://github.com/minchinweb/python-style-guide
8
+
9
+ In your ``tasks.py`` (import each for clarity)::
10
+
11
+ from minchin.tasks import check, format_code, lint, test, update, upgrade # noqa: F401
12
+
13
+ The tasks are defined in ``minchin.tasks.dev``; ``minchin.tasks`` re-exports them.
14
+
15
+ This gives you:
16
+
17
+ - ``invoke update`` -- update requirements to the current versions
18
+ - ``invoke upgrade`` -- install the locked requirements
19
+ - ``invoke lint`` -- report only format check, including ``pyproject.toml``
20
+ - ``invoke format`` -- fix formatting issues
21
+ - ``invoke test`` -- run ``pytest``.
22
+ - ``invoke check`` -- ``lint`` and ``test``.
@@ -0,0 +1,24 @@
1
+ """Common development tasks to use from `invoke`.
2
+
3
+ In a project's ``tasks.py``::
4
+
5
+ from minchin.tasks import ( # noqa: F401
6
+ check,
7
+ format_code,
8
+ lint,
9
+ test,
10
+ update,
11
+ upgrade,
12
+ )
13
+
14
+ Uses ``uv run`` under the hood by default, or ``pip-tools`` on "legacy" setups.
15
+ The tasks themselves live in `minchin.tasks.dev`.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from minchin.tasks.dev import check, format_code, lint, test, update, upgrade
21
+
22
+ __version__ = "1.0.0"
23
+
24
+ __all__ = ["check", "format_code", "lint", "test", "update", "upgrade"]
@@ -0,0 +1,162 @@
1
+ """Common development tasks (lint, format, test, ...) to use from `invoke`.
2
+
3
+ Uses ``uv run`` under the hood by default, or ``pip-tools`` on "legacy" setups.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from invoke import task
12
+
13
+ __all__ = ["check", "format_code", "lint", "test", "update", "upgrade"]
14
+
15
+
16
+ def _uses_uv() -> bool:
17
+ # A `uv.lock` implies the project is managed by uv
18
+ return Path("uv.lock").is_file()
19
+
20
+
21
+ def _pip_tools_inputs() -> list[Path]:
22
+ # look for `.in` files in both `.requirements/*.in` or the repo root `*.in`
23
+ found = [*Path(".requirements").glob("*.in"), *Path().glob("*.in")]
24
+ # blacklist `MANIFEST.in` as a setuptools file
25
+ return sorted(p for p in found if p.name.lower() != "manifest.in")
26
+
27
+
28
+ def _compiled_requirements() -> dict[str, Path]:
29
+ """Get a list of the requirement files.
30
+
31
+ `requirements-base.in` is assumed to the "main" one.
32
+ """
33
+ found = {}
34
+ for path in _pip_tools_inputs():
35
+ compiled = path.with_suffix(".txt")
36
+ if not compiled.is_file():
37
+ continue
38
+ name = compiled.stem.removeprefix("requirements").lstrip("-_")
39
+ if name in ("", "base"):
40
+ name = "main"
41
+ found[name] = compiled
42
+ return found
43
+
44
+
45
+ def _uses_black() -> bool:
46
+ # Black/isort config in `pyproject.toml` without a ruff config
47
+ pyproject = Path("pyproject.toml")
48
+ if not pyproject.is_file():
49
+ return False
50
+ text = pyproject.read_text(encoding="utf-8")
51
+ legacy = "[tool.black]" in text or "[tool.isort]" in text
52
+ return legacy and "[tool.ruff" not in text
53
+
54
+
55
+ def _run_prefix() -> str:
56
+ # run uv projects using `uv`
57
+ return "uv run " if _uses_uv() else ""
58
+
59
+
60
+ @task
61
+ def update(ctx):
62
+ """Refresh the requirements with the current versions.
63
+
64
+ Rewrites `uv.lock`, or recompiles pip-tools' `.txt`.
65
+ """
66
+ if _uses_uv():
67
+ ctx.run("uv lock --upgrade", pty=False)
68
+ return
69
+ inputs = _pip_tools_inputs()
70
+ if not inputs:
71
+ raise SystemExit("No `uv.lock` or pip-tools `.in` files found.")
72
+ for source in inputs:
73
+ ctx.run(f"pip-compile --upgrade {source.as_posix()}", pty=False)
74
+
75
+
76
+ @task(
77
+ iterable=["extra"],
78
+ help={
79
+ "extra": "Also install this requirements set, e.g. `dev` ("
80
+ "pip-tools projects only)",
81
+ "all_": "Install every compiled requirements file (pip-tools only)",
82
+ "dry_run": "Show what would change without installing anything",
83
+ },
84
+ )
85
+ def upgrade(ctx, extra=None, all_=False, dry_run=False):
86
+ """Install based on the current requiremnts.
87
+
88
+ Runs `uv sync`, or `pip-sync` on the main compiled pip-tools file plus any
89
+ sets named with `--extra` (or all of them with `--all`).
90
+
91
+ Use `--dry-run` to check first.
92
+ """
93
+ flag = " --dry-run" if dry_run else ""
94
+ if _uses_uv():
95
+ ctx.run(f"uv sync{flag}", pty=False)
96
+ return
97
+ available = _compiled_requirements()
98
+ if not available:
99
+ raise SystemExit("No `uv.lock` or compiled pip-tools `.txt` files found.")
100
+ names = list(available) if all_ else ["main", *(extra or [])]
101
+ unknown = [n for n in names if n not in available]
102
+ if unknown:
103
+ raise SystemExit(
104
+ f"No compiled requirements for: {', '.join(unknown)}. "
105
+ f"Available: {', '.join(available)}."
106
+ )
107
+ # pip-sync wants each file once; keep the order given
108
+ files = dict.fromkeys(available[n].as_posix() for n in names)
109
+ ctx.run(f"pip-sync{flag} {' '.join(files)}", pty=False)
110
+
111
+
112
+ @task
113
+ def lint(ctx):
114
+ """Check code, formatting and `pyproject.toml`."""
115
+ run = _run_prefix()
116
+ if _uses_black():
117
+ ctx.run(f"{run}black --check .", pty=False)
118
+ ctx.run(f"{run}isort --check-only .", pty=False)
119
+ print(
120
+ "WARNING: pyproject.toml was not checked (validate-pyproject and "
121
+ "taplo only run for ruff projects); review it by hand.",
122
+ file=sys.stderr,
123
+ )
124
+ return
125
+
126
+ ctx.run("uv run ruff check", pty=False)
127
+ ctx.run("uv run ruff format --check", pty=False)
128
+ ctx.run("uv run validate-pyproject pyproject.toml", pty=False)
129
+ ctx.run("uv run taplo format --check pyproject.toml", pty=False)
130
+
131
+
132
+ # named `format_code` so it doesn't shadow the builtin (ruff rule `A`)
133
+ @task(name="format")
134
+ def format_code(ctx):
135
+ """Apply safe lint fixes, import sorting and formatting (code and TOML)."""
136
+ if _uses_black():
137
+ run = _run_prefix()
138
+ ctx.run(f"{run}isort .", pty=False)
139
+ ctx.run(f"{run}black .", pty=False)
140
+ return
141
+
142
+ # unfixable problems exit non-zero; still format, `lint` reports them
143
+ ctx.run("uv run ruff check --fix", pty=False, warn=True)
144
+ ctx.run("uv run ruff format", pty=False)
145
+ ctx.run("uv run taplo format pyproject.toml", pty=False)
146
+
147
+
148
+ @task
149
+ def test(ctx):
150
+ """Run the test suite.
151
+
152
+ Assumes `pytest`.
153
+ """
154
+ ctx.run(f"{_run_prefix()}pytest", pty=False)
155
+
156
+
157
+ @task(pre=[lint, test])
158
+ def check(ctx):
159
+ """lint, formatting and tests.
160
+
161
+ Useful for single-run CI runs.
162
+ """
@@ -0,0 +1,58 @@
1
+ """Tests for the shared development tasks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from invoke import Collection, Context
6
+
7
+ from minchin.tasks import dev as tasks
8
+
9
+
10
+ class RecordingContext(Context):
11
+ """Stands in for an Invoke context and records the commands it is given."""
12
+
13
+ def __init__(self):
14
+ super().__init__()
15
+ self.commands = []
16
+
17
+ def run(self, command, **kwargs):
18
+ self.commands.append((command, kwargs))
19
+
20
+
21
+ def run_task(task):
22
+ ctx = RecordingContext()
23
+ task(ctx)
24
+ return ctx.commands
25
+
26
+
27
+ def test_update_and_upgrade_mirror_apt():
28
+ assert run_task(tasks.update)[0][0] == "uv lock --upgrade"
29
+ assert run_task(tasks.upgrade)[0][0] == "uv sync"
30
+
31
+
32
+ def test_lint_changes_nothing_and_checks_pyproject():
33
+ commands = [c for c, _ in run_task(tasks.lint)]
34
+ assert commands == [
35
+ "uv run ruff check",
36
+ "uv run ruff format --check",
37
+ "uv run validate-pyproject pyproject.toml",
38
+ "uv run taplo format --check pyproject.toml",
39
+ ]
40
+
41
+
42
+ def test_format_still_formats_when_lint_fixes_fail():
43
+ commands = run_task(tasks.format_code)
44
+ assert commands[0][0] == "uv run ruff check --fix"
45
+ assert commands[0][1]["warn"] is True
46
+ assert [c for c, _ in commands[1:]] == [
47
+ "uv run ruff format",
48
+ "uv run taplo format pyproject.toml",
49
+ ]
50
+
51
+
52
+ def test_task_names_match_the_documented_commands():
53
+ names = set(Collection.from_module(tasks).task_names)
54
+ assert names == {"check", "format", "lint", "test", "update", "upgrade"}
55
+
56
+
57
+ def test_check_runs_lint_then_test():
58
+ assert [t.__name__ for t in tasks.check.pre] == ["lint", "test"]
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: minchin.tasks
3
+ Version: 1.0.0
4
+ Summary: Common development tasks (lint, format, test, ...) for use with Invoke.
5
+ Author-email: William Minchin <w_minchin@hotmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Bug Tracker, https://github.com/MinchinWeb/minchin.tasks/issues
8
+ Project-URL: Repository, https://github.com/MinchinWeb/minchin.tasks
9
+ Project-URL: Changelog, https://github.com/MinchinWeb/minchin.tasks/blob/master/CHANGELOG.rst
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Natural Language :: English
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Environment :: Console
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/x-rst
19
+ License-File: LICENSE.txt
20
+ Requires-Dist: invoke>=2.0.0
21
+ Requires-Dist: taplo
22
+ Requires-Dist: validate-pyproject
23
+ Dynamic: license-file
24
+
25
+ Minchin.Tasks
26
+ =============
27
+
28
+ Common development tasks for use with `Invoke <https://www.pyinvoke.org/>`_.
29
+ Defaults to a ``uv`` and ``ruff`` setup, but should continue to work with
30
+ ``black``, ``isort``, and ``pip-tools``. Useful for applying my in-house Python
31
+ style, see https://github.com/minchinweb/python-style-guide
32
+
33
+ In your ``tasks.py`` (import each for clarity)::
34
+
35
+ from minchin.tasks import check, format_code, lint, test, update, upgrade # noqa: F401
36
+
37
+ The tasks are defined in ``minchin.tasks.dev``; ``minchin.tasks`` re-exports them.
38
+
39
+ This gives you:
40
+
41
+ - ``invoke update`` -- update requirements to the current versions
42
+ - ``invoke upgrade`` -- install the locked requirements
43
+ - ``invoke lint`` -- report only format check, including ``pyproject.toml``
44
+ - ``invoke format`` -- fix formatting issues
45
+ - ``invoke test`` -- run ``pytest``.
46
+ - ``invoke check`` -- ``lint`` and ``test``.
@@ -0,0 +1,11 @@
1
+ LICENSE.txt
2
+ README.rst
3
+ pyproject.toml
4
+ minchin.tasks.egg-info/PKG-INFO
5
+ minchin.tasks.egg-info/SOURCES.txt
6
+ minchin.tasks.egg-info/dependency_links.txt
7
+ minchin.tasks.egg-info/requires.txt
8
+ minchin.tasks.egg-info/top_level.txt
9
+ minchin/tasks/__init__.py
10
+ minchin/tasks/dev.py
11
+ minchin/tasks/tests/test_dev.py
@@ -0,0 +1,3 @@
1
+ invoke>=2.0.0
2
+ taplo
3
+ validate-pyproject
@@ -0,0 +1,132 @@
1
+ [build-system]
2
+ # v77 needed for SPDX `license` expressions (PEP 639)
3
+ requires = ["setuptools >= 77"]
4
+ build-backend = "setuptools.build_meta"
5
+
6
+ [project]
7
+ name = "minchin.tasks"
8
+ description = "Common development tasks (lint, format, test, ...) for use with Invoke."
9
+ dynamic = ["version", "readme"]
10
+ authors = [{ name = "William Minchin", email = "w_minchin@hotmail.com" }]
11
+ requires-python = ">= 3.9"
12
+ license = "MIT"
13
+ license-files = ["LICEN[CS]E*"]
14
+ classifiers = [
15
+ # having an unknown classifier should keep PyPI from accepting the
16
+ # package as an upload
17
+ # 'Private :: Do Not Upload',
18
+ # 'Development Status :: 1 - Planning',
19
+ # 'Development Status :: 2 - Pre-Alpha',
20
+ # 'Development Status :: 3 - Alpha',
21
+ 'Development Status :: 4 - Beta',
22
+ # 'Development Status :: 5 - Production/Stable',
23
+ # 'Development Status :: 6 - Mature',
24
+ # 'Development Status :: 7 - Inactive',
25
+ "Programming Language :: Python :: 3",
26
+ # "Programming Language :: Python :: 3.9",
27
+ # "Programming Language :: Python :: 3.10",
28
+ # "Programming Language :: Python :: 3.11",
29
+ # "Programming Language :: Python :: 3.12",
30
+ # "Programming Language :: Python :: 3.13",
31
+ # "Programming Language :: Python :: 3.14",
32
+ # "Programming Language :: Python :: 3.15",
33
+ "Programming Language :: Python :: 3 :: Only",
34
+ "Natural Language :: English",
35
+ "Intended Audience :: Developers",
36
+ "Operating System :: OS Independent",
37
+ "Environment :: Console",
38
+ # full list at https://pypi.org/classifiers/
39
+ ]
40
+ dependencies = [
41
+ "invoke >= 2.0.0",
42
+ # for pyproject.toml format checks
43
+ "taplo",
44
+ "validate-pyproject",
45
+ ]
46
+
47
+ [project.urls]
48
+ "Bug Tracker" = "https://github.com/MinchinWeb/minchin.tasks/issues"
49
+ Repository = "https://github.com/MinchinWeb/minchin.tasks"
50
+ Changelog = "https://github.com/MinchinWeb/minchin.tasks/blob/master/CHANGELOG.rst"
51
+
52
+ [dependency-groups]
53
+ # PEP 735; not published. `uv sync` installs `dev` by default.
54
+ dev = ["pytest >= 8", "ruff >= 0.14", { include-group = "release" }]
55
+ release = ["minchin.releaser >= 0.9.6"]
56
+
57
+ [tool.setuptools.packages.find]
58
+ # flat layout; tests live in the package (`minchin/<name>/tests/`).
59
+ # `minchin` is an implicit (PEP 420) namespace package, so it has NO
60
+ # `__init__.py`
61
+ include = ["minchin*"]
62
+ exclude = [
63
+ # top-level only; `minchin.<name>.tests` ships with the package
64
+ "build",
65
+ "dist",
66
+ "docs",
67
+ "env",
68
+ "test",
69
+ "tests",
70
+ ]
71
+
72
+ [tool.setuptools.dynamic]
73
+ # this has to be the place the version is located via static analysis, i.e.
74
+ # a plain `__version__ = "x.y.z"` with no imports needed to compute it
75
+ version = { attr = "minchin.tasks.__version__" }
76
+ readme = { file = ["README.rst"], content-type = "text/x-rst" }
77
+
78
+ [tool.pytest.ini_options]
79
+ addopts = ["--import-mode=importlib"]
80
+ # tests ship inside the package: minchin/<name>/tests/
81
+ testpaths = ["minchin"]
82
+
83
+ [tool.ruff]
84
+ line-length = 88
85
+ # also skip excluded paths when a path is given explicitly
86
+ force-exclude = true
87
+ extend-exclude = ["_build"]
88
+
89
+ [tool.ruff.format]
90
+ # Reformat code snippets in docstrings.
91
+ docstring-code-format = true
92
+ # Keep each file's existing line endings, so Windows (CRLF working tree) and
93
+ # the Linux CI runner don't fight each other.
94
+ line-ending = "auto"
95
+
96
+ [tool.ruff.lint]
97
+ # Enable `from __future__ import annotations` imports
98
+ future-annotations = true
99
+ # Added to ruff's default (E4, E7, E9, F)
100
+ extend-select = [
101
+ "A", # shadowing builtin names
102
+ "B", # bugbear
103
+ "E",
104
+ "W", # pycodestyle
105
+ "I", # isort
106
+ "N", # PEP8 naming
107
+ "PTH", # use pathlib
108
+ "RUF", # ruff-specific
109
+ "SIM", # simplify
110
+ "UP", # pyupgrade (limited by `requires-python`)
111
+ "YTT", # flake8-2020 (common issues with Python version checks)
112
+ ]
113
+
114
+ [tool.ruff.lint.isort]
115
+ # Sort imports within their section independent of the import type
116
+ force-sort-within-sections = true
117
+ known-first-party = ["minchin"]
118
+ # Framework sections are standard boilerplate; an unused one costs nothing.
119
+ # Delete a section's name from `section-order` only if its package clashes.
120
+ section-order = [
121
+ "future",
122
+ "standard-library",
123
+ "django",
124
+ "pelican",
125
+ "third-party",
126
+ "first-party",
127
+ "local-folder",
128
+ ]
129
+
130
+ [tool.ruff.lint.isort.sections]
131
+ "django" = ["django"]
132
+ "pelican" = ["pelican"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+