summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--alot/commands/globals.py27
-rw-r--r--alot/helper.py4
-rw-r--r--docs/source/configuration/contacts_completion.rst14
-rw-r--r--docs/source/configuration/hooks.rst2
-rw-r--r--tests/commands/global_test.py61
-rw-r--r--tests/helper_test.py78
-rw-r--r--tests/utilities.py10
7 files changed, 166 insertions, 30 deletions
diff --git a/alot/commands/globals.py b/alot/commands/globals.py
index f8f58749..565bd742 100644
--- a/alot/commands/globals.py
+++ b/alot/commands/globals.py
@@ -247,7 +247,7 @@ class ExternalCommand(Command):
# set standard input for subcommand
stdin = None
if self.stdin is not None:
- # wrap strings in StrinIO so that they behaves like a file
+ # wrap strings in StringIO so that they behave like files
if isinstance(self.stdin, unicode):
stdin = StringIO(self.stdin)
else:
@@ -255,7 +255,7 @@ class ExternalCommand(Command):
def afterwards(data):
if data == 'success':
- if callable(self.on_success):
+ if self.on_success is not None:
self.on_success()
else:
ui.notify(data, priority='error')
@@ -267,24 +267,17 @@ class ExternalCommand(Command):
def thread_code(*_):
try:
- if stdin is None:
- proc = subprocess.Popen(self.cmdlist, shell=self.shell,
- stderr=subprocess.PIPE)
- ret = proc.wait()
- err = proc.stderr.read()
- else:
- proc = subprocess.Popen(self.cmdlist, shell=self.shell,
- stdin=subprocess.PIPE,
- stderr=subprocess.PIPE)
- _, err = proc.communicate(stdin.read())
- ret = proc.wait()
- if ret == 0:
- return 'success'
- else:
- return err.strip()
+ proc = subprocess.Popen(self.cmdlist, shell=self.shell,
+ stdin=subprocess.PIPE if stdin else None,
+ stderr=subprocess.PIPE)
except OSError as e:
return str(e)
+ _, err = proc.communicate(stdin.read() if stdin else None)
+ if proc.returncode == 0:
+ return 'success'
+ return err.strip()
+
if self.in_thread:
d = threads.deferToThread(thread_code)
d.addCallback(afterwards)
diff --git a/alot/helper.py b/alot/helper.py
index 1c569a23..26787fa8 100644
--- a/alot/helper.py
+++ b/alot/helper.py
@@ -129,7 +129,7 @@ def string_decode(string, enc='ascii'):
def shorten(string, maxlen):
"""shortens string if longer than maxlen, appending ellipsis"""
if 1 < maxlen < len(string):
- string = string[:maxlen - 1] + u'\u2026'
+ string = string[:maxlen - 1] + u'…'
return string[:maxlen]
@@ -333,7 +333,7 @@ def call_cmd_async(cmdlist, stdin=None, env=None):
self.deferred.errback(terminated_obj)
d = Deferred()
- environment = os.environ
+ environment = os.environ.copy()
if env is not None:
environment.update(env)
logging.debug('ENV = %s', environment)
diff --git a/docs/source/configuration/contacts_completion.rst b/docs/source/configuration/contacts_completion.rst
index 9185424f..5f18831c 100644
--- a/docs/source/configuration/contacts_completion.rst
+++ b/docs/source/configuration/contacts_completion.rst
@@ -56,11 +56,12 @@ Both respect the `ignorecase` option which defaults to `True` and results in cas
command = notmuch_abook.py lookup
regexp = ^((?P<name>[^(\\s+\<)]*)\s+<)?(?P<email>[^@]+?@[^>]+)>?$
- `notmuch address`
+ `notmuch address <https://notmuchmail.org/manpages/notmuch-address-1/>`_
Since version `0.19`, notmuch itself offers a subcommand `address`, that
returns email addresses found in the notmuch index.
Combined with the `date:` syntax to query for mails within a certain
- timeframe, this allows to search for all recently used contacts:
+ timeframe, this allows to search contacts that you've sent emails to
+ (output all addresses from the `To`, `Cc` and `Bcc` headers):
.. code-block:: ini
@@ -68,6 +69,15 @@ Both respect the `ignorecase` option which defaults to `True` and results in cas
regexp = '\[?{"name": "(?P<name>.*)", "address": "(?P<email>.+)", "name-addr": ".*"}[,\]]?'
shellcommand_external_filtering = False
+ If you want to search for senders in the `From` header (which should be
+ must faster according to `notmuch address docs
+ <https://notmuchmail.org/manpages/notmuch-address-1/>`_), then use the
+ following command:
+
+ .. code-block:: ini
+
+ command = 'notmuch address --format=json date:1Y..'
+
Don't hesitate to send me your custom `regexp` values to list them here.
.. describe:: abook
diff --git a/docs/source/configuration/hooks.rst b/docs/source/configuration/hooks.rst
index 352701c3..0b2b6a23 100644
--- a/docs/source/configuration/hooks.rst
+++ b/docs/source/configuration/hooks.rst
@@ -28,7 +28,7 @@ Consider this pre-hook for the exit command, that logs a personalized goodbye
message::
import logging
- from alot.settings import settings
+ from alot.settings.const import settings
def pre_global_exit(**kwargs):
accounts = settings.get_accounts()
if accounts:
diff --git a/tests/commands/global_test.py b/tests/commands/global_test.py
index 860a6ccb..9489c909 100644
--- a/tests/commands/global_test.py
+++ b/tests/commands/global_test.py
@@ -17,6 +17,7 @@
"""Tests for global commands."""
from __future__ import absolute_import
+import os
from twisted.trial import unittest
from twisted.internet.defer import inlineCallbacks
@@ -116,3 +117,63 @@ class TestComposeCommand(unittest.TestCase):
self.assertFalse(envelope.sign)
self.assertIs(envelope.sign_key, None)
+
+
+class TestExternalCommand(unittest.TestCase):
+
+ def test_no_spawn_no_stdin_success(self):
+ ui = mock.Mock()
+ cmd = g_commands.ExternalCommand(u'true', refocus=False)
+ cmd.apply(ui)
+ ui.notify.assert_not_called()
+
+ def test_no_spawn_stdin_success(self):
+ ui = mock.Mock()
+ cmd = g_commands.ExternalCommand(u"awk '{ exit $0 }'", stdin=u'0',
+ refocus=False)
+ cmd.apply(ui)
+ ui.notify.assert_not_called()
+
+ def test_no_spawn_no_stdin_attached(self):
+ ui = mock.Mock()
+ cmd = g_commands.ExternalCommand(u'test -t 0', refocus=False)
+ cmd.apply(ui)
+ ui.notify.assert_not_called()
+
+ def test_no_spawn_stdin_attached(self):
+ ui = mock.Mock()
+ cmd = g_commands.ExternalCommand(u"test -t 0", stdin=u'0', refocus=False)
+ cmd.apply(ui)
+ ui.notify.assert_called_once_with('', priority='error')
+
+ def test_no_spawn_failure(self):
+ ui = mock.Mock()
+ cmd = g_commands.ExternalCommand(u'false', refocus=False)
+ cmd.apply(ui)
+ ui.notify.assert_called_once_with('', priority='error')
+
+ @mock.patch('alot.commands.globals.settings.get', mock.Mock(return_value=''))
+ @mock.patch.dict(os.environ, {'DISPLAY': ':0'})
+ def test_spawn_no_stdin_success(self):
+ ui = mock.Mock()
+ cmd = g_commands.ExternalCommand(u'true', refocus=False, spawn=True)
+ cmd.apply(ui)
+ ui.notify.assert_not_called()
+
+ @mock.patch('alot.commands.globals.settings.get', mock.Mock(return_value=''))
+ @mock.patch.dict(os.environ, {'DISPLAY': ':0'})
+ def test_spawn_stdin_success(self):
+ ui = mock.Mock()
+ cmd = g_commands.ExternalCommand(
+ u"awk '{ exit $0 }'",
+ stdin=u'0', refocus=False, spawn=True)
+ cmd.apply(ui)
+ ui.notify.assert_not_called()
+
+ @mock.patch('alot.commands.globals.settings.get', mock.Mock(return_value=''))
+ @mock.patch.dict(os.environ, {'DISPLAY': ':0'})
+ def test_spawn_failure(self):
+ ui = mock.Mock()
+ cmd = g_commands.ExternalCommand(u'false', refocus=False, spawn=True)
+ cmd.apply(ui)
+ ui.notify.assert_called_once_with('', priority='error')
diff --git a/tests/helper_test.py b/tests/helper_test.py
index 62e37ed8..c2927725 100644
--- a/tests/helper_test.py
+++ b/tests/helper_test.py
@@ -18,17 +18,21 @@
"""Test suite for alot.helper module."""
from __future__ import absolute_import
-
import datetime
import email
import errno
+import os
import random
-import unittest
import mock
+from twisted.trial import unittest
+from twisted.internet.defer import inlineCallbacks
+from twisted.internet.error import ProcessTerminated
from alot import helper
+from . import utilities
+
# Descriptive names for tests often violate PEP8. That's not an issue, users
# aren't meant to call these functions.
# pylint: disable=invalid-name
@@ -257,7 +261,7 @@ class TestPrettyDatetime(unittest.TestCase):
self.assertEqual(actual, expected)
# Returns 'just now', instead of 'from future' or something similar
- @unittest.expectedFailure
+ @utilities.expected_failure
def test_future_minutes(self):
test = self.now + datetime.timedelta(minutes=5)
actual = helper.pretty_datetime(test)
@@ -265,7 +269,7 @@ class TestPrettyDatetime(unittest.TestCase):
self.assertEqual(actual, expected)
# Returns 'just now', instead of 'from future' or something similar
- @unittest.expectedFailure
+ @utilities.expected_failure
def test_future_hours(self):
test = self.now + datetime.timedelta(hours=1)
actual = helper.pretty_datetime(test)
@@ -273,7 +277,7 @@ class TestPrettyDatetime(unittest.TestCase):
self.assertEqual(actual, expected)
# Returns 'just now', instead of 'from future' or something similar
- @unittest.expectedFailure
+ @utilities.expected_failure
def test_future_days(self):
def make_expected():
# Uses the hourfmt instead of the hourminfmt from pretty_datetime
@@ -290,7 +294,7 @@ class TestPrettyDatetime(unittest.TestCase):
self.assertEqual(actual, expected)
# Returns 'just now', instead of 'from future' or something similar
- @unittest.expectedFailure
+ @utilities.expected_failure
def test_future_week(self):
test = self.now + datetime.timedelta(days=7)
actual = helper.pretty_datetime(test)
@@ -298,7 +302,7 @@ class TestPrettyDatetime(unittest.TestCase):
self.assertEqual(actual, expected)
# Returns 'just now', instead of 'from future' or something similar
- @unittest.expectedFailure
+ @utilities.expected_failure
def test_future_month(self):
test = self.now + datetime.timedelta(days=31)
actual = helper.pretty_datetime(test)
@@ -306,7 +310,7 @@ class TestPrettyDatetime(unittest.TestCase):
self.assertEqual(actual, expected)
# Returns 'just now', instead of 'from future' or something similar
- @unittest.expectedFailure
+ @utilities.expected_failure
def test_future_year(self):
test = self.now + datetime.timedelta(days=365)
actual = helper.pretty_datetime(test)
@@ -402,3 +406,61 @@ class TestEmailAsString(unittest.TestCase):
actual = helper.email_as_string(message)
expected = 'X-Unicode-Header: dummy value\r\n\r\n'
self.assertEqual(actual, expected)
+
+
+class TestShorten(unittest.TestCase):
+
+ def test_lt_maxlen(self):
+ expected = u'a string'
+ actual = helper.shorten(expected, 25)
+ self.assertEqual(expected, actual)
+
+ def test_eq_maxlen(self):
+ expected = 'a string'
+ actual = helper.shorten(expected, len(expected))
+ self.assertEqual(expected, actual)
+
+ def test_gt_maxlen(self):
+ expected = u'a long string…'
+ actual = helper.shorten('a long string that is full of text', 14)
+ self.assertEqual(expected, actual)
+
+
+class TestCallCmdAsync(unittest.TestCase):
+
+ @inlineCallbacks
+ def test_no_stdin(self):
+ ret = yield helper.call_cmd_async(['echo', '-n', 'foo'])
+ self.assertEqual(ret, 'foo')
+
+ @inlineCallbacks
+ def test_stdin(self):
+ ret = yield helper.call_cmd_async(['cat', '-'], stdin='foo')
+ self.assertEqual(ret, 'foo')
+
+ @inlineCallbacks
+ def test_env_set(self):
+ with mock.patch.dict(os.environ, {}, clear=True):
+ ret = yield 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; '
+ 'import os; '
+ 'print(os.environ.get("foo", "fail"), end="")'
+ ],
+ env={'foo': 'bar'})
+ self.assertEqual(ret, 'bar')
+
+ @inlineCallbacks
+ def test_env_doesnt_pollute(self):
+ with mock.patch.dict(os.environ, {}, clear=True):
+ yield 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)
diff --git a/tests/utilities.py b/tests/utilities.py
index 402feb38..8123afd8 100644
--- a/tests/utilities.py
+++ b/tests/utilities.py
@@ -171,3 +171,13 @@ def make_key(revoked=False, expired=False, invalid=False, can_encrypt=True,
mock_key.can_sign = can_sign
return mock_key
+
+
+def expected_failure(func):
+ """For marking expected failures for twisted.trial based unit tests.
+
+ The builtin unittest.expectedFailure does not work with twisted.trail,
+ there is an outstanding bug for this, but no one has ever fixed it.
+ """
+ func.todo = 'expected failure'
+ return func