ci_exec.core¶
The core functionality of the ci_exec
package.
fail
(why, *[, exit_code, no_prefix])Write a failure message to
sys.stderr
and 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()
bypassingFileNotFoundError
andNotADirectoryError
.
which
(cmd, *[, mode, path])Restrictive wrapper around
shutil.which()
that willfail()
if not found.
-
ci_exec.core.
fail
(why, *, exit_code = 1, no_prefix = False) → NoReturn[source]¶ Write a failure message to
sys.stderr
and exit.
-
class
ci_exec.core.
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.py
and 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 aconsole
lexer. Set to the empty string""
to have no prefix.- Type
-
log_color
¶ The
color
code to use when callingcolorize()
to display the next invocation of__call__()
. Set toNone
to disable colorizing each log of__call__()
. Default:Colors.Cyan
.- Type
-
log_style
¶ The
style
code to use when callingcolorize()
to display the next invocation of__call__()
. If no colors are desired, setlog_color
toNone
. Default:Styles.Bold
.- Type
- Raises
ValueError – If
exe_path
is 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) → subprocess.CompletedProcess[source]¶ Run
exe_path
with 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_path
tosubprocess.run()
.**kwargs –
The key-value arguments are all forwarded to
subprocess.run()
. Ifcheck
is 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
-
ci_exec.core.
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 thatparents
andexist_ok
default toTrue
for this method (rather thanFalse
forpathlib
).- 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=True
the command willfail()
.
-
ci_exec.core.
rm_rf
(path, ignore_errors = False, onerror=None)[source]¶ Permissive wrapper around
shutil.rmtree()
bypassingFileNotFoundError
andNotADirectoryError
.This function simply checks if
path
exists first before callingshutil.rmtree()
. If thepath
does 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.
-
ci_exec.core.
which
(cmd, *, mode = 1, path = None, **kwargs) → ci_exec.core.Executable[source]¶ Restrictive wrapper around
shutil.which()
that willfail()
if not found.The primary difference is that when
cmd
is not found,shutil.which()
will returnNone
whereas 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
Executable
constructor. Suppose a non-loggingExecutable
is 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
which
to 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.
-
tests.core.
test_executable_construction_failures
()[source]¶ Validate that non-absolute and non-(executable)file constructions will raise.
-
tests.core.
test_executable_relative
()[source]¶ Validate
Executable
accepts relative paths.
-
tests.core.
test_executable_logging
(capsys)[source]¶ Validate
Executable
runs and logs as expected.