duty 1.4.2__tar.gz → 1.5.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.
Files changed (102) hide show
  1. {duty-1.4.2 → duty-1.5.0}/CHANGELOG.md +16 -0
  2. {duty-1.4.2 → duty-1.5.0}/CONTRIBUTING.md +2 -3
  3. {duty-1.4.2 → duty-1.5.0}/PKG-INFO +5 -9
  4. {duty-1.4.2 → duty-1.5.0}/README.md +2 -6
  5. {duty-1.4.2 → duty-1.5.0}/config/ruff.toml +1 -1
  6. {duty-1.4.2 → duty-1.5.0}/docs/index.md +1 -1
  7. {duty-1.4.2 → duty-1.5.0}/docs/usage.md +19 -5
  8. {duty-1.4.2 → duty-1.5.0}/duties.py +5 -2
  9. {duty-1.4.2 → duty-1.5.0}/mkdocs.yml +4 -2
  10. {duty-1.4.2 → duty-1.5.0}/pyproject.toml +33 -7
  11. {duty-1.4.2 → duty-1.5.0}/scripts/gen_credits.py +6 -6
  12. duty-1.5.0/scripts/get_version.py +27 -0
  13. duty-1.5.0/scripts/make +1 -0
  14. duty-1.4.2/scripts/make → duty-1.5.0/scripts/make.py +49 -68
  15. {duty-1.4.2 → duty-1.5.0}/src/duty/callables/blacken_docs.py +5 -1
  16. {duty-1.4.2 → duty-1.5.0}/src/duty/callables/ruff.py +2 -2
  17. {duty-1.4.2 → duty-1.5.0}/src/duty/callables/safety.py +4 -1
  18. {duty-1.4.2 → duty-1.5.0}/src/duty/cli.py +29 -1
  19. {duty-1.4.2 → duty-1.5.0}/src/duty/collection.py +31 -2
  20. duty-1.5.0/src/duty/completions.bash +30 -0
  21. {duty-1.4.2 → duty-1.5.0}/src/duty/context.py +5 -2
  22. {duty-1.4.2 → duty-1.5.0}/src/duty/decorator.py +3 -1
  23. {duty-1.4.2 → duty-1.5.0}/src/duty/tools/_blacken_docs.py +5 -1
  24. {duty-1.4.2 → duty-1.5.0}/src/duty/tools/_ruff.py +2 -2
  25. {duty-1.4.2 → duty-1.5.0}/src/duty/tools/_safety.py +4 -1
  26. {duty-1.4.2 → duty-1.5.0}/src/duty/validation.py +5 -2
  27. {duty-1.4.2 → duty-1.5.0}/tests/test_collection.py +16 -0
  28. {duty-1.4.2 → duty-1.5.0}/tests/test_context.py +5 -1
  29. duty-1.4.2/devdeps.txt +0 -32
  30. {duty-1.4.2 → duty-1.5.0}/CODE_OF_CONDUCT.md +0 -0
  31. {duty-1.4.2 → duty-1.5.0}/LICENSE +0 -0
  32. {duty-1.4.2 → duty-1.5.0}/config/coverage.ini +0 -0
  33. {duty-1.4.2 → duty-1.5.0}/config/git-changelog.toml +0 -0
  34. {duty-1.4.2 → duty-1.5.0}/config/mypy.ini +0 -0
  35. {duty-1.4.2 → duty-1.5.0}/config/pytest.ini +0 -0
  36. {duty-1.4.2 → duty-1.5.0}/config/vscode/launch.json +0 -0
  37. {duty-1.4.2 → duty-1.5.0}/config/vscode/settings.json +0 -0
  38. {duty-1.4.2 → duty-1.5.0}/config/vscode/tasks.json +0 -0
  39. {duty-1.4.2 → duty-1.5.0}/docs/.overrides/main.html +0 -0
  40. {duty-1.4.2 → duty-1.5.0}/docs/.overrides/partials/comments.html +0 -0
  41. {duty-1.4.2 → duty-1.5.0}/docs/changelog.md +0 -0
  42. {duty-1.4.2 → duty-1.5.0}/docs/code_of_conduct.md +0 -0
  43. {duty-1.4.2 → duty-1.5.0}/docs/contributing.md +0 -0
  44. {duty-1.4.2 → duty-1.5.0}/docs/credits.md +0 -0
  45. {duty-1.4.2 → duty-1.5.0}/docs/css/material.css +0 -0
  46. {duty-1.4.2 → duty-1.5.0}/docs/css/mkdocstrings.css +0 -0
  47. {duty-1.4.2 → duty-1.5.0}/docs/demo.svg +0 -0
  48. {duty-1.4.2 → duty-1.5.0}/docs/gen_credits.py +0 -0
  49. {duty-1.4.2 → duty-1.5.0}/docs/js/feedback.js +0 -0
  50. {duty-1.4.2 → duty-1.5.0}/docs/license.md +0 -0
  51. {duty-1.4.2 → duty-1.5.0}/scripts/gen_ref_nav.py +0 -0
  52. {duty-1.4.2 → duty-1.5.0}/src/duty/__init__.py +0 -0
  53. {duty-1.4.2 → duty-1.5.0}/src/duty/__main__.py +0 -0
  54. {duty-1.4.2 → duty-1.5.0}/src/duty/callables/__init__.py +0 -0
  55. {duty-1.4.2 → duty-1.5.0}/src/duty/callables/_io.py +0 -0
  56. {duty-1.4.2 → duty-1.5.0}/src/duty/callables/autoflake.py +0 -0
  57. {duty-1.4.2 → duty-1.5.0}/src/duty/callables/black.py +0 -0
  58. {duty-1.4.2 → duty-1.5.0}/src/duty/callables/build.py +0 -0
  59. {duty-1.4.2 → duty-1.5.0}/src/duty/callables/coverage.py +0 -0
  60. {duty-1.4.2 → duty-1.5.0}/src/duty/callables/flake8.py +0 -0
  61. {duty-1.4.2 → duty-1.5.0}/src/duty/callables/git_changelog.py +0 -0
  62. {duty-1.4.2 → duty-1.5.0}/src/duty/callables/griffe.py +0 -0
  63. {duty-1.4.2 → duty-1.5.0}/src/duty/callables/interrogate.py +0 -0
  64. {duty-1.4.2 → duty-1.5.0}/src/duty/callables/isort.py +0 -0
  65. {duty-1.4.2 → duty-1.5.0}/src/duty/callables/mkdocs.py +0 -0
  66. {duty-1.4.2 → duty-1.5.0}/src/duty/callables/mypy.py +0 -0
  67. {duty-1.4.2 → duty-1.5.0}/src/duty/callables/pytest.py +0 -0
  68. {duty-1.4.2 → duty-1.5.0}/src/duty/callables/ssort.py +0 -0
  69. {duty-1.4.2 → duty-1.5.0}/src/duty/callables/twine.py +0 -0
  70. {duty-1.4.2 → duty-1.5.0}/src/duty/debug.py +0 -0
  71. {duty-1.4.2 → duty-1.5.0}/src/duty/exceptions.py +0 -0
  72. {duty-1.4.2 → duty-1.5.0}/src/duty/py.typed +0 -0
  73. {duty-1.4.2 → duty-1.5.0}/src/duty/tools/__init__.py +3 -3
  74. {duty-1.4.2 → duty-1.5.0}/src/duty/tools/_autoflake.py +0 -0
  75. {duty-1.4.2 → duty-1.5.0}/src/duty/tools/_base.py +0 -0
  76. {duty-1.4.2 → duty-1.5.0}/src/duty/tools/_black.py +0 -0
  77. {duty-1.4.2 → duty-1.5.0}/src/duty/tools/_build.py +0 -0
  78. {duty-1.4.2 → duty-1.5.0}/src/duty/tools/_coverage.py +0 -0
  79. {duty-1.4.2 → duty-1.5.0}/src/duty/tools/_flake8.py +0 -0
  80. {duty-1.4.2 → duty-1.5.0}/src/duty/tools/_git_changelog.py +0 -0
  81. {duty-1.4.2 → duty-1.5.0}/src/duty/tools/_griffe.py +0 -0
  82. {duty-1.4.2 → duty-1.5.0}/src/duty/tools/_interrogate.py +0 -0
  83. {duty-1.4.2 → duty-1.5.0}/src/duty/tools/_isort.py +0 -0
  84. {duty-1.4.2 → duty-1.5.0}/src/duty/tools/_mkdocs.py +0 -0
  85. {duty-1.4.2 → duty-1.5.0}/src/duty/tools/_mypy.py +0 -0
  86. {duty-1.4.2 → duty-1.5.0}/src/duty/tools/_pytest.py +0 -0
  87. {duty-1.4.2 → duty-1.5.0}/src/duty/tools/_ssort.py +0 -0
  88. {duty-1.4.2 → duty-1.5.0}/src/duty/tools/_twine.py +0 -0
  89. {duty-1.4.2 → duty-1.5.0}/tests/__init__.py +0 -0
  90. {duty-1.4.2 → duty-1.5.0}/tests/conftest.py +0 -0
  91. {duty-1.4.2 → duty-1.5.0}/tests/fixtures/arguments.py +0 -0
  92. {duty-1.4.2 → duty-1.5.0}/tests/fixtures/basic.py +0 -0
  93. {duty-1.4.2 → duty-1.5.0}/tests/fixtures/booleans.py +0 -0
  94. {duty-1.4.2 → duty-1.5.0}/tests/fixtures/code.py +0 -0
  95. {duty-1.4.2 → duty-1.5.0}/tests/fixtures/list.py +0 -0
  96. {duty-1.4.2 → duty-1.5.0}/tests/fixtures/multiple.py +0 -0
  97. {duty-1.4.2 → duty-1.5.0}/tests/fixtures/precedence.py +0 -0
  98. {duty-1.4.2 → duty-1.5.0}/tests/fixtures/validation.py +0 -0
  99. {duty-1.4.2 → duty-1.5.0}/tests/test_cli.py +0 -0
  100. {duty-1.4.2 → duty-1.5.0}/tests/test_decorator.py +0 -0
  101. {duty-1.4.2 → duty-1.5.0}/tests/test_running.py +0 -0
  102. {duty-1.4.2 → duty-1.5.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.5.0](https://github.com/pawamoy/duty/releases/tag/1.5.0) - 2025-02-02
9
+
10
+ <small>[Compare with 1.4.3](https://github.com/pawamoy/duty/compare/1.4.3...1.5.0)</small>
11
+
12
+ ### Features
13
+
14
+ - 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>
15
+
16
+ ## [1.4.3](https://github.com/pawamoy/duty/releases/tag/1.4.3) - 2024-10-17
17
+
18
+ <small>[Compare with 1.4.2](https://github.com/pawamoy/duty/compare/1.4.2...1.4.3)</small>
19
+
20
+ ### Build
21
+
22
+ - Drop support for Python 3.8 ([4f5d6ec](https://github.com/pawamoy/duty/commit/4f5d6ecbb0a84e5c42cab4d584239f16e8397d86) by Timothée Mazzucotelli).
23
+
8
24
  ## [1.4.2](https://github.com/pawamoy/duty/releases/tag/1.4.2) - 2024-09-10
9
25
 
10
26
  <small>[Compare with 1.4.1](https://github.com/pawamoy/duty/compare/1.4.1...1.4.2)</small>
@@ -23,12 +23,11 @@ make setup
23
23
  > You can install it with:
24
24
  >
25
25
  > ```bash
26
- > python3 -m pip install --user pipx
27
- > pipx install uv
26
+ > curl -LsSf https://astral.sh/uv/install.sh | sh
28
27
  > ```
29
28
  >
30
29
  > Now you can try running `make setup` again,
31
- > or simply `uv install`.
30
+ > or simply `uv sync`.
32
31
 
33
32
  You now have the dependencies installed.
34
33
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: duty
3
- Version: 1.4.2
3
+ Version: 1.5.0
4
4
  Summary: A simple task runner.
5
5
  Keywords: task-runner,task,runner,cross-platform
6
6
  Author-Email: =?utf-8?q?Timoth=C3=A9e_Mazzucotelli?= <dev@pawamoy.fr>
@@ -10,12 +10,12 @@ Classifier: Intended Audience :: Developers
10
10
  Classifier: Programming Language :: Python
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3 :: Only
13
- Classifier: Programming Language :: Python :: 3.8
14
13
  Classifier: Programming Language :: Python :: 3.9
15
14
  Classifier: Programming Language :: Python :: 3.10
16
15
  Classifier: Programming Language :: Python :: 3.11
17
16
  Classifier: Programming Language :: Python :: 3.12
18
17
  Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
19
  Classifier: Topic :: Documentation
20
20
  Classifier: Topic :: Software Development
21
21
  Classifier: Topic :: Utilities
@@ -28,7 +28,7 @@ Project-URL: Issues, https://github.com/pawamoy/duty/issues
28
28
  Project-URL: Discussions, https://github.com/pawamoy/duty/discussions
29
29
  Project-URL: Gitter, https://gitter.im/duty/community
30
30
  Project-URL: Funding, https://github.com/sponsors/pawamoy
31
- Requires-Python: >=3.8
31
+ Requires-Python: >=3.9
32
32
  Requires-Dist: eval-type-backport; python_version < "3.10"
33
33
  Requires-Dist: failprint!=1.0.0,>=0.11
34
34
  Requires-Dist: typing-extensions>=4.0; python_version < "3.11"
@@ -39,7 +39,6 @@ Description-Content-Type: text/markdown
39
39
  [![ci](https://github.com/pawamoy/duty/workflows/ci/badge.svg)](https://github.com/pawamoy/duty/actions?query=workflow%3Aci)
40
40
  [![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)](https://pawamoy.github.io/duty/)
41
41
  [![pypi version](https://img.shields.io/pypi/v/duty.svg)](https://pypi.org/project/duty/)
42
- [![gitpod](https://img.shields.io/badge/gitpod-workspace-708FCC.svg?style=flat)](https://gitpod.io/#https://github.com/pawamoy/duty)
43
42
  [![gitter](https://badges.gitter.im/join%20chat.svg)](https://app.gitter.im/#/room/#duty:gitter.im)
44
43
 
45
44
  A simple task runner.
@@ -50,17 +49,14 @@ Inspired by [Invoke](https://github.com/pyinvoke/invoke).
50
49
 
51
50
  ## Installation
52
51
 
53
- With `pip`:
54
-
55
52
  ```bash
56
53
  pip install duty
57
54
  ```
58
55
 
59
- With [`pipx`](https://github.com/pipxproject/pipx):
56
+ With [`uv`](https://docs.astral.sh/uv/):
60
57
 
61
58
  ```bash
62
- python3.8 -m pip install --user pipx
63
- pipx install duty
59
+ uv tool install duty
64
60
  ```
65
61
 
66
62
  ## Quick start
@@ -3,7 +3,6 @@
3
3
  [![ci](https://github.com/pawamoy/duty/workflows/ci/badge.svg)](https://github.com/pawamoy/duty/actions?query=workflow%3Aci)
4
4
  [![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)](https://pawamoy.github.io/duty/)
5
5
  [![pypi version](https://img.shields.io/pypi/v/duty.svg)](https://pypi.org/project/duty/)
6
- [![gitpod](https://img.shields.io/badge/gitpod-workspace-708FCC.svg?style=flat)](https://gitpod.io/#https://github.com/pawamoy/duty)
7
6
  [![gitter](https://badges.gitter.im/join%20chat.svg)](https://app.gitter.im/#/room/#duty:gitter.im)
8
7
 
9
8
  A simple task runner.
@@ -14,17 +13,14 @@ Inspired by [Invoke](https://github.com/pyinvoke/invoke).
14
13
 
15
14
  ## Installation
16
15
 
17
- With `pip`:
18
-
19
16
  ```bash
20
17
  pip install duty
21
18
  ```
22
19
 
23
- With [`pipx`](https://github.com/pipxproject/pipx):
20
+ With [`uv`](https://docs.astral.sh/uv/):
24
21
 
25
22
  ```bash
26
- python3.8 -m pip install --user pipx
27
- pipx install duty
23
+ uv tool install duty
28
24
  ```
29
25
 
30
26
  ## Quick start
@@ -1,4 +1,4 @@
1
- target-version = "py38"
1
+ target-version = "py39"
2
2
  line-length = 120
3
3
 
4
4
  [lint]
@@ -63,7 +63,7 @@ clean:
63
63
  ```
64
64
 
65
65
  See [our Makefile](https://github.com/pawamoy/duty/blob/main/Makefile)
66
- for inspiration.
66
+ for inspiration.
67
67
 
68
68
  ### [Task](https://taskfile.dev/)?
69
69
 
@@ -647,7 +647,7 @@ You can also pass parameters as positional arguments:
647
647
  duty shoot 5,15
648
648
  ```
649
649
 
650
- WARNING: **Limitation with positional arguments.**
650
+ WARNING: **Limitation with positional arguments.**
651
651
  When passing positional arguments,
652
652
  make sure there is no overlap between other duties' names
653
653
  and the argument value, otherwise `duty` will not be able
@@ -713,7 +713,7 @@ def play(ctx, file):
713
713
  ```bash
714
714
  duty --capture=none --strict play this-file.mp4
715
715
  # or with the short options
716
- duty -Zc none play this-file.mp4
716
+ duty -Zc none play this-file.mp4
717
717
  ```
718
718
 
719
719
  #### Local options
@@ -724,7 +724,7 @@ you can pass them to a specific duty on the command line.
724
724
  If we use the previous example again:
725
725
 
726
726
  ```bash
727
- duty play -Zc none this-file.mp4
727
+ duty play -Zc none this-file.mp4
728
728
  ```
729
729
 
730
730
  It allows to use different options for different duties
@@ -778,6 +778,8 @@ It is not possible to capture only stdout, or only stderr,
778
778
  and let the other one be printed to the console.
779
779
  Capturing one is capturing both, but discarding the other.
780
780
 
781
+ 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).
782
+
781
783
  ### Formatting duty output
782
784
 
783
785
  Thanks to its underlying [`failprint`](https://github.com/pawamoy/failprint) dependency,
@@ -789,7 +791,7 @@ For example, the two builtin `failprint` formats are:
789
791
  ```jinja
790
792
  {% if success %}<green>✓</green>
791
793
  {% elif nofail %}<yellow>✗</yellow>
792
- {% else %}<red>✗</red>{% endif %}
794
+ {% else %}<red>✗</red>{% endif %}
793
795
  <bold>{{ title or command }}</bold>
794
796
  {% if failure %} ({{ code }}){% endif %}
795
797
  {% if failure and output and not quiet %}\n
@@ -871,4 +873,16 @@ export FAILPRINT_FORMAT="custom={{output}}"
871
873
  # always print the captured output, nothing else
872
874
 
873
875
  duty task1 task2
874
- ```
876
+ ```
877
+
878
+ ### Shell completions
879
+
880
+ You can enable auto-completion in Bash with these commands:
881
+
882
+ ```bash
883
+ completions_dir="${BASH_COMPLETION_USER_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion}/completions"
884
+ mkdir -p "${completions_dir}"
885
+ duty --completion > "${completions_dir}/duty"
886
+ ```
887
+
888
+ Only Bash is supported for now.
@@ -7,11 +7,13 @@ import sys
7
7
  from contextlib import contextmanager
8
8
  from importlib.metadata import version as pkgversion
9
9
  from pathlib import Path
10
- from typing import TYPE_CHECKING, Iterator
10
+ from typing import TYPE_CHECKING
11
11
 
12
12
  from duty import duty, tools
13
13
 
14
14
  if TYPE_CHECKING:
15
+ from collections.abc import Iterator
16
+
15
17
  from duty.context import Context
16
18
 
17
19
 
@@ -53,7 +55,7 @@ def changelog(ctx: Context, bump: str = "") -> None:
53
55
  ctx.run(tools.git_changelog(bump=bump or None), title="Updating changelog")
54
56
 
55
57
 
56
- @duty(pre=["check_quality", "check_types", "check_docs", "check_dependencies", "check-api"])
58
+ @duty(pre=["check-quality", "check-types", "check-docs", "check-api"])
57
59
  def check(ctx: Context) -> None:
58
60
  """Check it all!"""
59
61
 
@@ -82,6 +84,7 @@ def check_docs(ctx: Context) -> None:
82
84
  @duty
83
85
  def check_types(ctx: Context) -> None:
84
86
  """Check that the code is correctly typed."""
87
+ os.environ["FORCE_COLOR"] = "1"
85
88
  ctx.run(
86
89
  tools.mypy(*PY_SRC_LIST, config_file="config/mypy.ini"),
87
90
  title=pyprefix("Type-checking"),
@@ -129,13 +129,15 @@ plugins:
129
129
  show_root_heading: true
130
130
  show_root_full_path: false
131
131
  show_signature_annotations: true
132
+ show_source: true
132
133
  show_symbol_type_heading: true
133
134
  show_symbol_type_toc: true
134
135
  signature_crossrefs: true
135
136
  summary: true
136
- - git-committers:
137
+ - git-revision-date-localized:
137
138
  enabled: !ENV [DEPLOY, false]
138
- repository: pawamoy/duty
139
+ enable_creation_date: true
140
+ type: timeago
139
141
  - minify:
140
142
  minify_html: !ENV [DEPLOY, false]
141
143
  - group:
@@ -11,7 +11,7 @@ authors = [
11
11
  { name = "Timothée Mazzucotelli", email = "dev@pawamoy.fr" },
12
12
  ]
13
13
  readme = "README.md"
14
- requires-python = ">=3.8"
14
+ requires-python = ">=3.9"
15
15
  keywords = [
16
16
  "task-runner",
17
17
  "task",
@@ -25,12 +25,12 @@ classifiers = [
25
25
  "Programming Language :: Python",
26
26
  "Programming Language :: Python :: 3",
27
27
  "Programming Language :: Python :: 3 :: Only",
28
- "Programming Language :: Python :: 3.8",
29
28
  "Programming Language :: Python :: 3.9",
30
29
  "Programming Language :: Python :: 3.10",
31
30
  "Programming Language :: Python :: 3.11",
32
31
  "Programming Language :: Python :: 3.12",
33
32
  "Programming Language :: Python :: 3.13",
33
+ "Programming Language :: Python :: 3.14",
34
34
  "Topic :: Documentation",
35
35
  "Topic :: Software Development",
36
36
  "Topic :: Utilities",
@@ -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.4.2"
44
+ version = "1.5.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 = "scm"
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
  ]
@@ -74,7 +73,6 @@ source-includes = [
74
73
  "scripts",
75
74
  "share",
76
75
  "tests",
77
- "devdeps.txt",
78
76
  "duties.py",
79
77
  "mkdocs.yml",
80
78
  "*.md",
@@ -85,3 +83,31 @@ source-includes = [
85
83
  data = [
86
84
  { path = "share/**/*", relative-to = "." },
87
85
  ]
86
+
87
+ [dependency-groups]
88
+ dev = [
89
+ "build>=1.2",
90
+ "git-changelog>=2.5",
91
+ "twine>=5.1",
92
+ "duty>=1.4",
93
+ "ruff>=0.4",
94
+ "pytest>=8.2",
95
+ "pytest-cov>=5.0",
96
+ "pytest-randomly>=3.15",
97
+ "pytest-xdist>=3.6",
98
+ "mypy>=1.10",
99
+ "types-markdown>=3.6",
100
+ "types-pyyaml>=6.0",
101
+ "black>=24.4",
102
+ "markdown-callouts>=0.4",
103
+ "markdown-exec>=1.8",
104
+ "mkdocs>=1.6",
105
+ "mkdocs-coverage>=1.0",
106
+ "mkdocs-gen-files>=0.5",
107
+ "mkdocs-git-revision-date-localized-plugin>=1.2",
108
+ "mkdocs-literate-nav>=0.6",
109
+ "mkdocs-material>=9.5",
110
+ "mkdocs-minify-plugin>=0.8",
111
+ "mkdocstrings[python]>=0.25",
112
+ "tomli>=2.0; python_version < '3.11'",
113
+ ]
@@ -5,17 +5,18 @@ from __future__ import annotations
5
5
  import os
6
6
  import sys
7
7
  from collections import defaultdict
8
+ from collections.abc import Iterable
8
9
  from importlib.metadata import distributions
9
10
  from itertools import chain
10
11
  from pathlib import Path
11
12
  from textwrap import dedent
12
- from typing import Dict, Iterable, Union
13
+ from typing import Union
13
14
 
14
15
  from jinja2 import StrictUndefined
15
16
  from jinja2.sandbox import SandboxedEnvironment
16
17
  from packaging.requirements import Requirement
17
18
 
18
- # TODO: Remove once support for Python 3.10 is dropped.
19
+ # YORE: EOL 3.10: Replace block with line 2.
19
20
  if sys.version_info >= (3, 11):
20
21
  import tomllib
21
22
  else:
@@ -26,11 +27,10 @@ with project_dir.joinpath("pyproject.toml").open("rb") as pyproject_file:
26
27
  pyproject = tomllib.load(pyproject_file)
27
28
  project = pyproject["project"]
28
29
  project_name = project["name"]
29
- with project_dir.joinpath("devdeps.txt").open() as devdeps_file:
30
- devdeps = [line.strip() for line in devdeps_file if line.strip() and not line.strip().startswith(("-e", "#"))]
30
+ devdeps = [dep for dep in pyproject["dependency-groups"]["dev"] if not dep.startswith("-e")]
31
31
 
32
- PackageMetadata = Dict[str, Union[str, Iterable[str]]]
33
- Metadata = Dict[str, PackageMetadata]
32
+ PackageMetadata = dict[str, Union[str, Iterable[str]]]
33
+ Metadata = dict[str, PackageMetadata]
34
34
 
35
35
 
36
36
  def _merge_fields(metadata: dict) -> 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())
@@ -0,0 +1 @@
1
+ make.py
@@ -9,15 +9,17 @@ import subprocess
9
9
  import sys
10
10
  from contextlib import contextmanager
11
11
  from pathlib import Path
12
- from typing import Any, Iterator
12
+ from textwrap import dedent
13
+ from typing import TYPE_CHECKING, Any
13
14
 
14
- PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.8 3.9 3.10 3.11 3.12 3.13").split()
15
+ if TYPE_CHECKING:
16
+ from collections.abc import Iterator
15
17
 
16
- exe = ""
17
- prefix = ""
18
18
 
19
+ PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.9 3.10 3.11 3.12 3.13 3.14").split()
19
20
 
20
- def shell(cmd: str, capture_output: bool = False, **kwargs: Any) -> str | None:
21
+
22
+ def shell(cmd: str, *, capture_output: bool = False, **kwargs: Any) -> str | None:
21
23
  """Run a shell command."""
22
24
  if capture_output:
23
25
  return subprocess.check_output(cmd, shell=True, text=True, **kwargs) # noqa: S602
@@ -37,17 +39,13 @@ def environ(**kwargs: str) -> Iterator[None]:
37
39
  os.environ.update(original)
38
40
 
39
41
 
40
- def uv_install() -> None:
42
+ def uv_install(venv: Path) -> None:
41
43
  """Install dependencies using uv."""
42
- uv_opts = ""
43
- if "UV_RESOLUTION" in os.environ:
44
- uv_opts = f"--resolution={os.getenv('UV_RESOLUTION')}"
45
- requirements = shell(f"uv pip compile {uv_opts} pyproject.toml devdeps.txt", capture_output=True)
46
- shell("uv pip install -r -", input=requirements, text=True)
47
- if "CI" not in os.environ:
48
- shell("uv pip install --no-deps -e .")
49
- else:
50
- shell("uv pip install --no-deps .")
44
+ with environ(UV_PROJECT_ENVIRONMENT=str(venv), PYO3_USE_ABI3_FORWARD_COMPATIBILITY="1"):
45
+ if "CI" in os.environ:
46
+ shell("uv sync --no-editable")
47
+ else:
48
+ shell("uv sync")
51
49
 
52
50
 
53
51
  def setup() -> None:
@@ -55,51 +53,32 @@ def setup() -> None:
55
53
  if not shutil.which("uv"):
56
54
  raise ValueError("make: setup: uv must be installed, see https://github.com/astral-sh/uv")
57
55
 
58
- print("Installing dependencies (default environment)") # noqa: T201
56
+ print("Installing dependencies (default environment)")
59
57
  default_venv = Path(".venv")
60
58
  if not default_venv.exists():
61
- shell("uv venv --python python")
62
- uv_install()
59
+ shell("uv venv")
60
+ uv_install(default_venv)
63
61
 
64
62
  if PYTHON_VERSIONS:
65
63
  for version in PYTHON_VERSIONS:
66
- print(f"\nInstalling dependencies (python{version})") # noqa: T201
64
+ print(f"\nInstalling dependencies (python{version})")
67
65
  venv_path = Path(f".venvs/{version}")
68
66
  if not venv_path.exists():
69
67
  shell(f"uv venv --python {version} {venv_path}")
70
- with environ(VIRTUAL_ENV=str(venv_path.resolve())):
71
- uv_install()
72
-
73
-
74
- def activate(path: str) -> None:
75
- """Activate a virtual environment."""
76
- global exe, prefix # noqa: PLW0603
77
-
78
- if (bin := Path(path, "bin")).exists():
79
- activate_script = bin / "activate_this.py"
80
- elif (scripts := Path(path, "Scripts")).exists():
81
- activate_script = scripts / "activate_this.py"
82
- exe = ".exe"
83
- prefix = f"{path}/Scripts/"
84
- else:
85
- raise ValueError(f"make: activate: Cannot find activation script in {path}")
86
-
87
- if not activate_script.exists():
88
- raise ValueError(f"make: activate: Cannot find activation script in {path}")
89
-
90
- exec(activate_script.read_text(), {"__file__": str(activate_script)}) # noqa: S102
68
+ with environ(UV_PROJECT_ENVIRONMENT=str(venv_path.resolve())):
69
+ uv_install(venv_path)
91
70
 
92
71
 
93
72
  def run(version: str, cmd: str, *args: str, **kwargs: Any) -> None:
94
73
  """Run a command in a virtual environment."""
95
74
  kwargs = {"check": True, **kwargs}
75
+ uv_run = ["uv", "run", "--no-sync"]
96
76
  if version == "default":
97
- activate(".venv")
98
- subprocess.run([f"{prefix}{cmd}{exe}", *args], **kwargs) # noqa: S603, PLW1510
77
+ with environ(UV_PROJECT_ENVIRONMENT=".venv"):
78
+ subprocess.run([*uv_run, cmd, *args], **kwargs) # noqa: S603, PLW1510
99
79
  else:
100
- activate(f".venvs/{version}")
101
- os.environ["MULTIRUN"] = "1"
102
- subprocess.run([f"{prefix}{cmd}{exe}", *args], **kwargs) # noqa: S603, PLW1510
80
+ with environ(UV_PROJECT_ENVIRONMENT=f".venvs/{version}", MULTIRUN="1"):
81
+ subprocess.run([*uv_run, cmd, *args], **kwargs) # noqa: S603, PLW1510
103
82
 
104
83
 
105
84
  def multirun(cmd: str, *args: str, **kwargs: Any) -> None:
@@ -122,18 +101,17 @@ def clean() -> None:
122
101
  """Delete build artifacts and cache files."""
123
102
  paths_to_clean = ["build", "dist", "htmlcov", "site", ".coverage*", ".pdm-build"]
124
103
  for path in paths_to_clean:
125
- shell(f"rm -rf {path}")
104
+ shutil.rmtree(path, ignore_errors=True)
126
105
 
127
- cache_dirs = [".cache", ".pytest_cache", ".mypy_cache", ".ruff_cache", "__pycache__"]
128
- for dirpath in Path(".").rglob("*"):
129
- if any(dirpath.match(pattern) for pattern in cache_dirs) and not (dirpath.match(".venv") or dirpath.match(".venvs")):
130
- shutil.rmtree(path, ignore_errors=True)
106
+ cache_dirs = {".cache", ".pytest_cache", ".mypy_cache", ".ruff_cache", "__pycache__"}
107
+ for dirpath in Path(".").rglob("*/"):
108
+ if dirpath.parts[0] not in (".venv", ".venvs") and dirpath.name in cache_dirs:
109
+ shutil.rmtree(dirpath, ignore_errors=True)
131
110
 
132
111
 
133
112
  def vscode() -> None:
134
113
  """Configure VSCode to work on this project."""
135
- Path(".vscode").mkdir(parents=True, exist_ok=True)
136
- shell("cp -v config/vscode/* .vscode")
114
+ shutil.copytree("config/vscode", ".vscode", dirs_exist_ok=True)
137
115
 
138
116
 
139
117
  def main() -> int:
@@ -143,21 +121,24 @@ def main() -> int:
143
121
  if len(args) > 1:
144
122
  run("default", "duty", "--help", args[1])
145
123
  else:
146
- print("Available commands") # noqa: T201
147
- print(" help Print this help. Add task name to print help.") # noqa: T201
148
- print(" setup Setup all virtual environments (install dependencies).") # noqa: T201
149
- print(" run Run a command in the default virtual environment.") # noqa: T201
150
- print(" multirun Run a command for all configured Python versions.") # noqa: T201
151
- print(" allrun Run a command in all virtual environments.") # noqa: T201
152
- print(" 3.x Run a command in the virtual environment for Python 3.x.") # noqa: T201
153
- print(" clean Delete build artifacts and cache files.") # noqa: T201
154
- print(" vscode Configure VSCode to work on this project.") # noqa: T201
155
- try:
156
- run("default", "python", "-V", capture_output=True)
157
- except (subprocess.CalledProcessError, ValueError):
158
- pass
159
- else:
160
- print("\nAvailable tasks") # noqa: T201
124
+ print(
125
+ dedent(
126
+ """
127
+ Available commands
128
+ help Print this help. Add task name to print help.
129
+ setup Setup all virtual environments (install dependencies).
130
+ run Run a command in the default virtual environment.
131
+ multirun Run a command for all configured Python versions.
132
+ allrun Run a command in all virtual environments.
133
+ 3.x Run a command in the virtual environment for Python 3.x.
134
+ clean Delete build artifacts and cache files.
135
+ vscode Configure VSCode to work on this project.
136
+ """,
137
+ ),
138
+ flush=True,
139
+ )
140
+ if os.path.exists(".venv"):
141
+ print("\nAvailable tasks", flush=True)
161
142
  run("default", "duty", "--list")
162
143
  return 0
163
144
 
@@ -206,5 +187,5 @@ if __name__ == "__main__":
206
187
  sys.exit(main())
207
188
  except subprocess.CalledProcessError as process:
208
189
  if process.output:
209
- print(process.output, file=sys.stderr) # noqa: T201
190
+ print(process.output, file=sys.stderr)
210
191
  sys.exit(process.returncode)
@@ -4,10 +4,14 @@ from __future__ import annotations
4
4
 
5
5
  import re
6
6
  from pathlib import Path
7
- from typing import Pattern, Sequence
7
+ from re import Pattern
8
+ from typing import TYPE_CHECKING
8
9
 
9
10
  from failprint.lazy import lazy
10
11
 
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Sequence
14
+
11
15
 
12
16
  @lazy(name="blacken_docs")
13
17
  def run(
@@ -5,12 +5,12 @@ from __future__ import annotations
5
5
  import os
6
6
  import subprocess
7
7
  import sys
8
- from functools import lru_cache
8
+ from functools import cache
9
9
 
10
10
  from failprint.lazy import lazy
11
11
 
12
12
 
13
- @lru_cache(maxsize=None)
13
+ @cache
14
14
  def _find_ruff() -> str:
15
15
  from ruff.__main__ import find_ruff_bin
16
16
 
@@ -5,10 +5,13 @@ from __future__ import annotations
5
5
  import importlib
6
6
  import sys
7
7
  from io import StringIO
8
- from typing import Literal, Sequence, cast
8
+ from typing import TYPE_CHECKING, Literal, cast
9
9
 
10
10
  from failprint.lazy import lazy
11
11
 
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Sequence
14
+
12
15
 
13
16
  @lazy(name="safety.check")
14
17
  def check(