duty 1.2.0__tar.gz → 1.3.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 (45) hide show
  1. {duty-1.2.0 → duty-1.3.0}/PKG-INFO +7 -5
  2. {duty-1.2.0 → duty-1.3.0}/README.md +2 -2
  3. {duty-1.2.0 → duty-1.3.0}/pyproject.toml +10 -49
  4. {duty-1.2.0 → duty-1.3.0}/src/duty/debug.py +4 -1
  5. {duty-1.2.0 → duty-1.3.0}/src/duty/validation.py +24 -6
  6. duty-1.2.0/tests/__init__.py +0 -7
  7. duty-1.2.0/tests/conftest.py +0 -1
  8. duty-1.2.0/tests/fixtures/arguments.py +0 -6
  9. duty-1.2.0/tests/fixtures/basic.py +0 -6
  10. duty-1.2.0/tests/fixtures/booleans.py +0 -6
  11. duty-1.2.0/tests/fixtures/code.py +0 -6
  12. duty-1.2.0/tests/fixtures/list.py +0 -11
  13. duty-1.2.0/tests/fixtures/multiple.py +0 -11
  14. duty-1.2.0/tests/fixtures/precedence.py +0 -6
  15. duty-1.2.0/tests/fixtures/validation.py +0 -46
  16. duty-1.2.0/tests/test_cli.py +0 -247
  17. duty-1.2.0/tests/test_collection.py +0 -70
  18. duty-1.2.0/tests/test_context.py +0 -99
  19. duty-1.2.0/tests/test_decorator.py +0 -26
  20. duty-1.2.0/tests/test_running.py +0 -114
  21. duty-1.2.0/tests/test_validation.py +0 -147
  22. {duty-1.2.0 → duty-1.3.0}/LICENSE +0 -0
  23. {duty-1.2.0 → duty-1.3.0}/src/duty/__init__.py +0 -0
  24. {duty-1.2.0 → duty-1.3.0}/src/duty/__main__.py +0 -0
  25. {duty-1.2.0 → duty-1.3.0}/src/duty/callables/__init__.py +0 -0
  26. {duty-1.2.0 → duty-1.3.0}/src/duty/callables/_io.py +0 -0
  27. {duty-1.2.0 → duty-1.3.0}/src/duty/callables/autoflake.py +0 -0
  28. {duty-1.2.0 → duty-1.3.0}/src/duty/callables/black.py +0 -0
  29. {duty-1.2.0 → duty-1.3.0}/src/duty/callables/blacken_docs.py +0 -0
  30. {duty-1.2.0 → duty-1.3.0}/src/duty/callables/coverage.py +0 -0
  31. {duty-1.2.0 → duty-1.3.0}/src/duty/callables/flake8.py +0 -0
  32. {duty-1.2.0 → duty-1.3.0}/src/duty/callables/interrogate.py +0 -0
  33. {duty-1.2.0 → duty-1.3.0}/src/duty/callables/isort.py +0 -0
  34. {duty-1.2.0 → duty-1.3.0}/src/duty/callables/mkdocs.py +0 -0
  35. {duty-1.2.0 → duty-1.3.0}/src/duty/callables/mypy.py +0 -0
  36. {duty-1.2.0 → duty-1.3.0}/src/duty/callables/pytest.py +0 -0
  37. {duty-1.2.0 → duty-1.3.0}/src/duty/callables/ruff.py +0 -0
  38. {duty-1.2.0 → duty-1.3.0}/src/duty/callables/safety.py +0 -0
  39. {duty-1.2.0 → duty-1.3.0}/src/duty/callables/ssort.py +0 -0
  40. {duty-1.2.0 → duty-1.3.0}/src/duty/cli.py +0 -0
  41. {duty-1.2.0 → duty-1.3.0}/src/duty/collection.py +0 -0
  42. {duty-1.2.0 → duty-1.3.0}/src/duty/context.py +0 -0
  43. {duty-1.2.0 → duty-1.3.0}/src/duty/decorator.py +0 -0
  44. {duty-1.2.0 → duty-1.3.0}/src/duty/exceptions.py +0 -0
  45. {duty-1.2.0 → duty-1.3.0}/src/duty/py.typed +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: duty
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: A simple task runner.
5
- Keywords: task-runner task runner cross-platform
6
- Author-Email: Timothée Mazzucotelli <pawamoy@pm.me>
5
+ Keywords: task-runner,task,runner,cross-platform
6
+ Author-Email: =?utf-8?q?Timoth=C3=A9e_Mazzucotelli?= <dev@pawamoy.fr>
7
7
  License: ISC
8
8
  Classifier: Development Status :: 4 - Beta
9
9
  Classifier: Intended Audience :: Developers
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.9
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
18
19
  Classifier: Topic :: Documentation
19
20
  Classifier: Topic :: Software Development
20
21
  Classifier: Topic :: Utilities
@@ -28,15 +29,16 @@ Project-URL: Discussions, https://github.com/pawamoy/duty/discussions
28
29
  Project-URL: Gitter, https://gitter.im/duty/community
29
30
  Project-URL: Funding, https://github.com/sponsors/pawamoy
30
31
  Requires-Python: >=3.8
32
+ Requires-Dist: eval-type-backport; python_version < "3.10"
31
33
  Requires-Dist: failprint!=1.0.0,>=0.11
32
34
  Description-Content-Type: text/markdown
33
35
 
34
36
  # duty
35
37
 
36
38
  [![ci](https://github.com/pawamoy/duty/workflows/ci/badge.svg)](https://github.com/pawamoy/duty/actions?query=workflow%3Aci)
37
- [![documentation](https://img.shields.io/badge/docs-mkdocs%20material-blue.svg?style=flat)](https://pawamoy.github.io/duty/)
39
+ [![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)](https://pawamoy.github.io/duty/)
38
40
  [![pypi version](https://img.shields.io/pypi/v/duty.svg)](https://pypi.org/project/duty/)
39
- [![gitpod](https://img.shields.io/badge/gitpod-workspace-blue.svg?style=flat)](https://gitpod.io/#https://github.com/pawamoy/duty)
41
+ [![gitpod](https://img.shields.io/badge/gitpod-workspace-708FCC.svg?style=flat)](https://gitpod.io/#https://github.com/pawamoy/duty)
40
42
  [![gitter](https://badges.gitter.im/join%20chat.svg)](https://app.gitter.im/#/room/#duty:gitter.im)
41
43
 
42
44
  A simple task runner.
@@ -1,9 +1,9 @@
1
1
  # duty
2
2
 
3
3
  [![ci](https://github.com/pawamoy/duty/workflows/ci/badge.svg)](https://github.com/pawamoy/duty/actions?query=workflow%3Aci)
4
- [![documentation](https://img.shields.io/badge/docs-mkdocs%20material-blue.svg?style=flat)](https://pawamoy.github.io/duty/)
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-blue.svg?style=flat)](https://gitpod.io/#https://github.com/pawamoy/duty)
6
+ [![gitpod](https://img.shields.io/badge/gitpod-workspace-708FCC.svg?style=flat)](https://gitpod.io/#https://github.com/pawamoy/duty)
7
7
  [![gitter](https://badges.gitter.im/join%20chat.svg)](https://app.gitter.im/#/room/#duty:gitter.im)
8
8
 
9
9
  A simple task runner.
@@ -8,7 +8,7 @@ build-backend = "pdm.backend"
8
8
  name = "duty"
9
9
  description = "A simple task runner."
10
10
  authors = [
11
- { name = "Timothée Mazzucotelli", email = "pawamoy@pm.me" },
11
+ { name = "Timothée Mazzucotelli", email = "dev@pawamoy.fr" },
12
12
  ]
13
13
  readme = "README.md"
14
14
  requires-python = ">=3.8"
@@ -30,15 +30,17 @@ classifiers = [
30
30
  "Programming Language :: Python :: 3.10",
31
31
  "Programming Language :: Python :: 3.11",
32
32
  "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
33
34
  "Topic :: Documentation",
34
35
  "Topic :: Software Development",
35
36
  "Topic :: Utilities",
36
37
  "Typing :: Typed",
37
38
  ]
38
39
  dependencies = [
40
+ "eval-type-backport; python_version < '3.10'",
39
41
  "failprint>=0.11,!=1.0.0",
40
42
  ]
41
- version = "1.2.0"
43
+ version = "1.3.0"
42
44
 
43
45
  [project.license]
44
46
  text = "ISC"
@@ -56,58 +58,17 @@ Funding = "https://github.com/sponsors/pawamoy"
56
58
  [project.scripts]
57
59
  duty = "duty.cli:main"
58
60
 
59
- [tool.pdm]
60
- plugins = [
61
- "pdm-multirun",
62
- ]
63
-
64
61
  [tool.pdm.version]
65
62
  source = "scm"
66
63
 
67
64
  [tool.pdm.build]
68
65
  package-dir = "src"
69
66
  editable-backend = "editables"
70
-
71
- [tool.pdm.dev-dependencies]
72
- ci-quality = [
73
- "duty[docs,quality,typing,security]",
74
- ]
75
- ci-tests = [
76
- "duty[tests]",
77
- ]
78
- docs = [
79
- "black>=23.9",
80
- "markdown-callouts>=0.3",
81
- "markdown-exec>=1.7",
82
- "mkdocs>=1.5",
83
- "mkdocs-coverage>=1.0",
84
- "mkdocs-gen-files>=0.5",
85
- "mkdocs-git-committers-plugin-2>=1.2",
86
- "mkdocs-literate-nav>=0.6",
87
- "mkdocs-material>=9.4",
88
- "mkdocs-minify-plugin>=0.7",
89
- "mkdocstrings[python]>=0.23",
90
- "tomli>=2.0; python_version < '3.11'",
67
+ source-includes = [
68
+ "share",
91
69
  ]
92
- maintain = [
93
- "black>=23.9",
94
- "blacken-docs>=1.16",
95
- "git-changelog>=2.3",
96
- ]
97
- quality = [
98
- "ruff>=0.0",
99
- ]
100
- tests = [
101
- "pytest>=7.4",
102
- "pytest-cov>=4.1",
103
- "pytest-randomly>=3.15",
104
- "pytest-xdist>=3.3",
105
- ]
106
- typing = [
107
- "mypy>=1.5",
108
- "types-markdown>=3.5",
109
- "types-pyyaml>=6.0",
110
- ]
111
- security = [
112
- "safety>=2.3",
70
+
71
+ [tool.pdm.build.wheel-data]
72
+ data = [
73
+ { path = "share/**/*", relative-to = "." },
113
74
  ]
@@ -37,6 +37,8 @@ class Environment:
37
37
  """Python interpreter name."""
38
38
  interpreter_version: str
39
39
  """Python interpreter version."""
40
+ interpreter_path: str
41
+ """Path to Python executable."""
40
42
  platform: str
41
43
  """Operating System."""
42
44
  packages: list[Package]
@@ -83,6 +85,7 @@ def get_debug_info() -> Environment:
83
85
  return Environment(
84
86
  interpreter_name=py_name,
85
87
  interpreter_version=py_version,
88
+ interpreter_path=sys.executable,
86
89
  platform=platform.platform(),
87
90
  variables=[Variable(var, val) for var in variables if (val := os.getenv(var))],
88
91
  packages=[Package(pkg, get_version(pkg)) for pkg in packages],
@@ -93,7 +96,7 @@ def print_debug_info() -> None:
93
96
  """Print debug/environment information."""
94
97
  info = get_debug_info()
95
98
  print(f"- __System__: {info.platform}")
96
- print(f"- __Python__: {info.interpreter_name} {info.interpreter_version}")
99
+ print(f"- __Python__: {info.interpreter_name} {info.interpreter_version} ({info.interpreter_path})")
97
100
  print("- __Environment variables__:")
98
101
  for var in info.variables:
99
102
  print(f" - `{var.name}`: `{var.value}`")
@@ -9,9 +9,21 @@ from __future__ import annotations
9
9
 
10
10
  import sys
11
11
  import textwrap
12
+ from contextlib import suppress
12
13
  from functools import cached_property
13
14
  from inspect import Parameter, Signature, signature
14
- from typing import Any, Callable, Sequence
15
+ from typing import Any, Callable, ForwardRef, Sequence, Union, get_args, get_origin
16
+
17
+ # TODO: Update once support for Python 3.9 is dropped.
18
+ if sys.version_info < (3, 10):
19
+ from eval_type_backport import eval_type_backport as eval_type
20
+
21
+ union_types = (Union,)
22
+ else:
23
+ from types import UnionType
24
+ from typing import _eval_type as eval_type # type: ignore[attr-defined]
25
+
26
+ union_types = (Union, UnionType)
15
27
 
16
28
 
17
29
  def to_bool(value: str) -> bool:
@@ -40,6 +52,12 @@ def cast_arg(arg: Any, annotation: Any) -> Any:
40
52
  return arg
41
53
  if annotation is bool:
42
54
  annotation = to_bool
55
+ if get_origin(annotation) in union_types:
56
+ for sub_annotation in get_args(annotation):
57
+ if sub_annotation is type(None):
58
+ continue
59
+ with suppress(Exception):
60
+ return cast_arg(arg, sub_annotation)
43
61
  try:
44
62
  return annotation(arg)
45
63
  except Exception: # noqa: BLE001
@@ -65,11 +83,9 @@ class ParamsCaster:
65
83
  Returns:
66
84
  The position of the variable positional parameter.
67
85
  """
68
- pos = 0
69
- for param in self.params_list:
86
+ for pos, param in enumerate(self.params_list):
70
87
  if param.kind is Parameter.VAR_POSITIONAL:
71
88
  return pos
72
- pos += 1
73
89
  return -1
74
90
 
75
91
  @cached_property
@@ -175,6 +191,7 @@ def _get_params_caster(func: Callable, *args: Any, **kwargs: Any) -> ParamsCaste
175
191
  if exec_globals[name] is annotations:
176
192
  eval_str = True
177
193
  del exec_globals[name]
194
+ break
178
195
  exec_globals["__context_above"] = {}
179
196
 
180
197
  # Don't keep first parameter: context.
@@ -188,9 +205,10 @@ def _get_params_caster(func: Callable, *args: Any, **kwargs: Any) -> ParamsCaste
188
205
  param.kind,
189
206
  default=param.default,
190
207
  annotation=(
191
- eval( # noqa: PGH001,S307
192
- param.annotation,
208
+ eval_type(
209
+ ForwardRef(param.annotation) if isinstance(param.annotation, str) else param.annotation,
193
210
  exec_globals,
211
+ {},
194
212
  )
195
213
  if param.annotation is not Parameter.empty
196
214
  else type(param.default)
@@ -1,7 +0,0 @@
1
- """Tests suite for `duty`."""
2
-
3
- from pathlib import Path
4
-
5
- TESTS_DIR = Path(__file__).parent
6
- TMP_DIR = TESTS_DIR / "tmp"
7
- FIXTURES_DIR = TESTS_DIR / "fixtures"
@@ -1 +0,0 @@
1
- """Configuration for the pytest test suite."""
@@ -1,6 +0,0 @@
1
- from duty import duty
2
-
3
-
4
- @duty
5
- def say_hello(ctx, cat, dog="dog"):
6
- ctx.run(lambda: 0, title=f"Hello cat {cat} and dog {dog}!")
@@ -1,6 +0,0 @@
1
- from duty import duty
2
-
3
-
4
- @duty
5
- def hello(ctx):
6
- pass
@@ -1,6 +0,0 @@
1
- from duty import duty
2
-
3
-
4
- @duty
5
- def boolean(ctx, zero: bool = True):
6
- ctx.run(lambda: 0 if zero else 1)
@@ -1,6 +0,0 @@
1
- from duty import duty
2
-
3
-
4
- @duty
5
- def exit_with(ctx, code):
6
- ctx.run(lambda: code)
@@ -1,11 +0,0 @@
1
- from duty import duty
2
-
3
-
4
- @duty
5
- def tong(ctx):
6
- """Tong..."""
7
-
8
-
9
- @duty
10
- def deum(ctx):
11
- """DEUM!"""
@@ -1,11 +0,0 @@
1
- from duty import duty
2
-
3
-
4
- @duty
5
- def first_duty(ctx):
6
- ctx.run(lambda: 0, fmt="tap", title="first")
7
-
8
-
9
- @duty
10
- def second_duty(ctx):
11
- ctx.run(lambda: 0, fmt="tap", title="second")
@@ -1,6 +0,0 @@
1
- from duty import duty
2
-
3
-
4
- @duty(nofail=True)
5
- def precedence(ctx):
6
- ctx.run(lambda: 1, title="Precedence", nofail=False)
@@ -1,46 +0,0 @@
1
- def no_params(ctx):
2
- pass # pragma: no cover
3
-
4
-
5
- def pos_or_kw_param(ctx, a: int):
6
- pass # pragma: no cover
7
-
8
-
9
- def pos_or_kw_params(ctx, a: int, b: int):
10
- pass # pragma: no cover
11
-
12
-
13
- def varpos_param(ctx, *a: int):
14
- pass # pragma: no cover
15
-
16
-
17
- def pos_and_varpos_param(ctx, a: int, *b: int):
18
- pass # pragma: no cover
19
-
20
-
21
- def kwonly_param(ctx, *a: int, b: int):
22
- pass # pragma: no cover
23
-
24
-
25
- def varkw_param(ctx, a: int, **b: int):
26
- pass # pragma: no cover
27
-
28
-
29
- def varkw_no_annotation(ctx, **a):
30
- pass # pragma: no cover
31
-
32
-
33
- def posonly_marker(ctx, a: int, /, b: int):
34
- pass # pragma: no cover
35
-
36
-
37
- def kwonly_marker(ctx, a: int, *, b: int):
38
- pass # pragma: no cover
39
-
40
-
41
- def only_markers(ctx, a: int, /, b: int, *, c: int):
42
- pass # pragma: no cover
43
-
44
-
45
- def full(ctx, a: int, /, b: int, *c: int, d: int, e: int = 0, **f: int):
46
- pass # pragma: no cover
@@ -1,247 +0,0 @@
1
- """Tests for the `cli` module."""
2
-
3
- from __future__ import annotations
4
-
5
- import pytest
6
-
7
- from duty import cli, debug
8
-
9
-
10
- def test_no_duty(capsys: pytest.CaptureFixture) -> None:
11
- """Run no duties.
12
-
13
- Parameters:
14
- capsys: Pytest fixture to capture output.
15
- """
16
- assert cli.main([]) == 1
17
- captured = capsys.readouterr()
18
- assert "Available duties" in captured.out
19
-
20
-
21
- def test_show_help(capsys: pytest.CaptureFixture) -> None:
22
- """Show help.
23
-
24
- Parameters:
25
- capsys: Pytest fixture to capture output.
26
- """
27
- assert cli.main(["-h"]) == 0
28
- captured = capsys.readouterr()
29
- assert "duty" in captured.out
30
-
31
-
32
- def test_show_help_for_given_duties(capsys: pytest.CaptureFixture) -> None:
33
- """Show help for given duties.
34
-
35
- Parameters:
36
- capsys: Pytest fixture to capture output.
37
- """
38
- assert cli.main(["-d", "tests/fixtures/basic.py", "-h", "hello"]) == 0
39
- captured = capsys.readouterr()
40
- assert "hello" in captured.out
41
-
42
-
43
- def test_show_help_unknown_duty(capsys: pytest.CaptureFixture) -> None:
44
- """Show help for an unknown duty.
45
-
46
- Parameters:
47
- capsys: Pytest fixture to capture output.
48
- """
49
- assert cli.main(["-d", "tests/fixtures/basic.py", "-h", "not-here"]) == 0
50
- captured = capsys.readouterr()
51
- assert "Unknown duty" in captured.out
52
-
53
-
54
- def test_select_duties() -> None:
55
- """Run a duty."""
56
- assert cli.main(["-d", "tests/fixtures/basic.py", "hello"]) == 0
57
-
58
-
59
- def test_unknown_duty() -> None:
60
- """Don't run an unknown duty."""
61
- assert cli.main(["-d", "tests/fixtures/basic.py", "byebye"]) == 1
62
-
63
-
64
- def test_incorrect_arguments() -> None:
65
- """Use incorrect arguments."""
66
- assert cli.main(["-d", "tests/fixtures/basic.py", "hello=1"]) == 1
67
-
68
-
69
- # we use 300 because it's slightly above the valid maximum 255
70
- @pytest.mark.parametrize("code", range(-100, 300, 7))
71
- def test_duty_failure(code: int) -> None:
72
- """Check exit code.
73
-
74
- Parameters:
75
- code: Code to match.
76
- """
77
- assert cli.main(["-d", "tests/fixtures/code.py", "exit_with", f"code={code}"]) == code
78
-
79
-
80
- def test_multiple_duties(capfd: pytest.CaptureFixture) -> None:
81
- """Run multiple duties.
82
-
83
- Parameters:
84
- capfd: Pytest fixture to capture output.
85
- """
86
- assert cli.main(["-d", "tests/fixtures/multiple.py", "first_duty", "second_duty"]) == 0
87
- captured = capfd.readouterr()
88
- assert "first" in captured.out
89
- assert "second" in captured.out
90
-
91
-
92
- def test_duty_arguments(capfd: pytest.CaptureFixture) -> None:
93
- """Run duty with arguments.
94
-
95
- Parameters:
96
- capfd: Pytest fixture to capture output.
97
- """
98
- assert cli.main(["-d", "tests/fixtures/arguments.py", "say_hello", "cat=fabric"]) == 0
99
- captured = capfd.readouterr()
100
- assert "cat fabric" in captured.out
101
- assert "dog dog" in captured.out
102
-
103
- assert cli.main(["-d", "tests/fixtures/arguments.py", "say_hello", "dog=paramiko", "cat=invoke"]) == 0
104
- captured = capfd.readouterr()
105
- assert "cat invoke" in captured.out
106
- assert "dog paramiko" in captured.out
107
-
108
-
109
- def test_list_duties(capsys: pytest.CaptureFixture) -> None:
110
- """List duties.
111
-
112
- Parameters:
113
- capsys: Pytest fixture to capture output.
114
- """
115
- assert cli.main(["-d", "tests/fixtures/list.py", "-l"]) == 0
116
- captured = capsys.readouterr()
117
- assert "Tong..." in captured.out
118
- assert "DEUM!" in captured.out
119
-
120
-
121
- def test_global_options() -> None:
122
- """Test global options."""
123
- assert cli.main(["-d", "tests/fixtures/code.py", "-z", "exit_with", "1"]) == 0
124
-
125
-
126
- def test_global_and_local_options() -> None:
127
- """Test global and local options."""
128
- assert cli.main(["-d", "tests/fixtures/code.py", "-z", "exit_with", "-Z", "1"]) == 1
129
-
130
-
131
- def test_options_precedence() -> None:
132
- """Test options precedence."""
133
- # @duty(nofail=True) is overridden by ctx.run(nofail=False)
134
- assert cli.main(["-d", "tests/fixtures/precedence.py", "precedence"]) == 1
135
-
136
- # ctx.run(nofail=False) is overridden by local option -z
137
- assert cli.main(["-d", "tests/fixtures/precedence.py", "precedence", "-z"]) == 0
138
-
139
- # ctx.run(nofail=False) is overridden by global option -z
140
- assert cli.main(["-d", "tests/fixtures/precedence.py", "-z", "precedence"]) == 0
141
-
142
- # global option -z is overridden by local option -z
143
- assert cli.main(["-d", "tests/fixtures/precedence.py", "-z", "precedence", "-Z"]) == 1
144
-
145
-
146
- # test options precedence (CLI option, env var, ctx.run, @duty
147
- # test positional arguments
148
- # test extra keyword arguments
149
- # test complete (global options + local options + multi duties + positional args + keyword args + extra keyword args)
150
-
151
-
152
- @pytest.mark.parametrize(
153
- ("param", "expected"),
154
- [
155
- ("", 1),
156
- ("n", 1),
157
- ("N", 1),
158
- ("no", 1),
159
- ("NO", 1),
160
- ("false", 1),
161
- ("FALSE", 1),
162
- ("off", 1),
163
- ("OFF", 1),
164
- ("zero=", 1),
165
- ("zero=0", 1),
166
- ("zero=n", 1),
167
- ("zero=N", 1),
168
- ("zero=no", 1),
169
- ("zero=NO", 1),
170
- ("zero=false", 1),
171
- ("zero=FALSE", 1),
172
- ("zero=off", 1),
173
- ("zero=OFF", 1),
174
- ("y", 0),
175
- ("Y", 0),
176
- ("yes", 0),
177
- ("YES", 0),
178
- ("on", 0),
179
- ("ON", 0),
180
- ("true", 0),
181
- ("TRUE", 0),
182
- ("anything else", 0),
183
- ("-1", 0),
184
- ("1", 0),
185
- ("zero=y", 0),
186
- ("zero=Y", 0),
187
- ("zero=yes", 0),
188
- ("zero=YES", 0),
189
- ("zero=on", 0),
190
- ("zero=ON", 0),
191
- ("zero=true", 0),
192
- ("zero=TRUE", 0),
193
- ("zero=anything else", 0),
194
- ("zero=-1", 0),
195
- ("zero=1", 0),
196
- ],
197
- )
198
- def test_cast_bool_parameter(param: str, expected: int) -> None:
199
- """Test parameters casting as boolean.
200
-
201
- Parameters:
202
- param: Pytest parametrization fixture.
203
- expected: Pytest parametrization fixture.
204
- """
205
- assert cli.main(["-d", "tests/fixtures/booleans.py", "boolean", param]) == expected
206
-
207
-
208
- def test_invalid_params(capsys: pytest.CaptureFixture) -> None:
209
- """Check that invalid parameters are early and correctly detected.
210
-
211
- Parameters:
212
- capsys: Pytest fixture to capture output.
213
- """
214
- assert cli.main(["-d", "tests/fixtures/booleans.py", "boolean", "zore=off"]) == 1
215
- captured = capsys.readouterr()
216
- assert "unexpected keyword argument 'zore'" in captured.err
217
-
218
- assert cli.main(["-d", "tests/fixtures/code.py", "exit_with"]) == 1
219
- captured = capsys.readouterr()
220
- assert "missing 1 required positional argument: 'code'" in captured.err
221
-
222
-
223
- def test_show_version(capsys: pytest.CaptureFixture) -> None:
224
- """Show version.
225
-
226
- Parameters:
227
- capsys: Pytest fixture to capture output.
228
- """
229
- with pytest.raises(SystemExit):
230
- cli.main(["-V"])
231
- captured = capsys.readouterr()
232
- assert debug.get_version() in captured.out
233
-
234
-
235
- def test_show_debug_info(capsys: pytest.CaptureFixture) -> None:
236
- """Show debug information.
237
-
238
- Parameters:
239
- capsys: Pytest fixture to capture output.
240
- """
241
- with pytest.raises(SystemExit):
242
- cli.main(["--debug-info"])
243
- captured = capsys.readouterr().out.lower()
244
- assert "python" in captured
245
- assert "system" in captured
246
- assert "environment" in captured
247
- assert "packages" in captured
@@ -1,70 +0,0 @@
1
- """Tests for the `collection` module."""
2
-
3
- from __future__ import annotations
4
-
5
- import pytest
6
-
7
- from duty.collection import Collection, Duty
8
- from duty.decorator import duty as decorate
9
-
10
-
11
- def none(*args, **kwargs) -> None: # noqa: ANN002, ANN003, ARG001, D103
12
- ... # pragma: no cover
13
-
14
-
15
- def test_instantiate_duty() -> None:
16
- """Instantiate a duty."""
17
- assert Duty("name", "description", none)
18
- assert Duty("name", "description", none, pre=["0", "1"], post=["2"])
19
-
20
-
21
- def test_dont_get_duty() -> None:
22
- """Don't find a duty."""
23
- collection = Collection()
24
- with pytest.raises(KeyError):
25
- collection.get("hello")
26
-
27
-
28
- def test_register_aliases() -> None:
29
- """Register a duty and its aliases."""
30
- duty = decorate(none, name="hello", aliases=["HELLO", "_hello_", ".hello."]) # type: ignore[call-overload]
31
- collection = Collection()
32
- collection.add(duty)
33
- assert collection.get("hello")
34
- assert collection.get("HELLO")
35
- assert collection.get("_hello_")
36
- assert collection.get(".hello.")
37
-
38
-
39
- def test_replace_name_and_set_alias() -> None:
40
- """Replace underscores by dashes in duties names."""
41
- collection = Collection()
42
- collection.add(decorate(none, name="snake_case")) # type: ignore[call-overload]
43
- assert collection.get("snake_case") is collection.get("snake-case")
44
-
45
-
46
- def test_clear_collection() -> None:
47
- """Check that duties and their aliases are correctly cleared from a collection."""
48
- collection = Collection()
49
- collection.add(decorate(none, name="duty_1")) # type: ignore[call-overload]
50
- collection.clear()
51
- with pytest.raises(KeyError):
52
- collection.get("duty-1")
53
-
54
-
55
- def test_add_duty_to_multiple_collections() -> None:
56
- """Check what happens when adding the same duty to multiple collections."""
57
- collection1 = Collection()
58
- collection2 = Collection()
59
-
60
- duty = decorate(none, name="duty") # type: ignore[call-overload]
61
-
62
- collection1.add(duty)
63
- collection2.add(duty)
64
-
65
- duty1 = collection1.get("duty")
66
- duty2 = collection2.get("duty")
67
-
68
- assert duty1 is not duty2
69
- assert duty1.collection is collection1
70
- assert duty2.collection is collection2
@@ -1,99 +0,0 @@
1
- """Tests for the `context` module."""
2
-
3
- from __future__ import annotations
4
-
5
- from collections import namedtuple
6
- from pathlib import Path
7
-
8
- import pytest
9
-
10
- from duty import context
11
- from duty.exceptions import DutyFailure
12
-
13
- RunResult = namedtuple("RunResult", "code output") # noqa: PYI024
14
-
15
-
16
- def test_allow_overrides(monkeypatch: pytest.MonkeyPatch) -> None:
17
- """Test the `allow_overrides` option.
18
-
19
- Parameters:
20
- monkeypatch: A Pytest fixture to monkeypatch objects.
21
- """
22
- ctx = context.Context({"a": 1}, {"a": 2})
23
- records = []
24
- monkeypatch.setattr(context, "failprint_run", lambda _, **opts: RunResult(records.append(opts), "")) # type: ignore[func-returns-value]
25
- ctx.run("")
26
- ctx.run("", allow_overrides=False)
27
- ctx.run("", allow_overrides=True)
28
- ctx.run("", allow_overrides=False, a=3)
29
- assert records[0]["a"] == 2
30
- assert records[1]["a"] == 1
31
- assert records[2]["a"] == 2
32
- assert records[3]["a"] == 3
33
-
34
-
35
- def test_options_context_manager(monkeypatch: pytest.MonkeyPatch) -> None:
36
- """Test changing options using the context manager.
37
-
38
- Parameters:
39
- monkeypatch: A Pytest fixture to monkeypatch objects.
40
- """
41
- ctx = context.Context({"a": 1}, {"a": 2})
42
- records = []
43
- monkeypatch.setattr(context, "failprint_run", lambda _, **opts: RunResult(records.append(opts), "")) # type: ignore[func-returns-value]
44
-
45
- with ctx.options(a=3):
46
- ctx.run("") # should be overridden by 2
47
- with ctx.options(a=4, allow_overrides=False):
48
- ctx.run("") # should be 4
49
- ctx.run("", allow_overrides=True) # should be 2
50
- ctx.run("", allow_overrides=False) # should be 3
51
-
52
- assert records[0]["a"] == 2
53
- assert records[1]["a"] == 4
54
- assert records[2]["a"] == 2
55
- assert records[3]["a"] == 3
56
-
57
-
58
- def test_workdir(monkeypatch: pytest.MonkeyPatch) -> None:
59
- """Test the `workdir` option.
60
-
61
- Parameters:
62
- monkeypatch: A Pytest fixture to monkeypatch objects.
63
- """
64
- ctx = context.Context({})
65
- monkeypatch.setattr(context, "failprint_run", lambda _: RunResult(len(Path.cwd().parts), ""))
66
- records = []
67
- with pytest.raises(DutyFailure) as failure:
68
- ctx.run("")
69
- records.append(failure.value.code)
70
- with pytest.raises(DutyFailure) as failure:
71
- ctx.run("", workdir="..")
72
- records.append(failure.value.code)
73
- assert records[0] == records[1] + 1
74
-
75
-
76
- def test_workdir_as_context_manager(monkeypatch: pytest.MonkeyPatch) -> None:
77
- """Test the `workdir` option as a context manager, and the `cd` context manager.
78
-
79
- Parameters:
80
- monkeypatch: A Pytest fixture to monkeypatch objects.
81
- """
82
- ctx = context.Context({})
83
- monkeypatch.setattr(context, "failprint_run", lambda _: RunResult(len(Path.cwd().parts), ""))
84
- records = []
85
- with pytest.raises(DutyFailure) as failure, ctx.options(workdir=".."):
86
- ctx.run("")
87
- records.append(failure.value.code)
88
- with pytest.raises(DutyFailure) as failure, ctx.cd("../.."):
89
- ctx.run("")
90
- records.append(failure.value.code)
91
- with pytest.raises(DutyFailure) as failure, ctx.cd(".."), ctx.options(workdir="../.."):
92
- ctx.run("")
93
- records.append(failure.value.code)
94
- with pytest.raises(DutyFailure) as failure, ctx.cd("../../.."):
95
- ctx.run("", workdir="..")
96
- records.append(failure.value.code)
97
-
98
- base = records[0]
99
- assert records == [base, base - 1, base - 2, base - 3]
@@ -1,26 +0,0 @@
1
- """Tests for the `decorator` module."""
2
-
3
- from __future__ import annotations
4
-
5
- import inspect
6
-
7
- import pytest
8
-
9
- from duty.context import Context
10
- from duty.decorator import duty as decorate
11
- from duty.exceptions import DutyFailure
12
-
13
-
14
- def test_accept_one_posarg_when_decorating() -> None:
15
- """Accept only one positional argument when decorating."""
16
- with pytest.raises(ValueError, match="accepts only one positional argument"):
17
- decorate(0, 1) # type: ignore[call-overload]
18
-
19
-
20
- def test_skipping() -> None:
21
- """Wrap function that must be skipped."""
22
- duty = decorate(lambda ctx: ctx.run("false"), skip_if=True) # type: ignore[call-overload]
23
- # no DutyFailure raised
24
- assert duty.run() is None
25
- with pytest.raises(DutyFailure):
26
- assert inspect.unwrap(duty)(Context({}))
@@ -1,114 +0,0 @@
1
- """Tests about running duties."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import NoReturn
6
- from unittest.mock import NonCallableMock
7
-
8
- import pytest
9
-
10
- from duty.collection import Collection, Duty
11
- from duty.decorator import duty as decorate
12
- from duty.exceptions import DutyFailure
13
-
14
- INTERRUPT_CODE = 130
15
-
16
-
17
- def test_run_duty() -> None:
18
- """Run a duty."""
19
- duty = Duty("name", "description", lambda ctx: 1)
20
- assert duty.run() is None # type: ignore[func-returns-value]
21
- assert duty(duty.context) is None # type: ignore[func-returns-value]
22
-
23
-
24
- def test_run_pre_post_duties_lambdas() -> None:
25
- """Run pre- and post- duties as lambdas."""
26
- pre_calls = []
27
- post_calls = []
28
-
29
- duty = Duty(
30
- "name",
31
- "description",
32
- lambda ctx: None,
33
- pre=[lambda ctx: pre_calls.append(True)],
34
- post=[lambda ctx: post_calls.append(True)],
35
- )
36
-
37
- duty.run()
38
-
39
- assert pre_calls[0] is True
40
- assert post_calls[0] is True
41
-
42
-
43
- def test_run_pre_post_duties_instances() -> None:
44
- """Run pre- and post- duties as duties."""
45
- pre_calls = []
46
- post_calls = []
47
-
48
- pre_duty = Duty("pre", "", lambda ctx: pre_calls.append(True))
49
- post_duty = Duty("post", "", lambda ctx: post_calls.append(True))
50
-
51
- duty = Duty(
52
- name="name",
53
- description="description",
54
- function=lambda ctx: None,
55
- pre=[pre_duty],
56
- post=[post_duty],
57
- )
58
-
59
- duty.run()
60
-
61
- assert pre_calls[0] is True
62
- assert post_calls[0] is True
63
-
64
-
65
- def test_run_pre_post_duties_refs() -> None:
66
- """Run pre- and post- duties as duties references."""
67
- pre_calls = []
68
- post_calls = []
69
-
70
- collection = Collection()
71
- collection.add(decorate(lambda ctx: pre_calls.append(True), name="pre")) # type: ignore[call-overload]
72
- collection.add(decorate(lambda ctx: post_calls.append(True), name="post")) # type: ignore[call-overload]
73
-
74
- duty = Duty("name", "description", lambda ctx: None, collection=collection, pre=["pre"], post=["post"])
75
- duty.run()
76
-
77
- assert pre_calls[0] is True
78
- assert post_calls[0] is True
79
-
80
-
81
- def test_dont_run_other_pre_post_duties() -> None:
82
- """Don't run other types of pre- and post- duties."""
83
- pre_duty = NonCallableMock()
84
- post_duty = NonCallableMock()
85
-
86
- duty = Duty("name", "description", lambda ctx: 0, pre=[pre_duty], post=[post_duty])
87
- duty.run()
88
-
89
- assert not pre_duty.called
90
- assert not post_duty.called
91
-
92
-
93
- def test_code_when_keyboard_interrupt() -> None:
94
- """Return a code 130 on keyboard interruption."""
95
-
96
- def interrupt() -> NoReturn:
97
- raise KeyboardInterrupt
98
-
99
- with pytest.raises(DutyFailure) as excinfo:
100
- Duty("name", "description", lambda ctx: ctx.run(interrupt)).run()
101
- assert excinfo.value.code == INTERRUPT_CODE
102
-
103
-
104
- def test_dont_raise_duty_failure() -> None:
105
- """Don't raise a duty failure on success."""
106
- duty = Duty("n", "d", lambda ctx: ctx.run(lambda: 0))
107
- assert not duty.run() # type: ignore[func-returns-value]
108
-
109
-
110
- def test_cant_find_duty_without_collection() -> None:
111
- """Check that we can't find a duty with its name without a collection."""
112
- duty = decorate(lambda ctx: None, name="duty1", post=["duty2"]) # type: ignore[call-overload]
113
- with pytest.raises(RuntimeError):
114
- duty.run()
@@ -1,147 +0,0 @@
1
- """Tests for the `validation` module."""
2
-
3
- from __future__ import annotations
4
-
5
- from inspect import Parameter
6
- from typing import Any, Callable
7
-
8
- import pytest
9
-
10
- from duty.validation import _get_params_caster, cast_arg, to_bool
11
- from tests.fixtures import validation as valfix
12
-
13
-
14
- @pytest.mark.parametrize(
15
- ("value", "expected"),
16
- [
17
- ("y", True),
18
- ("Y", True),
19
- ("yes", True),
20
- ("YES", True),
21
- ("on", True),
22
- ("ON", True),
23
- ("true", True),
24
- ("TRUE", True),
25
- ("anything else", True),
26
- ("-1", True),
27
- ("1", True),
28
- ("", False),
29
- ("n", False),
30
- ("N", False),
31
- ("no", False),
32
- ("NO", False),
33
- ("false", False),
34
- ("FALSE", False),
35
- ("off", False),
36
- ("OFF", False),
37
- ],
38
- )
39
- def test_bool_casting(value: str, expected: bool) -> None:
40
- """Check that we correctly cast string values to booleans.
41
-
42
- Parameters:
43
- value: The value to cast.
44
- expected: The expected result.
45
- """
46
- assert to_bool(value) == expected
47
-
48
-
49
- class CustomType1:
50
- """Dummy type to test type-casting."""
51
-
52
- def __init__(self, value: str): # noqa: D107
53
- self.value = value
54
-
55
- def __eq__(self, other: object):
56
- return self.value == other.value # type: ignore[attr-defined]
57
-
58
-
59
- class CustomType2:
60
- """Dummy type to test type-casting."""
61
-
62
- def __init__(self, value, extra): # noqa: ANN001,D107
63
- ... # pragma: no cover
64
-
65
-
66
- @pytest.mark.parametrize(
67
- ("arg", "annotation", "expected"),
68
- [
69
- ("hello", Parameter.empty, "hello"),
70
- ("off", bool, False),
71
- ("on", bool, True),
72
- ("1", int, 1),
73
- ("1", float, 1.0),
74
- ("fie", str, "fie"),
75
- ("fih", CustomType1, CustomType1("fih")),
76
- ("foh", CustomType2, "foh"),
77
- ],
78
- )
79
- def test_cast_arg(arg: str, annotation: Any, expected: Any) -> None:
80
- """Check that arguments are properly casted given an annotation.
81
-
82
- Parameters:
83
- arg: The argument value to cast.
84
- annotation: The annotation to use.
85
- expected: The expected result.
86
- """
87
- assert cast_arg(arg, annotation) == expected
88
-
89
-
90
- _parametrization = [
91
- (valfix.no_params, (), {}, (), {}),
92
- (valfix.pos_or_kw_param, ("1",), {}, (1,), {}),
93
- (valfix.pos_or_kw_param, (), {"a": "1"}, (), {"a": 1}),
94
- (valfix.pos_or_kw_params, ("1", "2"), {}, (1, 2), {}),
95
- (valfix.pos_or_kw_params, ("1",), {"b": "2"}, (1,), {"b": 2}),
96
- (valfix.pos_or_kw_params, (), {"a": "1", "b": "2"}, (), {"a": 1, "b": 2}),
97
- (valfix.varpos_param, (), {}, (), {}),
98
- (valfix.varpos_param, ("1", "2"), {}, (1, 2), {}),
99
- (valfix.pos_and_varpos_param, ("1",), {}, (1,), {}),
100
- (valfix.pos_and_varpos_param, ("1", "2"), {}, (1, 2), {}),
101
- (valfix.pos_and_varpos_param, ("1", "2", "3"), {}, (1, 2, 3), {}),
102
- (valfix.kwonly_param, (), {"b": "1"}, (), {"b": 1}),
103
- (valfix.kwonly_param, ("2",), {"b": "1"}, (2,), {"b": 1}),
104
- (valfix.kwonly_param, ("2", "3"), {"b": "1"}, (2, 3), {"b": 1}),
105
- (valfix.varkw_param, ("1",), {}, (1,), {}),
106
- (valfix.varkw_param, ("1",), {"b": "2"}, (1,), {"b": 2}),
107
- (valfix.varkw_param, ("1",), {"b": "2", "c": "3"}, (1,), {"b": 2, "c": 3}),
108
- (valfix.varkw_no_annotation, (), {"a": "1"}, (), {"a": "1"}),
109
- (valfix.posonly_marker, ("1", "2"), {}, (1, 2), {}),
110
- (valfix.posonly_marker, ("1",), {"b": "2"}, (1,), {"b": 2}),
111
- (valfix.kwonly_marker, ("1",), {"b": "2"}, (1,), {"b": 2}),
112
- (valfix.kwonly_marker, (), {"a": "1", "b": "2"}, (), {"a": 1, "b": 2}),
113
- (valfix.only_markers, ("1",), {"b": "2", "c": "3"}, (1,), {"b": 2, "c": 3}),
114
- (valfix.only_markers, ("1", "2"), {"c": "3"}, (1, 2), {"c": 3}),
115
- (valfix.full, ("1", "2", "3", "4"), {"d": "5", "e": "6", "f": "7"}, (1, 2, 3, 4), {"d": 5, "e": 6, "f": 7}),
116
- ]
117
-
118
-
119
- @pytest.mark.parametrize(
120
- ("func", "args", "kwargs", "expected_args", "expected_kwargs"),
121
- _parametrization,
122
- )
123
- def test_params_caster(func: Callable, args: tuple, kwargs: dict, expected_args: tuple, expected_kwargs: dict) -> None:
124
- """Test the whole parameters casting helper class.
125
-
126
- Parameters:
127
- func: The function to work with.
128
- args: The positional arguments to cast.
129
- kwargs: The keyword arguments to cast.
130
- expected_args: The expected positional arguments result.
131
- expected_kwargs: The expected keyword arguments result.
132
- """
133
- caster = _get_params_caster(func, *args, **kwargs)
134
- new_args, new_kwargs = caster.cast(*args, **kwargs)
135
- assert new_args == expected_args
136
- assert new_kwargs == expected_kwargs
137
-
138
-
139
- def test_casting_based_on_default_value_type() -> None:
140
- """Test that we cast according to the default value type when there is no annotation."""
141
-
142
- def func(ctx, a=0): # noqa: ANN202,ARG001,ANN001
143
- ...
144
-
145
- caster = _get_params_caster(func, a="1")
146
- _, kwargs = caster.cast(a="1")
147
- assert kwargs == {"a": 1}
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