ci_exec.core¶
The core functionality of the ci_exec package.
fail(why, *[, exit_code, no_prefix])Write a failure message to
sys.stderrand exit.
Executable(exe_path, *[, log_calls, …])Represent a reusable executable.
mkdir_p(path[, mode, parents, exist_ok])Permissive wrapper around
pathlib.Path.mkdir().
rm_rf(path[, ignore_errors, onerror])Permissive wrapper around
shutil.rmtree()bypassingFileNotFoundErrorandNotADirectoryError.
which(cmd, *[, mode, path])Restrictive wrapper around
shutil.which()that willfail()if not found.
- fail(why, *, exit_code=1, no_prefix=False)[source]¶
Write a failure message to
sys.stderrand exit.
- class Executable(exe_path, *, log_calls=True, log_prefix='$ ', log_color='36', log_style='1')[source]¶
Represent a reusable executable.
Each executable is:
Failing by default: unless called with
check=False, any execution that fails (has a non-zero exit code) will result in a call tofail(), terminating the entire application.Logging by default: every call executed will print what will be run in color and then dump the output of the command. In the event of failure, this makes finding the last call issued much simpler.
Consider the following simple script:
from ci_exec import Executable git = Executable("/usr/bin/git") git("remote") # Oops! --pretty not --petty ;) git("log", "-1", "--petty=%B") git("status") # will not execute (previous failed)
When we execute
python simple.pyand check the exit code withecho $?:> python simple.py $ /usr/bin/git remote origin $ /usr/bin/git log -1 --petty=%B fatal: unrecognized argument: --petty=%B [X] Command '('/usr/bin/git', 'log', '-1', "--petty=%B")' returned non-zero exit status 128. > echo $? 128
See
__call__()for more information.Tip
Hard-coded paths in these examples were for demonstrative purposes. In practice this should not be done, use
which()instead.- log_calls¶
Whether or not every invocation of
__call__()should print what will execute before executing it. Default:True.- Type
- log_prefix¶
The prefix to use when printing a given invocation of
__call__(). Default:"$ "to simulate aconsolelexer. Set to the empty string""to have no prefix.- Type
- log_color¶
The
colorcode to use when callingcolorize()to display the next invocation of__call__(). Set toNoneto disable colorizing each log of__call__(). Default:Colors.Cyan.- Type
- log_style¶
The
stylecode to use when callingcolorize()to display the next invocation of__call__(). If no colors are desired, setlog_colortoNone. Default:Styles.Bold.- Type
- Raises
ValueError – If
exe_pathis not a file, or if it is not executable.
- PATH_EXTENSIONS = {}¶
The set of valid file extensions that can be executed on Windows.
On *nix systems this will be the empty set, and takes no meaning. On Windows, it is controlled by the user. These are stored in lower case, and comparisons should be lower case for consistency. The typical default value on Windows would be:
PATH_EXTENSIONS = {".com", ".exe", ".bat", ".cmd"}
- __call__(*args, **kwargs)[source]¶
Run
exe_pathwith the specified command-line*args.The usage of the parameters is best summarized in code:
popen_args = (self.exe_path, *args) # ... some potential logging ... return subprocess.run(popen_args, **kwargs)
For example, sending multiple arguments to the executable is as easy as:
cmake = Executable("/usr/bin/cmake") cmake("..", "-G", "Ninja", "-DBUILD_SHARED_LIBS=ON")
and any overrides to
subprocess.run()you wish to include should be done with**kwargs, which are forwarded directly.Warning
Any exceptions generated result in a call to
fail(), which will terminate the application.- Parameters
*args – The positional arguments will be forwarded along with
exe_pathtosubprocess.run().**kwargs –
The key-value arguments are all forwarded to
subprocess.run(). Ifcheckis not provided, this is an implicitcheck=True. That is, if you do not want the application to exit (viafail()), you must specifycheck=False:>>> from ci_exec import Executable >>> git = Executable("/usr/bin/git") >>> proc = git("not-a-command", check=False) $ /usr/bin/git not-a-command git: 'not-a-command' is not a git command. See 'git --help'. >>> proc.returncode 1 >>> git("not-a-command") $ /usr/bin/git not-a-command git: 'not-a-command' is not a git command. See 'git --help'. [X] Command '('/usr/bin/git', 'not-a-command')' returned non-zero exit status 1.
The final
git("not-a-command")exited the shell (this is what is meant by “failing by default”).
- Returns
The result of calling
subprocess.run()as outlined above.Note
Unless you are are calling with
check=False, you generally don’t need to store the return type.- Return type
- mkdir_p(path, mode=511, parents=True, exist_ok=True)[source]¶
Permissive wrapper around
pathlib.Path.mkdir().The intention is to behave like
mkdir -p, meaning the only real difference is thatparentsandexist_okdefault toTruefor this method (rather thanFalseforpathlib).- Parameters
path (pathlib.Path or str) – The directory path to make.
mode (int) – Access mask for directory permissions. See
pathlib.Path.mkdir().parents (bool) – Whether or not parent directories may be created. Default:
True.exist_ok (bool) –
Whether or not the command should be considered successful if the specified path already exists. Default:
True.Note
If the path exists and is a directory with
exist_ok=True, the command will succeed. If the path exists and is a file, even withexist_ok=Truethe command willfail().
- rm_rf(path, ignore_errors=False, onerror=None)[source]¶
Permissive wrapper around
shutil.rmtree()bypassingFileNotFoundErrorandNotADirectoryError.This function simply checks if
pathexists first before callingshutil.rmtree(). If thepathdoes not exist, nothing is done. If the path exists but is a file,pathlib.Path.unlink()is called instead.Essentially, this function tries to behave like
rm -rf, but in the event that removal is not possible (e.g., due to insufficient permissions), the function will stillfail().- Parameters
path (pathlib.Path or str) – The directory path to delete (including all children).
ignore_errors (bool) – Whether or not errors should be ignored. Default:
False, to ensure that permission errors are still caught.onerror – See
shutil.rmtree()for more information on the callback.
- which(cmd, *, mode=1, path=None, **kwargs)[source]¶
Restrictive wrapper around
shutil.which()that willfail()if not found.The primary difference is that when
cmdis not found,shutil.which()will returnNonewhereas this function willfail(). If you need to conditionally check for a command, do not use this function, useshutil.which()instead.- Parameters
cmd (str) – The name of the command to search for. E.g.,
"cmake".mode (int) – The flag permission mask. Default:
(os.F_OK | os.X_OK), see:os.F_OK,os.X_OK,shutil.which().path (str or None) – Default:
None. Seeshutil.which().**kwargs –
Included as a convenience bypass, forwards directly to
Executableconstructor. Suppose a non-loggingExecutableis desired. One option:git = which("git") git.log_calls = False
Or alternatively:
git = which("git", log_calls=False)
This is in recognition that for continuous integration users will likely have many different preferences. Users can provide their own
whichto always use this default, or say, change the logging color:from ci_exec import which as ci_which from ci_exec import Colors, Styles def which(cmd: str): return ci_which(cmd, log_color=Colors.Magenta, log_style=Styles.Regular)
- Returns
An executable created with the full path to the found
cmd.- Return type
Tests¶
Tests for the ci_exec.core module.
- test_executable_construction_failures()[source]¶
Validate that non-absolute and non-(executable)file constructions will raise.
- test_executable_relative()[source]¶
Validate
Executableaccepts relative paths.
- test_executable_logging(capsys)[source]¶
Validate
Executableruns and logs as expected.