fancy-subprocess 1.0__tar.gz → 2.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.
- {fancy_subprocess-1.0 → fancy_subprocess-2.0}/PKG-INFO +94 -51
- fancy_subprocess-2.0/README.md +259 -0
- fancy_subprocess-2.0/fancy_subprocess/__init__.py +6 -0
- fancy_subprocess-2.0/fancy_subprocess/_compat.py +21 -0
- fancy_subprocess-2.0/fancy_subprocess/_print.py +39 -0
- fancy_subprocess-2.0/fancy_subprocess/_reconfigure.py +33 -0
- fancy_subprocess-2.0/fancy_subprocess/_run_core.py +222 -0
- fancy_subprocess-2.0/fancy_subprocess/_run_param.py +72 -0
- fancy_subprocess-2.0/fancy_subprocess/_run_wrappers.py +67 -0
- fancy_subprocess-2.0/fancy_subprocess/_utils.py +52 -0
- fancy_subprocess-2.0/grab_output.py +26 -0
- {fancy_subprocess-1.0 → fancy_subprocess-2.0}/pyproject.toml +6 -3
- fancy_subprocess-2.0/uv.lock +84 -0
- fancy_subprocess-1.0/README.md +0 -219
- fancy_subprocess-1.0/fancy_subprocess/__init__.py +0 -403
- {fancy_subprocess-1.0 → fancy_subprocess-2.0}/.editorconfig +0 -0
- {fancy_subprocess-1.0 → fancy_subprocess-2.0}/.gitignore +0 -0
- {fancy_subprocess-1.0 → fancy_subprocess-2.0}/LICENSE +0 -0
- {fancy_subprocess-1.0 → fancy_subprocess-2.0}/fancy_subprocess/py.typed +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fancy_subprocess
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0
|
|
4
4
|
Summary: subprocess.run() with formatted output, detailed error messages and retry capabilities
|
|
5
|
-
Project-URL: Homepage, https://github.com/petamas/fancy-subprocess
|
|
6
|
-
Project-URL: Bug Tracker, https://github.com/petamas/fancy-subprocess/issues
|
|
5
|
+
Project-URL: Homepage, https://github.com/petamas/python-fancy-subprocess
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/petamas/python-fancy-subprocess/issues
|
|
7
7
|
Author-email: Tamás PEREGI <petamas@gmail.com>
|
|
8
8
|
License-Expression: MIT
|
|
9
9
|
License-File: LICENSE
|
|
@@ -20,15 +20,16 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
20
20
|
Requires-Python: >=3.10
|
|
21
21
|
Requires-Dist: ntstatus<2
|
|
22
22
|
Requires-Dist: oslex<2
|
|
23
|
+
Requires-Dist: pathext<2,>=1.2
|
|
24
|
+
Requires-Dist: typeguard<5,>=4.4.2
|
|
25
|
+
Requires-Dist: typing-extensions<5,>=4.14
|
|
23
26
|
Description-Content-Type: text/markdown
|
|
24
27
|
|
|
25
28
|
# fancy-subprocess
|
|
26
29
|
|
|
27
30
|
`fancy-subprocess` provides variants of `subprocess.run()` with formatted output, detailed error messages and retry capabilities.
|
|
28
31
|
|
|
29
|
-
##
|
|
30
|
-
|
|
31
|
-
### `fancy_subprocess.run()`
|
|
32
|
+
## `fancy_subprocess.run()` and related functionality
|
|
32
33
|
|
|
33
34
|
An extended (and in some aspects, constrained) version of `subprocess.run()`. It runs a command and prints its output line-by-line using a customizable `print_output` function, while printing informational messages (eg. which command it is running) using a customizable `print_message` function.
|
|
34
35
|
|
|
@@ -37,47 +38,54 @@ Key differences compared to `subprocess.run()`:
|
|
|
37
38
|
- The command's stdout and stderr is always combined into a single stream. (Like `subprocess.run(stderr=STDOUT)`.)
|
|
38
39
|
- The output of the command is always assumed to be textual, not binary. (Like `subprocess.run(text=True)`.)
|
|
39
40
|
- The output of the command is always captured, but it is also immediately printed using `print_output`.
|
|
40
|
-
- The exit code of the command is checked, and an exception is raised on failure, like `subprocess.run(check=True)`, but the list of exit codes treated as success is customizable, and the raised exception is `
|
|
41
|
-
- `OSError` is never raised, it gets converted to `
|
|
42
|
-
- `
|
|
41
|
+
- The exit code of the command is checked, and an exception is raised on failure, like `subprocess.run(check=True)`, but the list of exit codes treated as success is customizable, and the raised exception is `RunError` instead of `CalledProcessError`.
|
|
42
|
+
- `OSError` is never raised, it gets converted to `RunError`.
|
|
43
|
+
- `RunResult` is returned instead of `CompletedProcess` on success.
|
|
43
44
|
|
|
44
45
|
Arguments (all of them except `cmd` are optional):
|
|
45
46
|
- `cmd: Sequence[str | Path]` - Command to run. See `subprocess.run()`'s documentation for the interpretation of `cmd[0]`. It is recommended to use `fancy_subprocess.which()` to produce `cmd[0]`.
|
|
46
|
-
- `print_message:
|
|
47
|
-
-
|
|
48
|
-
- `
|
|
49
|
-
-
|
|
50
|
-
- `
|
|
51
|
-
- `
|
|
52
|
-
- `
|
|
53
|
-
- `
|
|
54
|
-
-
|
|
55
|
-
- `
|
|
56
|
-
- `
|
|
57
|
-
- `
|
|
58
|
-
- `
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
`
|
|
47
|
+
- `print_message: Callable[[str], None]` - Function used to print informational messages. If unspecified or set to `None`, defaults to `fancy_subprocess.default_print`. Use `message_quiet=True` to disable printing informational messages.
|
|
48
|
+
- The type of this argument is also aliased as `fancy_subprocess.PrintFunction`.
|
|
49
|
+
- `print_output: Callable[[str], None]` - Function used to print a line of the output of the command. If unspecified or set to `None`, defaults to `fancy_subprocess.default_print`. Use `output_quiet=True` to disable printing the command's output.
|
|
50
|
+
- The type of this argument is also aliased as `fancy_subprocess.PrintFunction`.
|
|
51
|
+
- `message_quiet: bool` - If `True`, `print_message` is ignored, and no informational messages are printed. If unspecified or set to `None`, defaults to `False`.
|
|
52
|
+
- `output_quiet: bool` - If `True`, `print_output` is ignored, and the command's output it not printed. If unspecified or set to `None`, defaults to `False`. Note that this parameter also affects the default value of `description`.
|
|
53
|
+
- `description: str` - Description printed before running the command. If unspecified or set to `None`, defaults to `Running command: ...` when `output_quiet` is `False`, and `Running command (output silenced): ...` when `output_quiet` is `True`.
|
|
54
|
+
- `success: Sequence[int] | AnyExitCode` - List of exit codes that should be considered successful. If set to `fancy_subprocess.ANY_EXIT_CODE`, then all exit codes are considered successful. If unspecified or set to `None`, defaults to `[0]`. Note that 0 is not automatically included in the list of successful exit codes, so if a list without 0 is specified, then the function will consider 0 a failure.
|
|
55
|
+
- The type of this argument is also aliased as `fancy_subprocess.Success`.
|
|
56
|
+
- `flush_before_subprocess: bool` - If `True`, flushes both the standard output and error streams before running the command. If unspecified or set to `None`, defaults to `True`.
|
|
57
|
+
- `trim_output_lines: bool` - If `True`, remove trailing whitespace from the lines of the output of the command before calling `print_output` and adding them to the `output` field of `RunResult`. If unspecified or set to `None`, defaults to `True`.
|
|
58
|
+
- `max_output_size: int` - Maximum number of characters to be recorded in the `output` field of `RunResult`. If the command produces more than `max_output_size` characters, only the last `max_output_size` will be recorded. If unspecified or set to `None`, defaults to 10,000,000.
|
|
59
|
+
- `retry: int` - Number of times to retry running the command on failure. Note that the total number of attempts is one greater than what's specified. (I.e. `retry=2` attempts to run the command 3 times.) If unspecified or set to `None`, defaults to 0.
|
|
60
|
+
- `retry_initial_sleep_seconds: float` - Number of seconds to wait before retrying for the first time. If unspecified or set to `None`, defaults to 10.
|
|
61
|
+
- `retry_backoff: float` - Factor used to increase wait times before subsequent retries. If unspecified or set to `None`, defaults to 2.
|
|
62
|
+
- `env_overrides: Mapping[str, str]` - Dictionary used to set environment variables. Note that unline the `env` argument of `subprocess.run()`, `env_overrides` does not need to contain all environment variables, only the ones you want to add/modify compared to os.environ. If unspecified or set to `None`, defaults to empty dictionary, i.e. no change to the environment.
|
|
63
|
+
- The type of this argument is also aliased as `fancy_subprocess.EnvOverrides`.
|
|
64
|
+
- `cwd: str | Path` - If not `None`, change current working directory to `cwd` before running the command.
|
|
65
|
+
- `encoding: str` - This encoding will be used to open stdout and stderr of the command. If unspecified or set to `None`, see default behaviour in `io.TextIOWrapper`'s documentation.
|
|
66
|
+
- `errors: str` - This specifies how text decoding errors will be handled. See details (including what happens if unspecified or set to `None`) in `io.TextIOWrapper`'s documentation.
|
|
67
|
+
|
|
68
|
+
### Return value: `fancy_subprocess.RunResult`
|
|
69
|
+
|
|
70
|
+
`fancy_subprocess.run()` and similar functions return a `RunResult` instance on success.
|
|
71
|
+
|
|
72
|
+
`RunResult` has the following properties:
|
|
65
73
|
- `exit_code: int` - Exit code of the finished process. (On Windows, this is a signed `int32` value, i.e. in the range of \[-2<sup>31</sup>, 2<sup>31</sup>-1\].)
|
|
66
74
|
- `output: str` - Combination of the process's output on stdout and stderr.
|
|
67
75
|
|
|
68
|
-
|
|
76
|
+
### Exception: `fancy_subprocess.RunError`
|
|
69
77
|
|
|
70
|
-
`fancy_subprocess.run()` and similar functions raise `
|
|
78
|
+
`fancy_subprocess.run()` and similar functions raise `RunError` on error. There are two kinds of errors that result in a `RunError`:
|
|
71
79
|
- If the requested command has failed, the `completed` property will be `True`, and the `exit_code` and `output` properties will be set.
|
|
72
80
|
- If the command couldn't be run (eg. because the executable wasn't found), the `completed` property will be `False`, and the `oserror` property will be set to the `OSError` exception instance originally raised by the underlying `subprocess.Popen()` call.
|
|
73
81
|
|
|
74
|
-
Calling `str()` on a `
|
|
82
|
+
Calling `str()` on a `RunError` object returns a detailed one-line description of the error:
|
|
75
83
|
- The failed command is included in the message.
|
|
76
84
|
- If an `OSError` happened, its message is included in the message.
|
|
77
85
|
- On Windows, if the exit code of the process is recognized as a known `NTSTATUS` error value, its name is included in the message, otherwise its hexadecimal representation is included (to make searching it on the internet easier).
|
|
78
86
|
- On Unix systems, if the exit code represents a signal, its name is included in the message.
|
|
79
87
|
|
|
80
|
-
`
|
|
88
|
+
`RunError` has the following properties:
|
|
81
89
|
- `cmd: Sequence[str | Path]` - Original command passed to `fancy_subprocess.run()`.
|
|
82
90
|
- `completed: bool` - `True` if the process completed (with an error), `False` if the underlying `subprocess.Popen()` call raised an OSError (eg. because it could not start the process).
|
|
83
91
|
- `exit_code: int` - Exit code of the completed process. Raises `ValueError` if `completed` is `False`.
|
|
@@ -88,9 +96,9 @@ Calling `str()` on a `RunProcessError` object returns a detailed one-line descri
|
|
|
88
96
|
|
|
89
97
|
Specialized version of `fancy_subprocess.run()`, primarily used to run a command and later process its output.
|
|
90
98
|
|
|
91
|
-
Differences
|
|
92
|
-
- `
|
|
93
|
-
- `
|
|
99
|
+
Differences compared to `fancy_subprocess.run()`:
|
|
100
|
+
- `output_quiet` cannot be set from the calling side, it is always set to `True`. Note that this affects `description`'s default value.
|
|
101
|
+
- `print_output` cannot be set from the calling side (because it wouldn't matter anyway because of `output_quiet=True`).
|
|
94
102
|
|
|
95
103
|
All other `fancy_subprocess.run()` arguments are available and behave the same.
|
|
96
104
|
|
|
@@ -102,22 +110,51 @@ The `print_output` argument is replaced by `indent`, which can be set to either
|
|
|
102
110
|
|
|
103
111
|
All other `fancy_subprocess.run()` arguments are available and behave the same.
|
|
104
112
|
|
|
105
|
-
###
|
|
113
|
+
### Writing your own wrapper
|
|
114
|
+
|
|
115
|
+
Most projects will likely use `fancy_subprocess.run()` through their own wrapper around it, customizing the behaviour of the function using its various arguments. To simplify writing wrappers that specify some of the arguments, and expose the rest to callers, `fancy_subprocess` provides a couple helpers, allowing you to write type-safe wrappers like this:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
import fancy_subprocess
|
|
119
|
+
from typing import Unpack
|
|
120
|
+
|
|
121
|
+
def grab_output(cmd: list[str], **kwargs: Unpack[fancy_subprocess.RunParams]) -> str:
|
|
122
|
+
# Raises ValueError if there are unknown parameters in kwargs or if a keyword argument's type is incorrect
|
|
123
|
+
fancy_subprocess.check_run_params(**kwargs)
|
|
124
|
+
|
|
125
|
+
# Make a copy of keyword arguments to be edited
|
|
126
|
+
forwarded_args = kwargs.copy()
|
|
127
|
+
# Make sure nothing's printed, raise ValueError if caller tries to specify "output_quiet" or "message_quiet"
|
|
128
|
+
fancy_subprocess.force_run_params(forwarded_args, message_quiet=True, output_quiet=True)
|
|
129
|
+
# Handle encoding/decoding errors by replacing them with placeholder character by default, but allow callers to still customize behaviour
|
|
130
|
+
fancy_subprocess.change_default_run_params(forwarded_args, errors='replace')
|
|
106
131
|
|
|
107
|
-
|
|
132
|
+
# Run command, raise fancy_subprocess.RunError on failure
|
|
133
|
+
result = fancy_subprocess.run(cmd, **forwarded_args)
|
|
108
134
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
135
|
+
# Return combined stdout and stderr
|
|
136
|
+
return result.output
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The `grab_output()` function supports all `fancy_subprocess.run()` arguments (eg. `retry`), except for `message_quiet` and `output_quiet`. If `errors` is unspecified or set to `None`, it uses `errors='replace'` instead of the default `errors='strict'` behaviour. It also passes `mypy --strict`.
|
|
140
|
+
|
|
141
|
+
(Using `typing.Unpack` requires Python 3.11 or later. In Python 3.10, use the `typing_extensions` module from PyPI.)
|
|
142
|
+
|
|
143
|
+
### Predefined printing functions
|
|
113
144
|
|
|
114
|
-
|
|
145
|
+
There are various predefined functions projects can use as the `print_message` and `print_output` parameters of `fancy_subprocess.run()`:
|
|
115
146
|
|
|
116
|
-
|
|
147
|
+
- `fancy_subprocess.default_print` prints the line to `sys.stdout`, then flushes it.
|
|
148
|
+
- `fancy_subprocess.errors_print` prints the line to `sys.stderr`, then flushes it.
|
|
149
|
+
- `fancy_subprocess.silenced_print` does not print anything. It can be used as an alternative to `output_quiet=True` if the caller does not want to change the default `description`.
|
|
150
|
+
- `fancy_subprocess.indented_print` prints the line to `sys.stdout` indented by 4 spaces, then flushes the file.
|
|
151
|
+
- `fancy_subprocess.indented_print_factory(indent)` returns a function that calls `fancy_subprocess.indented_print` with its parameter and the specified indent instead of the default 4 spaces.
|
|
152
|
+
- `logging.error(line)`, `logging.info(line)`, etc. can be used to redirect the messages (or even the output) to Python's builtin logging subsystem.
|
|
153
|
+
- The builtin `print` function can also be used to print without flushing.
|
|
117
154
|
|
|
118
|
-
|
|
155
|
+
### Example outputs
|
|
119
156
|
|
|
120
|
-
|
|
157
|
+
#### Success
|
|
121
158
|
|
|
122
159
|
Take this script:
|
|
123
160
|
|
|
@@ -169,7 +206,7 @@ Running the script will produce the following output (on Windows):
|
|
|
169
206
|
```
|
|
170
207
|
|
|
171
208
|
|
|
172
|
-
|
|
209
|
+
#### Failed command on Windows
|
|
173
210
|
|
|
174
211
|
Take this script:
|
|
175
212
|
|
|
@@ -182,7 +219,7 @@ try:
|
|
|
182
219
|
[sys.executable, '-c', 'import sys; print("Noooooo!"); sys.exit(-1072103376)'],
|
|
183
220
|
description='Demonstrating failure...',
|
|
184
221
|
)
|
|
185
|
-
except fancy_subprocess.
|
|
222
|
+
except fancy_subprocess.RunError as e:
|
|
186
223
|
print(e)
|
|
187
224
|
```
|
|
188
225
|
|
|
@@ -194,7 +231,7 @@ Noooooo!
|
|
|
194
231
|
Command failed with exit code -1072103376 (STATUS_LOG_CORRUPTION_DETECTED): d:\projects\python-libs\fancy_subprocess\.venv\Scripts\python.exe -c "import sys; print("\^"Noooooo^!\^""); sys.exit(-1072103376)"
|
|
195
232
|
```
|
|
196
233
|
|
|
197
|
-
|
|
234
|
+
#### Killed command on Linux
|
|
198
235
|
|
|
199
236
|
Take this script:
|
|
200
237
|
|
|
@@ -207,7 +244,7 @@ try:
|
|
|
207
244
|
[sys.executable, '-c', 'import time; time.sleep(60)'],
|
|
208
245
|
description='Sweet dreams!',
|
|
209
246
|
)
|
|
210
|
-
except fancy_subprocess.
|
|
247
|
+
except fancy_subprocess.RunError as e:
|
|
211
248
|
print(e)
|
|
212
249
|
```
|
|
213
250
|
|
|
@@ -218,7 +255,7 @@ Sweet dreams!
|
|
|
218
255
|
Command failed with exit code -9 (SIGKILL): /home/petamas/.venv/bin/python -c 'import time; time.sleep(60)'
|
|
219
256
|
```
|
|
220
257
|
|
|
221
|
-
|
|
258
|
+
#### Failure to find executable
|
|
222
259
|
|
|
223
260
|
Take this script:
|
|
224
261
|
|
|
@@ -227,7 +264,7 @@ import fancy_subprocess
|
|
|
227
264
|
|
|
228
265
|
try:
|
|
229
266
|
fancy_subprocess.run(['foo', '--bar', 'baz'])
|
|
230
|
-
except fancy_subprocess.
|
|
267
|
+
except fancy_subprocess.RunError as e:
|
|
231
268
|
print(e)
|
|
232
269
|
```
|
|
233
270
|
|
|
@@ -238,6 +275,12 @@ Running command: foo --bar baz
|
|
|
238
275
|
Exception FileNotFoundError with message "[Errno 2] No such file or directory: 'foo'" was raised while trying to run command: foo --bar baz
|
|
239
276
|
```
|
|
240
277
|
|
|
278
|
+
## Other utilities
|
|
279
|
+
|
|
280
|
+
### `fancy_subprocess.reconfigure_standard_output_streams()`
|
|
281
|
+
|
|
282
|
+
Calls `sys.stdout.reconfigure()` and `sys.stderr.reconfigure()` with the provided parameters. Raises `TypeError` if either `sys.stdout` or `sys.stderr` is not an instance of `io.TextIOWrapper`.
|
|
283
|
+
|
|
241
284
|
## Licensing
|
|
242
285
|
|
|
243
286
|
This library is licensed under the MIT license.
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# fancy-subprocess
|
|
2
|
+
|
|
3
|
+
`fancy-subprocess` provides variants of `subprocess.run()` with formatted output, detailed error messages and retry capabilities.
|
|
4
|
+
|
|
5
|
+
## `fancy_subprocess.run()` and related functionality
|
|
6
|
+
|
|
7
|
+
An extended (and in some aspects, constrained) version of `subprocess.run()`. It runs a command and prints its output line-by-line using a customizable `print_output` function, while printing informational messages (eg. which command it is running) using a customizable `print_message` function.
|
|
8
|
+
|
|
9
|
+
Key differences compared to `subprocess.run()`:
|
|
10
|
+
- The command must be specified as a list, simply specifying a string is not allowed.
|
|
11
|
+
- The command's stdout and stderr is always combined into a single stream. (Like `subprocess.run(stderr=STDOUT)`.)
|
|
12
|
+
- The output of the command is always assumed to be textual, not binary. (Like `subprocess.run(text=True)`.)
|
|
13
|
+
- The output of the command is always captured, but it is also immediately printed using `print_output`.
|
|
14
|
+
- The exit code of the command is checked, and an exception is raised on failure, like `subprocess.run(check=True)`, but the list of exit codes treated as success is customizable, and the raised exception is `RunError` instead of `CalledProcessError`.
|
|
15
|
+
- `OSError` is never raised, it gets converted to `RunError`.
|
|
16
|
+
- `RunResult` is returned instead of `CompletedProcess` on success.
|
|
17
|
+
|
|
18
|
+
Arguments (all of them except `cmd` are optional):
|
|
19
|
+
- `cmd: Sequence[str | Path]` - Command to run. See `subprocess.run()`'s documentation for the interpretation of `cmd[0]`. It is recommended to use `fancy_subprocess.which()` to produce `cmd[0]`.
|
|
20
|
+
- `print_message: Callable[[str], None]` - Function used to print informational messages. If unspecified or set to `None`, defaults to `fancy_subprocess.default_print`. Use `message_quiet=True` to disable printing informational messages.
|
|
21
|
+
- The type of this argument is also aliased as `fancy_subprocess.PrintFunction`.
|
|
22
|
+
- `print_output: Callable[[str], None]` - Function used to print a line of the output of the command. If unspecified or set to `None`, defaults to `fancy_subprocess.default_print`. Use `output_quiet=True` to disable printing the command's output.
|
|
23
|
+
- The type of this argument is also aliased as `fancy_subprocess.PrintFunction`.
|
|
24
|
+
- `message_quiet: bool` - If `True`, `print_message` is ignored, and no informational messages are printed. If unspecified or set to `None`, defaults to `False`.
|
|
25
|
+
- `output_quiet: bool` - If `True`, `print_output` is ignored, and the command's output it not printed. If unspecified or set to `None`, defaults to `False`. Note that this parameter also affects the default value of `description`.
|
|
26
|
+
- `description: str` - Description printed before running the command. If unspecified or set to `None`, defaults to `Running command: ...` when `output_quiet` is `False`, and `Running command (output silenced): ...` when `output_quiet` is `True`.
|
|
27
|
+
- `success: Sequence[int] | AnyExitCode` - List of exit codes that should be considered successful. If set to `fancy_subprocess.ANY_EXIT_CODE`, then all exit codes are considered successful. If unspecified or set to `None`, defaults to `[0]`. Note that 0 is not automatically included in the list of successful exit codes, so if a list without 0 is specified, then the function will consider 0 a failure.
|
|
28
|
+
- The type of this argument is also aliased as `fancy_subprocess.Success`.
|
|
29
|
+
- `flush_before_subprocess: bool` - If `True`, flushes both the standard output and error streams before running the command. If unspecified or set to `None`, defaults to `True`.
|
|
30
|
+
- `trim_output_lines: bool` - If `True`, remove trailing whitespace from the lines of the output of the command before calling `print_output` and adding them to the `output` field of `RunResult`. If unspecified or set to `None`, defaults to `True`.
|
|
31
|
+
- `max_output_size: int` - Maximum number of characters to be recorded in the `output` field of `RunResult`. If the command produces more than `max_output_size` characters, only the last `max_output_size` will be recorded. If unspecified or set to `None`, defaults to 10,000,000.
|
|
32
|
+
- `retry: int` - Number of times to retry running the command on failure. Note that the total number of attempts is one greater than what's specified. (I.e. `retry=2` attempts to run the command 3 times.) If unspecified or set to `None`, defaults to 0.
|
|
33
|
+
- `retry_initial_sleep_seconds: float` - Number of seconds to wait before retrying for the first time. If unspecified or set to `None`, defaults to 10.
|
|
34
|
+
- `retry_backoff: float` - Factor used to increase wait times before subsequent retries. If unspecified or set to `None`, defaults to 2.
|
|
35
|
+
- `env_overrides: Mapping[str, str]` - Dictionary used to set environment variables. Note that unline the `env` argument of `subprocess.run()`, `env_overrides` does not need to contain all environment variables, only the ones you want to add/modify compared to os.environ. If unspecified or set to `None`, defaults to empty dictionary, i.e. no change to the environment.
|
|
36
|
+
- The type of this argument is also aliased as `fancy_subprocess.EnvOverrides`.
|
|
37
|
+
- `cwd: str | Path` - If not `None`, change current working directory to `cwd` before running the command.
|
|
38
|
+
- `encoding: str` - This encoding will be used to open stdout and stderr of the command. If unspecified or set to `None`, see default behaviour in `io.TextIOWrapper`'s documentation.
|
|
39
|
+
- `errors: str` - This specifies how text decoding errors will be handled. See details (including what happens if unspecified or set to `None`) in `io.TextIOWrapper`'s documentation.
|
|
40
|
+
|
|
41
|
+
### Return value: `fancy_subprocess.RunResult`
|
|
42
|
+
|
|
43
|
+
`fancy_subprocess.run()` and similar functions return a `RunResult` instance on success.
|
|
44
|
+
|
|
45
|
+
`RunResult` has the following properties:
|
|
46
|
+
- `exit_code: int` - Exit code of the finished process. (On Windows, this is a signed `int32` value, i.e. in the range of \[-2<sup>31</sup>, 2<sup>31</sup>-1\].)
|
|
47
|
+
- `output: str` - Combination of the process's output on stdout and stderr.
|
|
48
|
+
|
|
49
|
+
### Exception: `fancy_subprocess.RunError`
|
|
50
|
+
|
|
51
|
+
`fancy_subprocess.run()` and similar functions raise `RunError` on error. There are two kinds of errors that result in a `RunError`:
|
|
52
|
+
- If the requested command has failed, the `completed` property will be `True`, and the `exit_code` and `output` properties will be set.
|
|
53
|
+
- If the command couldn't be run (eg. because the executable wasn't found), the `completed` property will be `False`, and the `oserror` property will be set to the `OSError` exception instance originally raised by the underlying `subprocess.Popen()` call.
|
|
54
|
+
|
|
55
|
+
Calling `str()` on a `RunError` object returns a detailed one-line description of the error:
|
|
56
|
+
- The failed command is included in the message.
|
|
57
|
+
- If an `OSError` happened, its message is included in the message.
|
|
58
|
+
- On Windows, if the exit code of the process is recognized as a known `NTSTATUS` error value, its name is included in the message, otherwise its hexadecimal representation is included (to make searching it on the internet easier).
|
|
59
|
+
- On Unix systems, if the exit code represents a signal, its name is included in the message.
|
|
60
|
+
|
|
61
|
+
`RunError` has the following properties:
|
|
62
|
+
- `cmd: Sequence[str | Path]` - Original command passed to `fancy_subprocess.run()`.
|
|
63
|
+
- `completed: bool` - `True` if the process completed (with an error), `False` if the underlying `subprocess.Popen()` call raised an OSError (eg. because it could not start the process).
|
|
64
|
+
- `exit_code: int` - Exit code of the completed process. Raises `ValueError` if `completed` is `False`.
|
|
65
|
+
- `output: str` - Combination of the process's output on stdout and stderr. Raises `ValueError` if `completed` is `False`.
|
|
66
|
+
- `oserror: OSError` - The `OSError` raised by `subprocess.Popen()`. Raises `ValueError` if `completed` is `True`.
|
|
67
|
+
|
|
68
|
+
### `fancy_subprocess.run_silenced()`
|
|
69
|
+
|
|
70
|
+
Specialized version of `fancy_subprocess.run()`, primarily used to run a command and later process its output.
|
|
71
|
+
|
|
72
|
+
Differences compared to `fancy_subprocess.run()`:
|
|
73
|
+
- `output_quiet` cannot be set from the calling side, it is always set to `True`. Note that this affects `description`'s default value.
|
|
74
|
+
- `print_output` cannot be set from the calling side (because it wouldn't matter anyway because of `output_quiet=True`).
|
|
75
|
+
|
|
76
|
+
All other `fancy_subprocess.run()` arguments are available and behave the same.
|
|
77
|
+
|
|
78
|
+
### `fancy_subprocess.run_indented()`
|
|
79
|
+
|
|
80
|
+
Specialized version of `fancy_subprocess.run()` which prints the command's output indented by a user-defined amount.
|
|
81
|
+
|
|
82
|
+
The `print_output` argument is replaced by `indent`, which can be set to either the number of spaces to use for indentation or any custom indentation string (eg. `\t`).
|
|
83
|
+
|
|
84
|
+
All other `fancy_subprocess.run()` arguments are available and behave the same.
|
|
85
|
+
|
|
86
|
+
### Writing your own wrapper
|
|
87
|
+
|
|
88
|
+
Most projects will likely use `fancy_subprocess.run()` through their own wrapper around it, customizing the behaviour of the function using its various arguments. To simplify writing wrappers that specify some of the arguments, and expose the rest to callers, `fancy_subprocess` provides a couple helpers, allowing you to write type-safe wrappers like this:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
import fancy_subprocess
|
|
92
|
+
from typing import Unpack
|
|
93
|
+
|
|
94
|
+
def grab_output(cmd: list[str], **kwargs: Unpack[fancy_subprocess.RunParams]) -> str:
|
|
95
|
+
# Raises ValueError if there are unknown parameters in kwargs or if a keyword argument's type is incorrect
|
|
96
|
+
fancy_subprocess.check_run_params(**kwargs)
|
|
97
|
+
|
|
98
|
+
# Make a copy of keyword arguments to be edited
|
|
99
|
+
forwarded_args = kwargs.copy()
|
|
100
|
+
# Make sure nothing's printed, raise ValueError if caller tries to specify "output_quiet" or "message_quiet"
|
|
101
|
+
fancy_subprocess.force_run_params(forwarded_args, message_quiet=True, output_quiet=True)
|
|
102
|
+
# Handle encoding/decoding errors by replacing them with placeholder character by default, but allow callers to still customize behaviour
|
|
103
|
+
fancy_subprocess.change_default_run_params(forwarded_args, errors='replace')
|
|
104
|
+
|
|
105
|
+
# Run command, raise fancy_subprocess.RunError on failure
|
|
106
|
+
result = fancy_subprocess.run(cmd, **forwarded_args)
|
|
107
|
+
|
|
108
|
+
# Return combined stdout and stderr
|
|
109
|
+
return result.output
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The `grab_output()` function supports all `fancy_subprocess.run()` arguments (eg. `retry`), except for `message_quiet` and `output_quiet`. If `errors` is unspecified or set to `None`, it uses `errors='replace'` instead of the default `errors='strict'` behaviour. It also passes `mypy --strict`.
|
|
113
|
+
|
|
114
|
+
(Using `typing.Unpack` requires Python 3.11 or later. In Python 3.10, use the `typing_extensions` module from PyPI.)
|
|
115
|
+
|
|
116
|
+
### Predefined printing functions
|
|
117
|
+
|
|
118
|
+
There are various predefined functions projects can use as the `print_message` and `print_output` parameters of `fancy_subprocess.run()`:
|
|
119
|
+
|
|
120
|
+
- `fancy_subprocess.default_print` prints the line to `sys.stdout`, then flushes it.
|
|
121
|
+
- `fancy_subprocess.errors_print` prints the line to `sys.stderr`, then flushes it.
|
|
122
|
+
- `fancy_subprocess.silenced_print` does not print anything. It can be used as an alternative to `output_quiet=True` if the caller does not want to change the default `description`.
|
|
123
|
+
- `fancy_subprocess.indented_print` prints the line to `sys.stdout` indented by 4 spaces, then flushes the file.
|
|
124
|
+
- `fancy_subprocess.indented_print_factory(indent)` returns a function that calls `fancy_subprocess.indented_print` with its parameter and the specified indent instead of the default 4 spaces.
|
|
125
|
+
- `logging.error(line)`, `logging.info(line)`, etc. can be used to redirect the messages (or even the output) to Python's builtin logging subsystem.
|
|
126
|
+
- The builtin `print` function can also be used to print without flushing.
|
|
127
|
+
|
|
128
|
+
### Example outputs
|
|
129
|
+
|
|
130
|
+
#### Success
|
|
131
|
+
|
|
132
|
+
Take this script:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
import fancy_subprocess
|
|
136
|
+
import sys
|
|
137
|
+
|
|
138
|
+
fancy_subprocess.run_indented(
|
|
139
|
+
[sys.executable, '-m', 'venv', '--help'],
|
|
140
|
+
print_message=lambda msg: print(f'[script-name] {msg}'),
|
|
141
|
+
success=fancy_subprocess.ANY_EXIT_CODE)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Running the script will produce the following output (on Windows):
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
[script-name] Running command: d:\projects\python-libs\fancy_subprocess\.venv\Scripts\python.exe -m venv --help
|
|
148
|
+
usage: venv [-h] [--system-site-packages] [--symlinks | --copies] [--clear]
|
|
149
|
+
[--upgrade] [--without-pip] [--prompt PROMPT] [--upgrade-deps]
|
|
150
|
+
ENV_DIR [ENV_DIR ...]
|
|
151
|
+
|
|
152
|
+
Creates virtual Python environments in one or more target directories.
|
|
153
|
+
|
|
154
|
+
positional arguments:
|
|
155
|
+
ENV_DIR A directory to create the environment in.
|
|
156
|
+
|
|
157
|
+
options:
|
|
158
|
+
-h, --help show this help message and exit
|
|
159
|
+
--system-site-packages
|
|
160
|
+
Give the virtual environment access to the system
|
|
161
|
+
site-packages dir.
|
|
162
|
+
--symlinks Try to use symlinks rather than copies, when symlinks
|
|
163
|
+
are not the default for the platform.
|
|
164
|
+
--copies Try to use copies rather than symlinks, even when
|
|
165
|
+
symlinks are the default for the platform.
|
|
166
|
+
--clear Delete the contents of the environment directory if it
|
|
167
|
+
already exists, before environment creation.
|
|
168
|
+
--upgrade Upgrade the environment directory to use this version
|
|
169
|
+
of Python, assuming Python has been upgraded in-place.
|
|
170
|
+
--without-pip Skips installing or upgrading pip in the virtual
|
|
171
|
+
environment (pip is bootstrapped by default)
|
|
172
|
+
--prompt PROMPT Provides an alternative prompt prefix for this
|
|
173
|
+
environment.
|
|
174
|
+
--upgrade-deps Upgrade core dependencies: pip setuptools to the
|
|
175
|
+
latest version in PyPI
|
|
176
|
+
|
|
177
|
+
Once an environment has been created, you may wish to activate it, e.g. by
|
|
178
|
+
sourcing an activate script in its bin directory.
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
#### Failed command on Windows
|
|
183
|
+
|
|
184
|
+
Take this script:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
import fancy_subprocess
|
|
188
|
+
import sys
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
fancy_subprocess.run(
|
|
192
|
+
[sys.executable, '-c', 'import sys; print("Noooooo!"); sys.exit(-1072103376)'],
|
|
193
|
+
description='Demonstrating failure...',
|
|
194
|
+
)
|
|
195
|
+
except fancy_subprocess.RunError as e:
|
|
196
|
+
print(e)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Running the script on Windows will produce the following output (-1072103376 is the signed integer interpretation of 0xC0190030, i.e. `STATUS_LOG_CORRUPTION_DETECTED`):
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
Demonstrating failure...
|
|
203
|
+
Noooooo!
|
|
204
|
+
Command failed with exit code -1072103376 (STATUS_LOG_CORRUPTION_DETECTED): d:\projects\python-libs\fancy_subprocess\.venv\Scripts\python.exe -c "import sys; print("\^"Noooooo^!\^""); sys.exit(-1072103376)"
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
#### Killed command on Linux
|
|
208
|
+
|
|
209
|
+
Take this script:
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
import fancy_subprocess
|
|
213
|
+
import sys
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
fancy_subprocess.run_silenced(
|
|
217
|
+
[sys.executable, '-c', 'import time; time.sleep(60)'],
|
|
218
|
+
description='Sweet dreams!',
|
|
219
|
+
)
|
|
220
|
+
except fancy_subprocess.RunError as e:
|
|
221
|
+
print(e)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Running the script on Linux and killing the subprocess using `kill -9` before the 60 seconds are up will result in the following output:
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
Sweet dreams!
|
|
228
|
+
Command failed with exit code -9 (SIGKILL): /home/petamas/.venv/bin/python -c 'import time; time.sleep(60)'
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
#### Failure to find executable
|
|
232
|
+
|
|
233
|
+
Take this script:
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
import fancy_subprocess
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
fancy_subprocess.run(['foo', '--bar', 'baz'])
|
|
240
|
+
except fancy_subprocess.RunError as e:
|
|
241
|
+
print(e)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Running the script will produce the following output (exact error message may depend on OS):
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
Running command: foo --bar baz
|
|
248
|
+
Exception FileNotFoundError with message "[Errno 2] No such file or directory: 'foo'" was raised while trying to run command: foo --bar baz
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Other utilities
|
|
252
|
+
|
|
253
|
+
### `fancy_subprocess.reconfigure_standard_output_streams()`
|
|
254
|
+
|
|
255
|
+
Calls `sys.stdout.reconfigure()` and `sys.stderr.reconfigure()` with the provided parameters. Raises `TypeError` if either `sys.stdout` or `sys.stderr` is not an instance of `io.TextIOWrapper`.
|
|
256
|
+
|
|
257
|
+
## Licensing
|
|
258
|
+
|
|
259
|
+
This library is licensed under the MIT license.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
'checked_which',
|
|
3
|
+
'RunProcessError',
|
|
4
|
+
'RunProcessResult',
|
|
5
|
+
'SILENCE',
|
|
6
|
+
'which',
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
from pathext import checked_which, which
|
|
10
|
+
|
|
11
|
+
from fancy_subprocess._run_core import RunError, RunResult
|
|
12
|
+
|
|
13
|
+
RunProcessError = RunError
|
|
14
|
+
RunProcessResult = RunResult
|
|
15
|
+
|
|
16
|
+
def SILENCE(msg: str) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Helper function that takes a string, and does nothing with it. Meant to be passed as the print_message or print_output argument of run() and related functions to silence the corresponding output stream.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
pass
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
'default_print',
|
|
3
|
+
'error_print',
|
|
4
|
+
'Indent',
|
|
5
|
+
'indented_print',
|
|
6
|
+
'indented_print_factory',
|
|
7
|
+
'PrintFunction',
|
|
8
|
+
'silenced_print',
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
PrintFunction = Callable[[str], None]
|
|
16
|
+
|
|
17
|
+
Indent = int | str
|
|
18
|
+
|
|
19
|
+
def silenced_print(line: str) -> None:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
def indented_print(line: str, indent: Optional[Indent] = None) -> None:
|
|
23
|
+
if indent is None:
|
|
24
|
+
real_indent = 4*' '
|
|
25
|
+
elif isinstance(indent, int):
|
|
26
|
+
real_indent = indent*' '
|
|
27
|
+
else:
|
|
28
|
+
real_indent = indent
|
|
29
|
+
|
|
30
|
+
print(f'{real_indent}{line}', flush=True)
|
|
31
|
+
|
|
32
|
+
def indented_print_factory(indent: Optional[Indent] = None) -> PrintFunction:
|
|
33
|
+
return lambda line: indented_print(line, indent)
|
|
34
|
+
|
|
35
|
+
def default_print(line: str) -> None:
|
|
36
|
+
indented_print(line, indent='')
|
|
37
|
+
|
|
38
|
+
def error_print(line: str) -> None:
|
|
39
|
+
print(line, file=sys.stderr, flush=True)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
'reconfigure_standard_output_streams',
|
|
3
|
+
]
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Optional, TypedDict
|
|
8
|
+
|
|
9
|
+
from typing_extensions import Unpack
|
|
10
|
+
|
|
11
|
+
class ReconfigureParams(TypedDict, total=False):
|
|
12
|
+
encoding: Optional[str]
|
|
13
|
+
errors: Optional[str]
|
|
14
|
+
newline: Optional[str]
|
|
15
|
+
line_buffering: Optional[bool]
|
|
16
|
+
write_through: Optional[bool]
|
|
17
|
+
|
|
18
|
+
def _reconfigure_standard_stream(stream: object, name: str, **kwargs: Unpack[ReconfigureParams]) -> None:
|
|
19
|
+
if stream is None:
|
|
20
|
+
raise TypeError(f'{name} is None')
|
|
21
|
+
|
|
22
|
+
if not isinstance(stream, io.TextIOWrapper):
|
|
23
|
+
raise TypeError(f'{name} is not a TextIOWrapper: {repr(stream)}')
|
|
24
|
+
|
|
25
|
+
stream.reconfigure(**kwargs)
|
|
26
|
+
|
|
27
|
+
def reconfigure_standard_output_streams(**kwargs: Unpack[ReconfigureParams]) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Calls `sys.stdout.reconfigure()` and `sys.stderr.reconfigure()` with the provided parameters. Raises `TypeError` if either `sys.stdout` or `sys.stderr` is not an instance of `io.TextIOWrapper`.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
_reconfigure_standard_stream(sys.stdout, 'sys.stdout', **kwargs)
|
|
33
|
+
_reconfigure_standard_stream(sys.stderr, 'sys.stderr', **kwargs)
|