summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--alot/helper.py62
-rw-r--r--tests/helper_test.py45
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):