subprocess.run() with formatted output, detailed error messages and retry capabilities
Project description
fancy-subprocess
fancy-subprocess provides variants of subprocess.run() with formatted output, detailed error messages and retry capabilities.
fancy_subprocess.run() and related functionality
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.
Key differences compared to subprocess.run():
- The command must be specified as a list, simply specifying a string is not allowed.
- The command's stdout and stderr is always combined into a single stream. (Like
subprocess.run(stderr=STDOUT).) - The output of the command is always assumed to be textual, not binary. (Like
subprocess.run(text=True).) - The output of the command is always captured, but it is also immediately printed using
print_output. - 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 isRunErrorinstead ofCalledProcessError. OSErroris never raised, it gets converted toRunError.RunResultis returned instead ofCompletedProcesson success.
Arguments (all of them except cmd are optional):
cmd: Sequence[str | Path]- Command to run. Seesubprocess.run()'s documentation for the interpretation ofcmd[0]. It is recommended to usefancy_subprocess.which()to producecmd[0].print_message: Callable[[str], None]- Function used to print informational messages. If unspecified or set toNone, defaults tofancy_subprocess.default_print. Usemessage_quiet=Trueto disable printing informational messages.- The type of this argument is also aliased as
fancy_subprocess.PrintFunction.
- The type of this argument is also aliased as
print_output: Callable[[str], None]- Function used to print a line of the output of the command. If unspecified or set toNone, defaults tofancy_subprocess.default_print. Useoutput_quiet=Trueto disable printing the command's output.- The type of this argument is also aliased as
fancy_subprocess.PrintFunction.
- The type of this argument is also aliased as
message_quiet: bool- IfTrue,print_messageis ignored, and no informational messages are printed. If unspecified or set toNone, defaults toFalse.output_quiet: bool- IfTrue,print_outputis ignored, and the command's output it not printed. If unspecified or set toNone, defaults toFalse. Note that this parameter also affects the default value ofdescription.description: str- Description printed before running the command. If unspecified or set toNone, defaults toRunning command: ...whenoutput_quietisFalse, andRunning command (output silenced): ...whenoutput_quietisTrue.success: Sequence[int] | AnyExitCode- List of exit codes that should be considered successful. If set tofancy_subprocess.ANY_EXIT_CODE, then all exit codes are considered successful. If unspecified or set toNone, 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.- The type of this argument is also aliased as
fancy_subprocess.Success.
- The type of this argument is also aliased as
flush_before_subprocess: bool- IfTrue, flushes both the standard output and error streams before running the command. If unspecified or set toNone, defaults toTrue.trim_output_lines: bool- IfTrue, remove trailing whitespace from the lines of the output of the command before callingprint_outputand adding them to theoutputfield ofRunResult. If unspecified or set toNone, defaults toTrue.max_output_size: int- Maximum number of characters to be recorded in theoutputfield ofRunResult. If the command produces more thanmax_output_sizecharacters, only the lastmax_output_sizewill be recorded. If unspecified or set toNone, defaults to 10,000,000.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=2attempts to run the command 3 times.) If unspecified or set toNone, defaults to 0.retry_initial_sleep_seconds: float- Number of seconds to wait before retrying for the first time. If unspecified or set toNone, defaults to 10.retry_backoff: float- Factor used to increase wait times before subsequent retries. If unspecified or set toNone, defaults to 2.env_overrides: Mapping[str, str]- Dictionary used to set environment variables. Note that unline theenvargument ofsubprocess.run(),env_overridesdoes not need to contain all environment variables, only the ones you want to add/modify compared to os.environ. If unspecified or set toNone, defaults to empty dictionary, i.e. no change to the environment.- The type of this argument is also aliased as
fancy_subprocess.EnvOverrides.
- The type of this argument is also aliased as
cwd: str | Path- If notNone, change current working directory tocwdbefore running the command.encoding: str- This encoding will be used to open stdout and stderr of the command. If unspecified or set toNone, see default behaviour inio.TextIOWrapper's documentation.errors: str- This specifies how text decoding errors will be handled. See details (including what happens if unspecified or set toNone) inio.TextIOWrapper's documentation.
Return value: fancy_subprocess.RunResult
fancy_subprocess.run() and similar functions return a RunResult instance on success.
RunResult has the following properties:
exit_code: int- Exit code of the finished process. (On Windows, this is a signedint32value, i.e. in the range of [-231, 231-1].)output: str- Combination of the process's output on stdout and stderr.
Exception: fancy_subprocess.RunError
fancy_subprocess.run() and similar functions raise RunError on error. There are two kinds of errors that result in a RunError:
- If the requested command has failed, the
completedproperty will beTrue, and theexit_codeandoutputproperties will be set. - If the command couldn't be run (eg. because the executable wasn't found), the
completedproperty will beFalse, and theoserrorproperty will be set to theOSErrorexception instance originally raised by the underlyingsubprocess.Popen()call.
Calling str() on a RunError object returns a detailed one-line description of the error:
- The failed command is included in the message.
- If an
OSErrorhappened, its message is included in the message. - On Windows, if the exit code of the process is recognized as a known
NTSTATUSerror value, its name is included in the message, otherwise its hexadecimal representation is included (to make searching it on the internet easier). - On Unix systems, if the exit code represents a signal, its name is included in the message.
RunError has the following properties:
cmd: Sequence[str | Path]- Original command passed tofancy_subprocess.run().completed: bool-Trueif the process completed (with an error),Falseif the underlyingsubprocess.Popen()call raised an OSError (eg. because it could not start the process).exit_code: int- Exit code of the completed process. RaisesValueErrorifcompletedisFalse.output: str- Combination of the process's output on stdout and stderr. RaisesValueErrorifcompletedisFalse.oserror: OSError- TheOSErrorraised bysubprocess.Popen(). RaisesValueErrorifcompletedisTrue.
fancy_subprocess.run_silenced()
Specialized version of fancy_subprocess.run(), primarily used to run a command and later process its output.
Differences compared to fancy_subprocess.run():
output_quietcannot be set from the calling side, it is always set toTrue. Note that this affectsdescription's default value.print_outputcannot be set from the calling side (because it wouldn't matter anyway because ofoutput_quiet=True).
All other fancy_subprocess.run() arguments are available and behave the same.
fancy_subprocess.run_indented()
Specialized version of fancy_subprocess.run() which prints the command's output indented by a user-defined amount.
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).
All other fancy_subprocess.run() arguments are available and behave the same.
Writing your own wrapper
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:
import fancy_subprocess
from typing import Unpack
def grab_output(cmd: list[str], **kwargs: Unpack[fancy_subprocess.RunParams]) -> str:
# Raises ValueError if there are unknown parameters in kwargs or if a keyword argument's type is incorrect
fancy_subprocess.check_run_params(**kwargs)
# Make a copy of keyword arguments to be edited
forwarded_args = kwargs.copy()
# Make sure nothing's printed, raise ValueError if caller tries to specify "output_quiet" or "message_quiet"
fancy_subprocess.force_run_params(forwarded_args, message_quiet=True, output_quiet=True)
# Handle encoding/decoding errors by replacing them with placeholder character by default, but allow callers to still customize behaviour
fancy_subprocess.change_default_run_params(forwarded_args, errors='replace')
# Run command, raise fancy_subprocess.RunError on failure
result = fancy_subprocess.run(cmd, **forwarded_args)
# Return combined stdout and stderr
return result.output
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.
(Using typing.Unpack requires Python 3.11 or later. In Python 3.10, use the typing_extensions module from PyPI.)
Predefined printing functions
There are various predefined functions projects can use as the print_message and print_output parameters of fancy_subprocess.run():
fancy_subprocess.default_printprints the line tosys.stdout, then flushes it.fancy_subprocess.errors_printprints the line tosys.stderr, then flushes it.fancy_subprocess.silenced_printdoes not print anything. It can be used as an alternative tooutput_quiet=Trueif the caller does not want to change the defaultdescription.fancy_subprocess.indented_printprints the line tosys.stdoutindented by 4 spaces, then flushes the file.fancy_subprocess.indented_print_factory(indent)returns a function that callsfancy_subprocess.indented_printwith its parameter and the specified indent instead of the default 4 spaces.logging.error(line),logging.info(line), etc. can be used to redirect the messages (or even the output) to Python's builtin logging subsystem.- The builtin
printfunction can also be used to print without flushing.
Example outputs
Success
Take this script:
import fancy_subprocess
import sys
fancy_subprocess.run_indented(
[sys.executable, '-m', 'venv', '--help'],
print_message=lambda msg: print(f'[script-name] {msg}'),
success=fancy_subprocess.ANY_EXIT_CODE)
Running the script will produce the following output (on Windows):
[script-name] Running command: d:\projects\python-libs\fancy_subprocess\.venv\Scripts\python.exe -m venv --help
usage: venv [-h] [--system-site-packages] [--symlinks | --copies] [--clear]
[--upgrade] [--without-pip] [--prompt PROMPT] [--upgrade-deps]
ENV_DIR [ENV_DIR ...]
Creates virtual Python environments in one or more target directories.
positional arguments:
ENV_DIR A directory to create the environment in.
options:
-h, --help show this help message and exit
--system-site-packages
Give the virtual environment access to the system
site-packages dir.
--symlinks Try to use symlinks rather than copies, when symlinks
are not the default for the platform.
--copies Try to use copies rather than symlinks, even when
symlinks are the default for the platform.
--clear Delete the contents of the environment directory if it
already exists, before environment creation.
--upgrade Upgrade the environment directory to use this version
of Python, assuming Python has been upgraded in-place.
--without-pip Skips installing or upgrading pip in the virtual
environment (pip is bootstrapped by default)
--prompt PROMPT Provides an alternative prompt prefix for this
environment.
--upgrade-deps Upgrade core dependencies: pip setuptools to the
latest version in PyPI
Once an environment has been created, you may wish to activate it, e.g. by
sourcing an activate script in its bin directory.
Failed command on Windows
Take this script:
import fancy_subprocess
import sys
try:
fancy_subprocess.run(
[sys.executable, '-c', 'import sys; print("Noooooo!"); sys.exit(-1072103376)'],
description='Demonstrating failure...',
)
except fancy_subprocess.RunError as e:
print(e)
Running the script on Windows will produce the following output (-1072103376 is the signed integer interpretation of 0xC0190030, i.e. STATUS_LOG_CORRUPTION_DETECTED):
Demonstrating failure...
Noooooo!
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)"
Killed command on Linux
Take this script:
import fancy_subprocess
import sys
try:
fancy_subprocess.run_silenced(
[sys.executable, '-c', 'import time; time.sleep(60)'],
description='Sweet dreams!',
)
except fancy_subprocess.RunError as e:
print(e)
Running the script on Linux and killing the subprocess using kill -9 before the 60 seconds are up will result in the following output:
Sweet dreams!
Command failed with exit code -9 (SIGKILL): /home/petamas/.venv/bin/python -c 'import time; time.sleep(60)'
Failure to find executable
Take this script:
import fancy_subprocess
try:
fancy_subprocess.run(['foo', '--bar', 'baz'])
except fancy_subprocess.RunError as e:
print(e)
Running the script will produce the following output (exact error message may depend on OS):
Running command: foo --bar baz
Exception FileNotFoundError with message "[Errno 2] No such file or directory: 'foo'" was raised while trying to run command: foo --bar baz
Other utilities
fancy_subprocess.reconfigure_standard_output_streams()
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.
Licensing
This library is licensed under the MIT license.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fancy_subprocess-2.0.tar.gz.
File metadata
- Download URL: fancy_subprocess-2.0.tar.gz
- Upload date:
- Size: 15.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.10.16
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
989ea8315b012ac340a35df1dedc0058667dd2ad147a1d08ea74006c82830c4d
|
|
| MD5 |
8c4debac45d5c683dd37c360ed93abc4
|
|
| BLAKE2b-256 |
de6f5d03e3b44b8e4195bff6b94cf87def6bf58f75a37b2573d9bcbdb7fe2bc8
|
File details
Details for the file fancy_subprocess-2.0-py3-none-any.whl.
File metadata
- Download URL: fancy_subprocess-2.0-py3-none-any.whl
- Upload date:
- Size: 15.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.10.16
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
197e2be98500515d54cc24ffc0b93e6ab0127a11597e1869f3419920f4a7a5cb
|
|
| MD5 |
edda8730bfc97d31440fd0bd40de8810
|
|
| BLAKE2b-256 |
3a7d5cc6fac006241381bdf5fb19fab415a2e8974e6b76c5245a6341b9cfeffc
|