duty 1.4.3__tar.gz → 1.6.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.
- {duty-1.4.3 → duty-1.6.0}/CHANGELOG.md +16 -0
- {duty-1.4.3 → duty-1.6.0}/PKG-INFO +1 -1
- {duty-1.4.3 → duty-1.6.0}/docs/index.md +1 -1
- {duty-1.4.3 → duty-1.6.0}/docs/usage.md +25 -13
- {duty-1.4.3 → duty-1.6.0}/duties.py +1 -0
- {duty-1.4.3 → duty-1.6.0}/pyproject.toml +5 -7
- {duty-1.4.3 → duty-1.6.0}/scripts/gen_credits.py +1 -1
- duty-1.6.0/scripts/get_version.py +27 -0
- duty-1.6.0/scripts/make +1 -0
- duty-1.4.3/scripts/make → duty-1.6.0/scripts/make.py +18 -17
- {duty-1.4.3 → duty-1.6.0}/src/duty/cli.py +29 -1
- {duty-1.4.3 → duty-1.6.0}/src/duty/collection.py +29 -0
- duty-1.6.0/src/duty/completions.bash +30 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/tools/__init__.py +5 -3
- duty-1.6.0/src/duty/tools/_yore.py +54 -0
- {duty-1.4.3 → duty-1.6.0}/tests/test_collection.py +16 -0
- {duty-1.4.3 → duty-1.6.0}/CODE_OF_CONDUCT.md +0 -0
- {duty-1.4.3 → duty-1.6.0}/CONTRIBUTING.md +0 -0
- {duty-1.4.3 → duty-1.6.0}/LICENSE +0 -0
- {duty-1.4.3 → duty-1.6.0}/README.md +0 -0
- {duty-1.4.3 → duty-1.6.0}/config/coverage.ini +0 -0
- {duty-1.4.3 → duty-1.6.0}/config/git-changelog.toml +0 -0
- {duty-1.4.3 → duty-1.6.0}/config/mypy.ini +0 -0
- {duty-1.4.3 → duty-1.6.0}/config/pytest.ini +0 -0
- {duty-1.4.3 → duty-1.6.0}/config/ruff.toml +0 -0
- {duty-1.4.3 → duty-1.6.0}/config/vscode/launch.json +0 -0
- {duty-1.4.3 → duty-1.6.0}/config/vscode/settings.json +0 -0
- {duty-1.4.3 → duty-1.6.0}/config/vscode/tasks.json +0 -0
- {duty-1.4.3 → duty-1.6.0}/docs/.overrides/main.html +0 -0
- {duty-1.4.3 → duty-1.6.0}/docs/.overrides/partials/comments.html +0 -0
- {duty-1.4.3 → duty-1.6.0}/docs/changelog.md +0 -0
- {duty-1.4.3 → duty-1.6.0}/docs/code_of_conduct.md +0 -0
- {duty-1.4.3 → duty-1.6.0}/docs/contributing.md +0 -0
- {duty-1.4.3 → duty-1.6.0}/docs/credits.md +0 -0
- {duty-1.4.3 → duty-1.6.0}/docs/css/material.css +0 -0
- {duty-1.4.3 → duty-1.6.0}/docs/css/mkdocstrings.css +0 -0
- {duty-1.4.3 → duty-1.6.0}/docs/demo.svg +0 -0
- {duty-1.4.3 → duty-1.6.0}/docs/gen_credits.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/docs/js/feedback.js +0 -0
- {duty-1.4.3 → duty-1.6.0}/docs/license.md +0 -0
- {duty-1.4.3 → duty-1.6.0}/mkdocs.yml +0 -0
- {duty-1.4.3 → duty-1.6.0}/scripts/gen_ref_nav.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/__init__.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/__main__.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/callables/__init__.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/callables/_io.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/callables/autoflake.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/callables/black.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/callables/blacken_docs.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/callables/build.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/callables/coverage.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/callables/flake8.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/callables/git_changelog.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/callables/griffe.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/callables/interrogate.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/callables/isort.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/callables/mkdocs.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/callables/mypy.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/callables/pytest.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/callables/ruff.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/callables/safety.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/callables/ssort.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/callables/twine.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/context.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/debug.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/decorator.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/exceptions.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/py.typed +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/tools/_autoflake.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/tools/_base.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/tools/_black.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/tools/_blacken_docs.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/tools/_build.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/tools/_coverage.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/tools/_flake8.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/tools/_git_changelog.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/tools/_griffe.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/tools/_interrogate.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/tools/_isort.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/tools/_mkdocs.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/tools/_mypy.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/tools/_pytest.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/tools/_ruff.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/tools/_safety.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/tools/_ssort.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/tools/_twine.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/src/duty/validation.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/tests/__init__.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/tests/conftest.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/tests/fixtures/arguments.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/tests/fixtures/basic.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/tests/fixtures/booleans.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/tests/fixtures/code.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/tests/fixtures/list.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/tests/fixtures/multiple.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/tests/fixtures/precedence.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/tests/fixtures/validation.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/tests/test_cli.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/tests/test_context.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/tests/test_decorator.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/tests/test_running.py +0 -0
- {duty-1.4.3 → duty-1.6.0}/tests/test_validation.py +0 -0
|
@@ -5,6 +5,22 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
|
5
5
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
7
|
<!-- insertion marker -->
|
|
8
|
+
## [1.6.0](https://github.com/pawamoy/duty/releases/tag/1.6.0) - 2025-03-01
|
|
9
|
+
|
|
10
|
+
<small>[Compare with 1.5.0](https://github.com/pawamoy/duty/compare/1.5.0...1.6.0)</small>
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- Add Yore tool ([4cbb478](https://github.com/pawamoy/duty/commit/4cbb478ac41258316188fcd3402e5165b0c233ac) by Timothée Mazzucotelli).
|
|
15
|
+
|
|
16
|
+
## [1.5.0](https://github.com/pawamoy/duty/releases/tag/1.5.0) - 2025-02-02
|
|
17
|
+
|
|
18
|
+
<small>[Compare with 1.4.3](https://github.com/pawamoy/duty/compare/1.4.3...1.5.0)</small>
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
- Enable Bash completions ([9ed4400](https://github.com/pawamoy/duty/commit/9ed44002ff8e122ea6e5aaaf4a968e08d0dc83fd) by Bartosz Sławecki). [Issue-27](https://github.com/pawamoy/duty/issues/27), [PR-33](https://github.com/pawamoy/duty/pull/33), Co-authored-by: Timothée Mazzucotelli <dev@pawamoy.fr>
|
|
23
|
+
|
|
8
24
|
## [1.4.3](https://github.com/pawamoy/duty/releases/tag/1.4.3) - 2024-10-17
|
|
9
25
|
|
|
10
26
|
<small>[Compare with 1.4.2](https://github.com/pawamoy/duty/compare/1.4.2...1.4.3)</small>
|
|
@@ -54,18 +54,17 @@ def docs(ctx):
|
|
|
54
54
|
# avoid the overhead of an extra Python process
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
For convenience, `duty` provides callables for many popular Python tools,
|
|
57
|
+
For convenience, `duty` provides callables (called "tools") for many popular Python tools,
|
|
58
58
|
so that you don't have to read their source and learn how to call them.
|
|
59
59
|
For example, the `mkdocs build` command can be called like this:
|
|
60
60
|
|
|
61
61
|
```python
|
|
62
|
-
from duty import duty
|
|
63
|
-
from duty.callables import mkdocs
|
|
62
|
+
from duty import duty, tools
|
|
64
63
|
|
|
65
64
|
|
|
66
65
|
@duty
|
|
67
66
|
def docs(ctx):
|
|
68
|
-
ctx.run(mkdocs.build, kwargs={"strict": True}, title="Building documentation")
|
|
67
|
+
ctx.run(tool.mkdocs.build, kwargs={"strict": True}, title="Building documentation")
|
|
69
68
|
```
|
|
70
69
|
|
|
71
70
|
### Lazy callables
|
|
@@ -78,19 +77,18 @@ without passing arguments and keyword arguments
|
|
|
78
77
|
with the `args` and `kwargs` parameters of `ctx.run()`:
|
|
79
78
|
|
|
80
79
|
```python
|
|
81
|
-
from duty import duty
|
|
82
|
-
from duty.callables import mkdocs
|
|
80
|
+
from duty import duty, tools
|
|
83
81
|
|
|
84
82
|
|
|
85
83
|
@duty
|
|
86
84
|
def docs(ctx):
|
|
87
|
-
ctx.run(mkdocs.build(strict=True), title="Building documentation")
|
|
85
|
+
ctx.run(tools.mkdocs.build(strict=True), title="Building documentation")
|
|
88
86
|
```
|
|
89
87
|
|
|
90
88
|
The main benefit is that it enables IDE features like help tooltips and auto-completion,
|
|
91
89
|
as well as improving readability and writability.
|
|
92
90
|
|
|
93
|
-
**[See all our
|
|
91
|
+
**[See all our tools in the Code reference][duty.tools].**
|
|
94
92
|
|
|
95
93
|
You can also create your own lazy callables with [`duty.tools.Tool`][]
|
|
96
94
|
and [`duty.tools.lazy`][failprint.lazy.lazy].
|
|
@@ -647,7 +645,7 @@ You can also pass parameters as positional arguments:
|
|
|
647
645
|
duty shoot 5,15
|
|
648
646
|
```
|
|
649
647
|
|
|
650
|
-
WARNING: **Limitation with positional arguments.**
|
|
648
|
+
WARNING: **Limitation with positional arguments.**
|
|
651
649
|
When passing positional arguments,
|
|
652
650
|
make sure there is no overlap between other duties' names
|
|
653
651
|
and the argument value, otherwise `duty` will not be able
|
|
@@ -713,7 +711,7 @@ def play(ctx, file):
|
|
|
713
711
|
```bash
|
|
714
712
|
duty --capture=none --strict play this-file.mp4
|
|
715
713
|
# or with the short options
|
|
716
|
-
duty -Zc none play this-file.mp4
|
|
714
|
+
duty -Zc none play this-file.mp4
|
|
717
715
|
```
|
|
718
716
|
|
|
719
717
|
#### Local options
|
|
@@ -724,7 +722,7 @@ you can pass them to a specific duty on the command line.
|
|
|
724
722
|
If we use the previous example again:
|
|
725
723
|
|
|
726
724
|
```bash
|
|
727
|
-
duty play -Zc none this-file.mp4
|
|
725
|
+
duty play -Zc none this-file.mp4
|
|
728
726
|
```
|
|
729
727
|
|
|
730
728
|
It allows to use different options for different duties
|
|
@@ -778,6 +776,8 @@ It is not possible to capture only stdout, or only stderr,
|
|
|
778
776
|
and let the other one be printed to the console.
|
|
779
777
|
Capturing one is capturing both, but discarding the other.
|
|
780
778
|
|
|
779
|
+
WARNING: **Windows quirks.** On Windows you might need to set the following environment variables to allow proper output capture: `PYTHONLEGACYWINDOWSSTDIO=1`, `PYTHONUTF8=1`, `PYTHONIOENCODING=UTF8`. If the `✓` and `✗` characters are mangled, try changing them by [customizing the output format](#formatting-duty-output).
|
|
780
|
+
|
|
781
781
|
### Formatting duty output
|
|
782
782
|
|
|
783
783
|
Thanks to its underlying [`failprint`](https://github.com/pawamoy/failprint) dependency,
|
|
@@ -789,7 +789,7 @@ For example, the two builtin `failprint` formats are:
|
|
|
789
789
|
```jinja
|
|
790
790
|
{% if success %}<green>✓</green>
|
|
791
791
|
{% elif nofail %}<yellow>✗</yellow>
|
|
792
|
-
{% else %}<red>✗</red>{% endif %}
|
|
792
|
+
{% else %}<red>✗</red>{% endif %}
|
|
793
793
|
<bold>{{ title or command }}</bold>
|
|
794
794
|
{% if failure %} ({{ code }}){% endif %}
|
|
795
795
|
{% if failure and output and not quiet %}\n
|
|
@@ -871,4 +871,16 @@ export FAILPRINT_FORMAT="custom={{output}}"
|
|
|
871
871
|
# always print the captured output, nothing else
|
|
872
872
|
|
|
873
873
|
duty task1 task2
|
|
874
|
-
```
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
### Shell completions
|
|
877
|
+
|
|
878
|
+
You can enable auto-completion in Bash with these commands:
|
|
879
|
+
|
|
880
|
+
```bash
|
|
881
|
+
completions_dir="${BASH_COMPLETION_USER_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion}/completions"
|
|
882
|
+
mkdir -p "${completions_dir}"
|
|
883
|
+
duty --completion > "${completions_dir}/duty"
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
Only Bash is supported for now.
|
|
@@ -84,6 +84,7 @@ def check_docs(ctx: Context) -> None:
|
|
|
84
84
|
@duty
|
|
85
85
|
def check_types(ctx: Context) -> None:
|
|
86
86
|
"""Check that the code is correctly typed."""
|
|
87
|
+
os.environ["FORCE_COLOR"] = "1"
|
|
87
88
|
ctx.run(
|
|
88
89
|
tools.mypy(*PY_SRC_LIST, config_file="config/mypy.ini"),
|
|
89
90
|
title=pyprefix("Type-checking"),
|
|
@@ -41,7 +41,7 @@ dependencies = [
|
|
|
41
41
|
"failprint>=0.11,!=1.0.0",
|
|
42
42
|
"typing-extensions>=4.0; python_version < '3.11'",
|
|
43
43
|
]
|
|
44
|
-
version = "1.
|
|
44
|
+
version = "1.6.0"
|
|
45
45
|
|
|
46
46
|
[project.license]
|
|
47
47
|
text = "ISC"
|
|
@@ -60,11 +60,10 @@ Funding = "https://github.com/sponsors/pawamoy"
|
|
|
60
60
|
duty = "duty.cli:main"
|
|
61
61
|
|
|
62
62
|
[tool.pdm.version]
|
|
63
|
-
source = "
|
|
63
|
+
source = "call"
|
|
64
|
+
getter = "scripts.get_version:get_version"
|
|
64
65
|
|
|
65
66
|
[tool.pdm.build]
|
|
66
|
-
package-dir = "src"
|
|
67
|
-
editable-backend = "editables"
|
|
68
67
|
excludes = [
|
|
69
68
|
"**/.pytest_cache",
|
|
70
69
|
]
|
|
@@ -85,9 +84,8 @@ data = [
|
|
|
85
84
|
{ path = "share/**/*", relative-to = "." },
|
|
86
85
|
]
|
|
87
86
|
|
|
88
|
-
[
|
|
89
|
-
dev
|
|
90
|
-
"editables>=0.5",
|
|
87
|
+
[dependency-groups]
|
|
88
|
+
dev = [
|
|
91
89
|
"build>=1.2",
|
|
92
90
|
"git-changelog>=2.5",
|
|
93
91
|
"twine>=5.1",
|
|
@@ -27,7 +27,7 @@ with project_dir.joinpath("pyproject.toml").open("rb") as pyproject_file:
|
|
|
27
27
|
pyproject = tomllib.load(pyproject_file)
|
|
28
28
|
project = pyproject["project"]
|
|
29
29
|
project_name = project["name"]
|
|
30
|
-
devdeps = [dep for dep in pyproject["
|
|
30
|
+
devdeps = [dep for dep in pyproject["dependency-groups"]["dev"] if not dep.startswith("-e")]
|
|
31
31
|
|
|
32
32
|
PackageMetadata = dict[str, Union[str, Iterable[str]]]
|
|
33
33
|
Metadata = dict[str, PackageMetadata]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Get current project version from Git tags or changelog."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from pdm.backend.hooks.version import SCMVersion, Version, default_version_formatter, get_version_from_scm
|
|
8
|
+
|
|
9
|
+
_root = Path(__file__).parent.parent
|
|
10
|
+
_changelog = _root / "CHANGELOG.md"
|
|
11
|
+
_changelog_version_re = re.compile(r"^## \[(\d+\.\d+\.\d+)\].*$")
|
|
12
|
+
_default_scm_version = SCMVersion(Version("0.0.0"), None, False, None, None) # noqa: FBT003
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_version() -> str:
|
|
16
|
+
"""Get current project version from Git tags or changelog."""
|
|
17
|
+
scm_version = get_version_from_scm(_root) or _default_scm_version
|
|
18
|
+
if scm_version.version <= Version("0.1"): # Missing Git tags?
|
|
19
|
+
with suppress(OSError, StopIteration): # noqa: SIM117
|
|
20
|
+
with _changelog.open("r", encoding="utf8") as file:
|
|
21
|
+
match = next(filter(None, map(_changelog_version_re.match, file)))
|
|
22
|
+
scm_version = scm_version._replace(version=Version(match.group(1)))
|
|
23
|
+
return default_version_formatter(scm_version)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
print(get_version())
|
duty-1.6.0/scripts/make
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
make.py
|
|
@@ -10,12 +10,16 @@ import sys
|
|
|
10
10
|
from contextlib import contextmanager
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from textwrap import dedent
|
|
13
|
-
from typing import
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from collections.abc import Iterator
|
|
17
|
+
|
|
14
18
|
|
|
15
19
|
PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.9 3.10 3.11 3.12 3.13 3.14").split()
|
|
16
20
|
|
|
17
21
|
|
|
18
|
-
def shell(cmd: str, capture_output: bool = False, **kwargs: Any) -> str | None:
|
|
22
|
+
def shell(cmd: str, *, capture_output: bool = False, **kwargs: Any) -> str | None:
|
|
19
23
|
"""Run a shell command."""
|
|
20
24
|
if capture_output:
|
|
21
25
|
return subprocess.check_output(cmd, shell=True, text=True, **kwargs) # noqa: S602
|
|
@@ -49,15 +53,15 @@ def setup() -> None:
|
|
|
49
53
|
if not shutil.which("uv"):
|
|
50
54
|
raise ValueError("make: setup: uv must be installed, see https://github.com/astral-sh/uv")
|
|
51
55
|
|
|
52
|
-
print("Installing dependencies (default environment)")
|
|
56
|
+
print("Installing dependencies (default environment)")
|
|
53
57
|
default_venv = Path(".venv")
|
|
54
58
|
if not default_venv.exists():
|
|
55
|
-
shell("uv venv
|
|
59
|
+
shell("uv venv")
|
|
56
60
|
uv_install(default_venv)
|
|
57
61
|
|
|
58
62
|
if PYTHON_VERSIONS:
|
|
59
63
|
for version in PYTHON_VERSIONS:
|
|
60
|
-
print(f"\nInstalling dependencies (python{version})")
|
|
64
|
+
print(f"\nInstalling dependencies (python{version})")
|
|
61
65
|
venv_path = Path(f".venvs/{version}")
|
|
62
66
|
if not venv_path.exists():
|
|
63
67
|
shell(f"uv venv --python {version} {venv_path}")
|
|
@@ -65,12 +69,10 @@ def setup() -> None:
|
|
|
65
69
|
uv_install(venv_path)
|
|
66
70
|
|
|
67
71
|
|
|
68
|
-
def run(version: str, cmd: str, *args: str,
|
|
72
|
+
def run(version: str, cmd: str, *args: str, **kwargs: Any) -> None:
|
|
69
73
|
"""Run a command in a virtual environment."""
|
|
70
74
|
kwargs = {"check": True, **kwargs}
|
|
71
|
-
uv_run = ["uv", "run"]
|
|
72
|
-
if no_sync:
|
|
73
|
-
uv_run.append("--no-sync")
|
|
75
|
+
uv_run = ["uv", "run", "--no-sync"]
|
|
74
76
|
if version == "default":
|
|
75
77
|
with environ(UV_PROJECT_ENVIRONMENT=".venv"):
|
|
76
78
|
subprocess.run([*uv_run, cmd, *args], **kwargs) # noqa: S603, PLW1510
|
|
@@ -99,7 +101,7 @@ def clean() -> None:
|
|
|
99
101
|
"""Delete build artifacts and cache files."""
|
|
100
102
|
paths_to_clean = ["build", "dist", "htmlcov", "site", ".coverage*", ".pdm-build"]
|
|
101
103
|
for path in paths_to_clean:
|
|
102
|
-
|
|
104
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
103
105
|
|
|
104
106
|
cache_dirs = {".cache", ".pytest_cache", ".mypy_cache", ".ruff_cache", "__pycache__"}
|
|
105
107
|
for dirpath in Path(".").rglob("*/"):
|
|
@@ -109,8 +111,7 @@ def clean() -> None:
|
|
|
109
111
|
|
|
110
112
|
def vscode() -> None:
|
|
111
113
|
"""Configure VSCode to work on this project."""
|
|
112
|
-
|
|
113
|
-
shell("cp -v config/vscode/* .vscode")
|
|
114
|
+
shutil.copytree("config/vscode", ".vscode", dirs_exist_ok=True)
|
|
114
115
|
|
|
115
116
|
|
|
116
117
|
def main() -> int:
|
|
@@ -132,13 +133,13 @@ def main() -> int:
|
|
|
132
133
|
3.x Run a command in the virtual environment for Python 3.x.
|
|
133
134
|
clean Delete build artifacts and cache files.
|
|
134
135
|
vscode Configure VSCode to work on this project.
|
|
135
|
-
"""
|
|
136
|
+
""",
|
|
136
137
|
),
|
|
137
138
|
flush=True,
|
|
138
|
-
)
|
|
139
|
+
)
|
|
139
140
|
if os.path.exists(".venv"):
|
|
140
|
-
print("\nAvailable tasks", flush=True)
|
|
141
|
-
run("default", "duty", "--list"
|
|
141
|
+
print("\nAvailable tasks", flush=True)
|
|
142
|
+
run("default", "duty", "--list")
|
|
142
143
|
return 0
|
|
143
144
|
|
|
144
145
|
while args:
|
|
@@ -186,5 +187,5 @@ if __name__ == "__main__":
|
|
|
186
187
|
sys.exit(main())
|
|
187
188
|
except subprocess.CalledProcessError as process:
|
|
188
189
|
if process.output:
|
|
189
|
-
print(process.output, file=sys.stderr)
|
|
190
|
+
print(process.output, file=sys.stderr)
|
|
190
191
|
sys.exit(process.returncode)
|
|
@@ -17,6 +17,7 @@ import argparse
|
|
|
17
17
|
import inspect
|
|
18
18
|
import sys
|
|
19
19
|
import textwrap
|
|
20
|
+
from pathlib import Path
|
|
20
21
|
from typing import Any
|
|
21
22
|
|
|
22
23
|
from failprint.cli import ArgParser, add_flags
|
|
@@ -69,6 +70,18 @@ def get_parser() -> ArgParser:
|
|
|
69
70
|
metavar="DUTY",
|
|
70
71
|
help="Show this help message and exit. Pass duties names to print their help.",
|
|
71
72
|
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--completion",
|
|
75
|
+
dest="completion",
|
|
76
|
+
action="store_true",
|
|
77
|
+
help=argparse.SUPPRESS,
|
|
78
|
+
)
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"--complete",
|
|
81
|
+
dest="complete",
|
|
82
|
+
action="store_true",
|
|
83
|
+
help=argparse.SUPPRESS,
|
|
84
|
+
)
|
|
72
85
|
parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {debug.get_version()}")
|
|
73
86
|
parser.add_argument("--debug-info", action=_DebugInfo, help="Print debug information.")
|
|
74
87
|
|
|
@@ -261,6 +274,18 @@ def main(args: list[str] | None = None) -> int:
|
|
|
261
274
|
collection = Collection(opts.duties_file)
|
|
262
275
|
collection.load()
|
|
263
276
|
|
|
277
|
+
if opts.completion:
|
|
278
|
+
print(Path(__file__).parent.joinpath("completions.bash").read_text())
|
|
279
|
+
return 0
|
|
280
|
+
|
|
281
|
+
if opts.complete:
|
|
282
|
+
words = collection.completion_candidates(remainder)
|
|
283
|
+
words += sorted(
|
|
284
|
+
opt for opt, action in parser._option_string_actions.items() if action.help != argparse.SUPPRESS
|
|
285
|
+
)
|
|
286
|
+
print(*words, sep="\n")
|
|
287
|
+
return 0
|
|
288
|
+
|
|
264
289
|
if opts.help is not None:
|
|
265
290
|
print_help(parser, opts, collection)
|
|
266
291
|
return 0
|
|
@@ -279,7 +304,10 @@ def main(args: list[str] | None = None) -> int:
|
|
|
279
304
|
print_help(parser, opts, collection)
|
|
280
305
|
return 1
|
|
281
306
|
|
|
282
|
-
global_opts = specified_options(
|
|
307
|
+
global_opts = specified_options(
|
|
308
|
+
opts,
|
|
309
|
+
exclude={"duties_file", "list", "help", "remainder", "complete", "completion"},
|
|
310
|
+
)
|
|
283
311
|
try:
|
|
284
312
|
commands = parse_commands(arg_lists, global_opts, collection)
|
|
285
313
|
except TypeError as error:
|
|
@@ -143,6 +143,35 @@ class Collection:
|
|
|
143
143
|
"""
|
|
144
144
|
return list(self.duties.keys()) + list(self.aliases.keys())
|
|
145
145
|
|
|
146
|
+
def completion_candidates(self, args: tuple[str, ...]) -> list[str]:
|
|
147
|
+
"""Find shell completion candidates within this collection.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
The list of shell completion candidates, sorted alphabetically.
|
|
151
|
+
"""
|
|
152
|
+
# Find last duty name in args.
|
|
153
|
+
name = None
|
|
154
|
+
names = set(self.names())
|
|
155
|
+
for arg in reversed(args):
|
|
156
|
+
if arg in names:
|
|
157
|
+
name = arg
|
|
158
|
+
break
|
|
159
|
+
|
|
160
|
+
completion_names = sorted(names)
|
|
161
|
+
|
|
162
|
+
# If no duty found, return names.
|
|
163
|
+
if name is None:
|
|
164
|
+
return completion_names
|
|
165
|
+
|
|
166
|
+
params = [
|
|
167
|
+
f"{param.name}="
|
|
168
|
+
for param in inspect.signature(self.get(name).function).parameters.values()
|
|
169
|
+
if param.kind is not param.VAR_POSITIONAL
|
|
170
|
+
][1:]
|
|
171
|
+
|
|
172
|
+
# If duty found, return names *and* duty parameters.
|
|
173
|
+
return completion_names + sorted(params)
|
|
174
|
+
|
|
146
175
|
def get(self, name_or_alias: str) -> Duty:
|
|
147
176
|
"""Get a duty by its name or alias.
|
|
148
177
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Taken and adapted from pyinvoke:
|
|
2
|
+
# Copyright (c) 2020 Jeff Forcier.
|
|
3
|
+
# All rights reserved.
|
|
4
|
+
|
|
5
|
+
_complete_duty() {
|
|
6
|
+
local candidates
|
|
7
|
+
|
|
8
|
+
# COMP_WORDS contains the entire command string up til now (including # program name).
|
|
9
|
+
# We hand it to Invoke so it can figure out the current context:
|
|
10
|
+
# spit back core options, task names, the current task's options, or some combo.
|
|
11
|
+
candidates=$(duty --complete -- "${COMP_WORDS[@]}")
|
|
12
|
+
|
|
13
|
+
# `compgen -W` takes list of valid options & a partial word & spits back possible matches.
|
|
14
|
+
# Necessary for any partial word completions
|
|
15
|
+
# (vs. completions performed when no partial words are present).
|
|
16
|
+
#
|
|
17
|
+
# $2 is the current word or token being tabbed on, either empty string or a
|
|
18
|
+
# partial word, and thus wants to be compgen'd to arrive at some subset of
|
|
19
|
+
# our candidate list which actually matches.
|
|
20
|
+
#
|
|
21
|
+
# COMPREPLY is the list of valid completions handed back to `complete`.
|
|
22
|
+
COMPREPLY=( $(compgen -W "${candidates}" -- $2) )
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Tell shell builtin to use the above for completing our invocations.
|
|
27
|
+
# * -F: use given function name to generate completions.
|
|
28
|
+
# * -o default: when function generates no results, use filenames.
|
|
29
|
+
# * positional args: program names to complete for.
|
|
30
|
+
complete -F _complete_duty -o default duty
|
|
@@ -22,12 +22,12 @@ from duty.tools._ruff import ruff
|
|
|
22
22
|
from duty.tools._safety import safety
|
|
23
23
|
from duty.tools._ssort import ssort
|
|
24
24
|
from duty.tools._twine import twine
|
|
25
|
+
from duty.tools._yore import yore
|
|
25
26
|
|
|
26
27
|
__all__ = [
|
|
27
|
-
"Tool",
|
|
28
|
-
"LazyStdout",
|
|
29
28
|
"LazyStderr",
|
|
30
|
-
"
|
|
29
|
+
"LazyStdout",
|
|
30
|
+
"Tool",
|
|
31
31
|
"autoflake",
|
|
32
32
|
"black",
|
|
33
33
|
"blacken_docs",
|
|
@@ -38,6 +38,7 @@ __all__ = [
|
|
|
38
38
|
"griffe",
|
|
39
39
|
"interrogate",
|
|
40
40
|
"isort",
|
|
41
|
+
"lazy",
|
|
41
42
|
"mkdocs",
|
|
42
43
|
"mypy",
|
|
43
44
|
"pytest",
|
|
@@ -45,4 +46,5 @@ __all__ = [
|
|
|
45
46
|
"safety",
|
|
46
47
|
"ssort",
|
|
47
48
|
"twine",
|
|
49
|
+
"yore",
|
|
48
50
|
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Callable for [Yore](https://github.com/pawamoy/yore)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from duty.tools._base import Tool
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class yore(Tool): # noqa: N801
|
|
9
|
+
"""Call [Yore](https://github.com/pawamoy/yore)."""
|
|
10
|
+
|
|
11
|
+
cli_name = "yore"
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def check(
|
|
15
|
+
cls,
|
|
16
|
+
*paths: str,
|
|
17
|
+
bump: str | None = None,
|
|
18
|
+
eol_within: str | None = None,
|
|
19
|
+
bol_within: str | None = None,
|
|
20
|
+
) -> yore:
|
|
21
|
+
"""Checks Yore comments against Python EOL dates or the provided next version of your project.
|
|
22
|
+
|
|
23
|
+
Parameters:
|
|
24
|
+
paths: Path to files or directories to check.
|
|
25
|
+
bump: The next version of your project.
|
|
26
|
+
eol_within: The time delta to start checking before the End of Life of a Python version.
|
|
27
|
+
It is provided in a human-readable format, like `2 weeks` or `1 month`.
|
|
28
|
+
Spaces are optional, and the unit can be shortened to a single letter:
|
|
29
|
+
`d` for days, `w` for weeks, `m` for months, and `y` for years.
|
|
30
|
+
bol_within: The time delta to start checking before the Beginning of Life of a Python version.
|
|
31
|
+
It is provided in a human-readable format, like `2 weeks` or `1 month`.
|
|
32
|
+
Spaces are optional, and the unit can be shortened to a single letter:
|
|
33
|
+
`d` for days, `w` for weeks, `m` for months, and `y` for years.
|
|
34
|
+
"""
|
|
35
|
+
cli_args = ["check", *paths]
|
|
36
|
+
|
|
37
|
+
if bump:
|
|
38
|
+
cli_args.append("--bump")
|
|
39
|
+
cli_args.append(bump)
|
|
40
|
+
|
|
41
|
+
if eol_within:
|
|
42
|
+
cli_args.append("--eol-within")
|
|
43
|
+
cli_args.append(eol_within)
|
|
44
|
+
|
|
45
|
+
if bol_within:
|
|
46
|
+
cli_args.append("--bol-within")
|
|
47
|
+
cli_args.append(bol_within)
|
|
48
|
+
|
|
49
|
+
return cls(cli_args)
|
|
50
|
+
|
|
51
|
+
def __call__(self) -> int:
|
|
52
|
+
from yore import main as run_yore
|
|
53
|
+
|
|
54
|
+
return run_yore(self.cli_args)
|
|
@@ -68,3 +68,19 @@ def test_add_duty_to_multiple_collections() -> None:
|
|
|
68
68
|
assert duty1 is not duty2
|
|
69
69
|
assert duty1.collection is collection1
|
|
70
70
|
assert duty2.collection is collection2
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_completion_candidates() -> None:
|
|
74
|
+
"""Check whether proper completion candidates are returned from collections."""
|
|
75
|
+
collection = Collection()
|
|
76
|
+
|
|
77
|
+
collection.add(decorate(none, name="duty_1")) # type: ignore[call-overload]
|
|
78
|
+
collection.add(decorate(none, name="duty_2", aliases=["alias_2"])) # type: ignore[call-overload]
|
|
79
|
+
|
|
80
|
+
assert collection.completion_candidates(("duty",)) == [
|
|
81
|
+
"alias_2",
|
|
82
|
+
"duty-1",
|
|
83
|
+
"duty-2",
|
|
84
|
+
"duty_1",
|
|
85
|
+
"duty_2",
|
|
86
|
+
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|