Package: runez
Friendly misc/utils/convenience libraryOverviewrunezis a convenience"utils"library for common operations I found myself rewriting multiple times.The name was initially meant as “run ez” (“run easy”),
the fact that it sounds like “runes” gives it a bit of a mystery/magic side that’s also relatively appropriate
(it does indeed concentrate a bit of invocation magic, as you can save quite a few lines of repetitive code by using it)FeaturesUsable with any python versionPure python standalone library, does not bring in any additional dependencyTakes care of most edge cases, with nice errorsFunctions can be called without checking for return code etc (abort by default, with nice error)They can also be called withfatal=False, in which case the return value will indicate whether call succeeded or notSupport fordryrunmode (show what would be done, but don’t do it)Perform most typical logging setups in one call torunez.log.setup()Log operations systematically (at debug level mostly), examples:Running: foo ...
Copy foo -> bar
Would move foo -> bar    (for dryrun)CaptureOutputcontext manager -> grab output/logging from any code section100% test coverageExampleRun a program:import runez

# Aborts if "foo" doesn't exist
output = runez.run("ls", "foo")

# Output can also be ignored
runez.run("ls", "foo")

# Don't capture output, just run the command and let output "pass through"
runez.run("ls", "foo", stdout=None, stderr=None)

# Don't abort, return False on failure (or actual output when successful)
output = runez.run("ls", "foo", fatal=False)File operations:import runez

runez.touch("foo")
runez.copy("foo", "bar")
runez.move("foo", "baz")
runez.delete("foo")

runez.write("foo", "bar\nbaz\n")
content = "\n".join(runez.readlines("foo", first=10))

full_path = runez.resolved_path("foo/bar")
folder = runez.parent_folder(full_path)
runez.ensure_folder(folder)
with runez.Anchored(folder):
    assert runez.short(full_path) == "bar"InstallationAs usual, available onpypi:pip install runezPhilosophyruneztries to provide a consistent interface across functions.
Here are the main tenets for functions involving I/O (such as writing, reading, copy-ing files etc):All IO-related functionsNOT returning content(run(),delete(), …)
have this common signature:fatal=True, logger=UNSET, dryrun=UNSETfatal: decides whether operation should raise an exception on failure or notfatal=True(default): raise an exception on failure, log a meaningful errorfatal=False: don’t raise on failure, log a meaningful errorfatal=None: don’t raise on failure, don’t log anythingIn non-fatal mode, calls try to return a usable value appropriate for the call (see docstring of each function)logger: decides how chatty the operation should beLOG.error()is used for failures, except whenfatalis not True AND providedloggeris a callablelogger=UNSET(default):LOG.debug("Running:...")to trace activityprint("Wouldrun:...")in dryrun modelogger=False: Log errors only (used internally, to avoid unnecessary log chatter when one operation calls another)logger=mylogger: call providedmylogger()to trace activity (example:logger=MY_LOGGER.info)mylogger("Running:...")to trace activitymylogger("Wouldrun:...")in dryrun modelogger=None: Don’t log anything (even errors)dryrunallows to override currentrunez.DRYRUNsetting just for that callAll IO-related functionsreturning content(read_json(),readlines(), …)
use a simpler convention based on:default=UNSET,
which decides whether operation should raise an exception on failure or not:WhendefaultisNOT provided, the function call will abort on failure with an exception,
logging a meaningful error viaLOG.error()Whendefaultis provided(even ifNone), the function call will NOT abort,
but return the specifieddefaultinstead, it is up to the caller to log anything
in that case (no log chatter comes fromrunezin that case, at all)