Package: nicecall
WhilePython’ssubprocesslibraryis great, it may not be the easiest library to use. This is the reason I creatednicecall: it allows to do simple tasks with processes very easily.Note thatnicecallis not a substitute tosubprocess, because much ofsubprocessfunctionality doesn’t exist. For instance, one can’t usestdinor pipes withnicecall. The goal is not to replacesubprocess, but only to provide an easy way to do the most common tasks.How to use the libraryNote: make sure you also checkthe testswhich
give a few examples of how to use the library. Most pieces of code below are intests/smoke/test_docs.py.Fluent interfaceThe library uses method chaining, which allows to add logic on the fly before actually launching the process. Methods such ason_stdout,ignore, etc. create a copy of the object, modify this copy, and return it to the caller. This makes it possible to reuse base objects in multiple locations in your code, reducing code duplication.Exit codeLet’s start by executing a task:result = nicecall.Process().execute(["touch", "/tmp/hello"])Theresultcontains the exit code, which makes it possible to determine whether the process terminated successfully. Below, the value ofresultis expected to be zero. You may also make it fail:result = nicecall.Process().execute(["touch", "/tmp/a/b/c/d"])Theresultshould now be1, assuming you don’t have/tmp/a/b/cdirectory.Sometimes, one may prefer raising an exception if the process fails, instead of checking for the exit code manually. Theraise_if_error()method can be used for that. When called, exit code different from zero will lead tosubprocess.CalledProcessErrorbeing raised.stdoutandstderrOne can also perform a bunch of actions onstdoutandstderr. Let’s displaystdoutin terminal:nicecall.Process().on_stdout(print).execute(["echo", "a\nb\nc"])The output should be:a
b
cIf you just want to readstdoutoutput, then instead ofexecute, you can useread_stdout. The following piece of code produces the same output:lines = nicecall.Process().read_stdout(["echo", "a\nb\nc"])
print("\n".join(lines))Note that when usingread_stdout, a non-zero exit codewilllead tosubprocess.CalledProcessErrorbeing raised, independently of the presence ofraise_if_error()in the chain. The reason for this choice is that withread_stdout, there is no way to check for the actual exit code, and plainly ignoring it would lead to difficult to debug situations.Also note thatread_stdoutstoresstdoutin a list and isnotlazy. This means that:The method is appropriate only for commands which output a small quantity of lines.Looping through the lines and stopping in the middle wouldn’t affect neither the processing of the exit code, nor the other actions such as logging which may have been specified earlier in the chain throughon_stdout.The lines fromstderrare not returned. Theon_stderrmethod can still be used, exactly in the same way it is withexecute.All the actions defined throughon_stdoutwill be performedbeforethe list is returned.Filters added throughkeepandignoreapply to the lines in the result as well.LoggingA common thing, at least in my case, is to logstdoutorstderrto syslog. Withnicecall, it’s easy:# Initialize logging.
log_handler = logging.handlers.SysLogHandler(address="/dev/log")
formatter = logging.Formatter("demo: [%(levelname)s] %(message)s")
log_handler.setFormatter(formatter)
log_handler.setLevel(logging.DEBUG)

demo_logger = logging.getLogger("demo")
demo_logger.setLevel(logging.DEBUG)
demo_logger.addHandler(log_handler)

...

# Log stdout.
logger = nicecall.StdoutLogger("test")
nicecall.Process().on_stdout(logger.log).execute(["echo", "a\nb"])Note thatnicecall.StdoutLoggercan be initialized with either the name of the logger, or the instance of the logger itself.The library itself logs calls (INFOlevel) and call failures (WARNINGlevel) through the logger namednicecall.process. For instance, executingtouch /tmp/a/b/c/dwill produce two log messages:INFO:nicecall.process:Called “touch /tmp/a/b/c/d”.
WARNING:nicecall.process:“touch /tmp/a/b/c/d” failed with exit code 1.FilteringSometimes, you don’t want to process specific content such as empty lines or whitespace. This is what filters are about:nicecall \
    .Process() \
    .ignore(nicecall.filters.whitespace) \
    .on_stdout(print) \
    .execute(["echo", "a\n\nb"])Here,aandbwill be displayed in terminal; however, the empty line will be ignored. The reverse is calledkeep. Bothkeepandignoreaccept any function which takes a string as a parameter and returns a boolean. For instance, this will print onlystdoutcontent longer than fifteen characters:nicecall \
    .Process() \
    .keep(lambda line: len(line) > 15) \
    .on_stdout(print) \
    .execute(["echo", "Hello World!\nWhat a lovely day!"])Multiplekeepandignoremethods can be combined. The output will keep the lines which matchallpredicates fromkeepmethods andnonefromignoreones.Filters apply to bothstdoutandstderr; there is no way to apply them to only one of the streams.TestingIn order to be able to test your code, the library provides aNullProcessclass, a stub and a mock.NullProcessThis class creates an object which willnotlaunch any process whenexecuteis called. The purpose of this class is to replace the actualProcessclass during testing.StubThe stub makes it possible to emulateProcesswithout actually doing the system calls. The difference withNullProcessis that the stub makes it possible to define the exit codes andstdout/stderroutput for specific commands.The stub allows to define associations between the arguments and the expected response. For instance, imagine a situation where the tested code is expected to perform two calls: one to create a directory, another one to create a file in it. We want to test how the code under testing will perform if the second command fails: are the developers handling this edge case? For this purpose, one can use the stub like this:stub = nicecall.tests.ProcessStub()
stub.add_match(["mkdir", "/tmp/a"], 0)
stub.add_match(
    ["touch", "/tmp/a/b"],
    1,
    stderr=["touch: cannot touch '/tmp/a/b': No such file or directory"])Thestubcan now be passed to the code under tests instead ofnicecall.Process(). The tested code will run, perform amkdir, and, when executing thetouchcommand, will get back the exit code1and a call to the actions, if any, set throughon_stderr.MockThe mock performs in a similar way to a stub, but also records the activity of the code under tests, i.e. the parameters which were passed to different methods of the mock. Usually, the mock is used this way:with nicecall.tests.ProcessMockContext() as context:
    # Code under tests goes here.
    # The mock is `context.mock`.
    ...

    # Follows the assertions. In this example, I'm just ensuring that the
    # code under tests added `print` to the `stdout` actions, i.e. ran the
    # `...on_stdout(print)...` command.
    actual = context.on_stdout_actions
    expected = [print]
    self.assertCountEqual(expected, actual)The mock makes it possible to check the following elements:executed_args: theargswhich were used when callingexecute()method.ignore_predicates: the list of predicates added by the tested code using theignoremethod.keep_predicates: same as previous, but forkeep.on_stdout_actions: the list of actions added by the tested code using theon_stdoutmethod.on_stderr_actions: same as previous, but foron_stderr.Classesprocess.pyThe class is the entry point of the library. It makes it possible to specify different options before actually starting the process.execute: actually executes the process and blocks until the process finishes.Parameters:argsis an array which indicates the process to start, and its parameters. Example:["touch", "/tmp/hello"].Returns:Returns the exit code.keep: specifies a filter to apply to determine if the line ofstdoutorstderrshould be processed by the actions specified throughon_stdoutandon_stderr.The method can be called multiple times and mixed withignoreto aggregate multiple filters.Parameters:predicateis a function which takes a string as a parameter and returns a boolean value:trueif the line should be processed, orfalseotherwise.Returns:Returns a new instance of theProcessclass with the new filter.ignore: seekeep. Here, the predicate is reverted.on_stdout: adds an action to perform when a line fromstdoutis received.The method can be called multiple times if multiple actions should be performed for every line ofstdout.Parameters:action: a function which takes a string as a parameter and doesn’t return anything.Returns:Returns a new instance of theProcessclass with the new action.on_stderr: seeon_stdout. Here, it deals withstderrinstead.filters.pyThe file contains a bunch of filters which can be used inProcess.keepandProcess.ignore.logger.pyThis class is used to log output fromstdoutorstderr.CompatibilityThe library was written for Python 3 under Linux. I haven’t tested it neither with Python 2, nor under Windows.ReliabilityWhile I used Test Driven Development when creating this library and naturally have a 100% branch coverage, I don’t know neither Python, norsubprocesswell enough to be sure that the library can be used reliably in production. Use at own risk.ContributingIf you want to contribute, contact me atarseni.mourzenko@pelicandd.com. You’ll be able to contribute to the project using theofficial SVN repository. If you find it more convinient to clone the source to GitHub, you can do that too.The source code of the library and the corresponding documentation are covered by theMIT License.