diff options
-rw-r--r-- | alot/helper.py | 62 | ||||
-rw-r--r-- | tests/helper_test.py | 45 |
2 files changed, 46 insertions, 61 deletions
diff --git a/alot/helper.py b/alot/helper.py index d0c21a49..46dbb782 100644 --- a/alot/helper.py +++ b/alot/helper.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (C) 2011-2012 Patrick Totzke <patricktotzke@gmail.com> -# Copyright © 2017 Dylan Baker +# Copyright © 2017-2018 Dylan Baker # This file is released under the GNU GPL, version 3 or a later revision. # For further details see the COPYING file from datetime import timedelta @@ -21,12 +21,10 @@ from email.mime.base import MIMEBase from email.mime.image import MIMEImage from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart +import asyncio import urwid import magic -from twisted.internet import reactor -from twisted.internet.protocol import ProcessProtocol -from twisted.internet.defer import Deferred def split_commandline(s, comments=False, posix=True): @@ -297,52 +295,42 @@ def call_cmd(cmdlist, stdin=None): return out, err, ret -def call_cmd_async(cmdlist, stdin=None, env=None): - """ - get a shell commands output, error message and return value as a deferred. +async def call_cmd_async(cmdlist, stdin=None, env=None): + """Given a command, call that command asynchronously and return the output. + + This function only handles `OSError` when creating the subprocess, any + other exceptions raised either durring subprocess creation or while + exchanging data with the subprocess are the caller's responsibility to + handle. + + If such an `OSError` is caught, then returncode will be set to 1, and the + error value will be set to the str() method fo the exception. :type cmdlist: list of str :param stdin: string to pipe to the process :type stdin: str - :return: deferred that calls back with triple of stdout, stderr and - return value of the shell command - :rtype: `twisted.internet.defer.Deferred` + :return: Tuple of stdout, stderr, returncode + :rtype: tuple[str, str, int] """ termenc = urwid.util.detected_encoding cmdlist = [s.encode(termenc) for s in cmdlist] - class _EverythingGetter(ProcessProtocol): - def __init__(self, deferred): - self.deferred = deferred - self.outBuf = BytesIO() - self.errBuf = BytesIO() - self.outReceived = self.outBuf.write - self.errReceived = self.errBuf.write - - def processEnded(self, status): - out = string_decode(self.outBuf.getvalue(), termenc) - err = string_decode(self.errBuf.getvalue(), termenc) - if status.value.exitCode == 0: - self.deferred.callback(out) - else: - terminated_obj = status.value - terminated_obj.stderr = err - self.deferred.errback(terminated_obj) - - d = Deferred() environment = os.environ.copy() if env is not None: environment.update(env) logging.debug('ENV = %s', environment) logging.debug('CMD = %s', cmdlist) - proc = reactor.spawnProcess(_EverythingGetter(d), executable=cmdlist[0], - env=environment, - args=cmdlist) - if stdin: - logging.debug('writing to stdin') - proc.write(stdin.encode(termenc)) - proc.closeStdin() - return d + try: + proc = await asyncio.create_subprocess_exec( + *cmdlist, + env=environment, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + stdin=asyncio.subprocess.PIPE if stdin else None) + except OSError as e: + return ('', str(e), 1) + out, err = await proc.communicate(stdin.encode(termenc) if stdin else None) + return (out.decode(termenc), err.decode(termenc), proc.returncode) def guess_mimetype(blob): diff --git a/tests/helper_test.py b/tests/helper_test.py index 061ce122..6f0add53 100644 --- a/tests/helper_test.py +++ b/tests/helper_test.py @@ -1,5 +1,5 @@ # encoding=utf-8 -# Copyright © 2016-2017 Dylan Baker +# Copyright © 2016-2018 Dylan Baker # Copyright © 2017 Lucas Hoffman # This program is free software: you can redistribute it and/or modify @@ -25,8 +25,6 @@ import random import mock from twisted.trial import unittest -from twisted.internet.defer import inlineCallbacks -from twisted.internet.error import ProcessTerminated from alot import helper @@ -427,20 +425,20 @@ class TestShorten(unittest.TestCase): class TestCallCmdAsync(unittest.TestCase): - @inlineCallbacks - def test_no_stdin(self): - ret = yield helper.call_cmd_async(['echo', '-n', 'foo']) - self.assertEqual(ret, 'foo') + @utilities.async_test + async def test_no_stdin(self): + ret = await helper.call_cmd_async(['echo', '-n', 'foo']) + self.assertEqual(ret[0], 'foo') - @inlineCallbacks - def test_stdin(self): - ret = yield helper.call_cmd_async(['cat', '-'], stdin='foo') - self.assertEqual(ret, 'foo') + @utilities.async_test + async def test_stdin(self): + ret = await helper.call_cmd_async(['cat', '-'], stdin='foo') + self.assertEqual(ret[0], 'foo') - @inlineCallbacks - def test_env_set(self): + @utilities.async_test + async def test_env_set(self): with mock.patch.dict(os.environ, {}, clear=True): - ret = yield helper.call_cmd_async( + ret = await helper.call_cmd_async( # Thanks to the future import it doesn't matter if python is # python2 or python3 ['python', '-c', 'from __future__ import print_function; ' @@ -448,21 +446,20 @@ class TestCallCmdAsync(unittest.TestCase): 'print(os.environ.get("foo", "fail"), end="")' ], env={'foo': 'bar'}) - self.assertEqual(ret, 'bar') + self.assertEqual(ret[0], 'bar') - @inlineCallbacks - def test_env_doesnt_pollute(self): + @utilities.async_test + async def test_env_doesnt_pollute(self): with mock.patch.dict(os.environ, {}, clear=True): - yield helper.call_cmd_async(['echo', '-n', 'foo'], + await helper.call_cmd_async(['echo', '-n', 'foo'], env={'foo': 'bar'}) self.assertEqual(os.environ, {}) - @inlineCallbacks - def test_command_fails(self): - with self.assertRaises(ProcessTerminated) as cm: - yield helper.call_cmd_async(['_____better_not_exist']) - self.assertEqual(cm.exception.exitCode, 1) - self.assertTrue(cm.exception.stderr) + @utilities.async_test + async def test_command_fails(self): + _, err, ret = await helper.call_cmd_async(['_____better_not_exist']) + self.assertEqual(ret, 1) + self.assertTrue(err) class TestGetEnv(unittest.TestCase): |