Coverage for src/duty/context.py: 94.64%
48 statements
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-17 17:18 +0200
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-17 17:18 +0200
1"""Module containing the context definition."""
3from __future__ import annotations
5import os
6from contextlib import contextmanager, suppress
7from typing import TYPE_CHECKING, Any, Callable, Union
9from failprint.runners import run as failprint_run
11from duty.exceptions import DutyFailure
12from duty.tools import Tool
14if TYPE_CHECKING:
15 from collections.abc import Iterator
17CmdType = Union[str, list[str], Callable]
20class Context:
21 """A simple context class.
23 Context instances are passed to functions decorated with `duty`.
24 """
26 def __init__(self, options: dict[str, Any], options_override: dict[str, Any] | None = None) -> None:
27 """Initialize the context.
29 Parameters:
30 options: Base options specified in `@duty(**options)`.
31 options_override: Options that override `run` and `@duty` options.
32 This argument is used to allow users to override options from the CLI or environment.
33 """
34 self._options = options
35 self._option_stack: list[dict[str, Any]] = []
36 self._options_override = options_override or {}
38 @contextmanager
39 def cd(self, directory: str) -> Iterator:
40 """Change working directory as a context manager.
42 Parameters:
43 directory: The directory to go into.
45 Yields:
46 Nothing.
47 """
48 if not directory:
49 yield
50 return
51 old_wd = os.getcwd()
52 os.chdir(directory)
53 try:
54 yield
55 finally:
56 os.chdir(old_wd)
58 def run(self, cmd: CmdType, **options: Any) -> str:
59 """Run a command in a subprocess or a Python callable.
61 Parameters:
62 cmd: A command or a Python callable.
63 options: Options passed to `failprint` functions.
65 Raises:
66 DutyFailure: When the exit code / function result is greather than 0.
68 Returns:
69 The output of the command.
70 """
71 final_options = dict(self._options)
72 final_options.update(options)
74 if "command" not in final_options and isinstance(cmd, Tool): 74 ↛ 75line 74 didn't jump to line 75 because the condition on line 74 was never true
75 with suppress(ValueError):
76 final_options["command"] = cmd.cli_command
78 allow_overrides = final_options.pop("allow_overrides", True)
79 workdir = final_options.pop("workdir", None)
81 if allow_overrides:
82 final_options.update(self._options_override)
84 with self.cd(workdir):
85 try:
86 result = failprint_run(cmd, **final_options)
87 except KeyboardInterrupt as ki:
88 raise DutyFailure(130) from ki
90 if result.code:
91 raise DutyFailure(result.code)
93 return result.output
95 @contextmanager
96 def options(self, **opts: Any) -> Iterator:
97 """Change options as a context manager.
99 Can be nested as will, previous options will pop once out of the with clause.
101 Parameters:
102 **opts: Options used in `run`.
104 Yields:
105 Nothing.
106 """
107 self._option_stack.append(self._options)
108 self._options = {**self._options, **opts}
109 try:
110 yield
111 finally:
112 self._options = self._option_stack.pop()