diff options
author | Patrick Totzke <patricktotzke@gmail.com> | 2017-09-02 08:35:01 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-09-02 08:35:01 +0100 |
commit | 668925817cd8aeed7ef9926969d49c4586c26adf (patch) | |
tree | 0cd16d2279a971d0b5d650f03ee3f6ce0e592852 /alot | |
parent | 3a3898f2ce976fbb800f55764675c4962e7adf28 (diff) | |
parent | c09196eb92af61ce0efa2f2ea47f42856ef87ac9 (diff) |
Merge branch 'master' into fix/spelling
Diffstat (limited to 'alot')
-rw-r--r-- | alot/account.py | 149 | ||||
-rw-r--r-- | alot/addressbook/abook.py | 3 | ||||
-rw-r--r-- | alot/commands/__init__.py | 1 | ||||
-rw-r--r-- | alot/commands/envelope.py | 9 | ||||
-rw-r--r-- | alot/commands/globals.py | 9 | ||||
-rw-r--r-- | alot/commands/thread.py | 6 | ||||
-rw-r--r-- | alot/crypto.py | 44 | ||||
-rw-r--r-- | alot/db/manager.py | 3 | ||||
-rw-r--r-- | alot/db/message.py | 9 | ||||
-rw-r--r-- | alot/db/utils.py | 29 | ||||
-rw-r--r-- | alot/defaults/alot.rc.spec | 8 | ||||
-rw-r--r-- | alot/settings/manager.py | 12 | ||||
-rw-r--r-- | alot/settings/utils.py | 1 | ||||
-rw-r--r-- | alot/utils/cached_property.py | 48 | ||||
-rw-r--r-- | alot/widgets/globals.py | 4 | ||||
-rw-r--r-- | alot/widgets/search.py | 5 |
16 files changed, 250 insertions, 90 deletions
diff --git a/alot/account.py b/alot/account.py index 30f0e8da..a224c109 100644 --- a/alot/account.py +++ b/alot/account.py @@ -1,4 +1,6 @@ +# encoding=utf-8 # Copyright (C) 2011-2012 Patrick Totzke <patricktotzke@gmail.com> +# Copyright © 2017 Dylan Baker # This file is released under the GNU GPL, version 3 or a later revision. # For further details see the COPYING file from __future__ import absolute_import @@ -7,12 +9,152 @@ import abc import glob import logging import mailbox +import operator import os from .helper import call_cmd_async from .helper import split_commandstring +class Address(object): + + """A class that represents an email address. + + This class implements a number of RFC requirements (as explained in detail + below) specifically in the comparison of email addresses to each other. + + This class abstracts the requirements of RFC 5321 § 2.4 on the user name + portion of the email: + + local-part of a mailbox MUST BE treated as case sensitive. Therefore, + SMTP implementations MUST take care to preserve the case of mailbox + local-parts. In particular, for some hosts, the user "smith" is + different from the user "Smith". However, exploiting the case + sensitivity of mailbox local-parts impedes interoperability and is + discouraged. Mailbox domains follow normal DNS rules and are hence not + case sensitive. + + This is complicated by § 2.3.11 of the same RFC: + + The standard mailbox naming convention is defined to be + "local-part@domain"; contemporary usage permits a much broader set of + applications than simple "user names". Consequently, and due to a long + history of problems when intermediate hosts have attempted to optimize + transport by modifying them, the local-part MUST be interpreted and + assigned semantics only by the host specified in the domain part of the + address. + + And also the restrictions that RFC 1035 § 3.1 places on the domain name: + + Name servers and resolvers must compare [domains] in a case-insensitive + manner + + Because of RFC 6531 § 3.2, we take special care to ensure that unicode + names will work correctly: + + An SMTP server that announces the SMTPUTF8 extension MUST be prepared + to accept a UTF-8 string [RFC3629] in any position in which RFC 5321 + specifies that a <mailbox> can appear. Although the characters in the + <local-part> are permitted to contain non-ASCII characters, the actual + parsing of the <local-part> and the delimiters used are unchanged from + the base email specification [RFC5321] + + What this means is that the username can be either case-insensitive or not, + but only the receiving SMTP server can know what it's own rules are. The + consensus is that the vast majority (all?) of the SMTP servers in modern + usage treat user names as case-insensitve. Therefore we also, by default, + treat the user name as case insenstive. + + :param unicode user: The "user name" portion of the address. + :param unicode domain: The domain name portion of the address. + :param bool case_sensitive: If False (the default) the user name portion of + the address will be compared to the other user name portion without + regard to case. If True then it will. + """ + + def __init__(self, user, domain, case_sensitive=False): + assert isinstance(user, unicode), 'Username must be unicode' + assert isinstance(domain, unicode), 'Domain name must be unicode' + self.username = user + self.domainname = domain + self.case_sensitive = case_sensitive + + @classmethod + def from_string(cls, address, case_sensitive=False): + """Alternate constructor for building from a string. + + :param unicode address: An email address in <user>@<domain> form + :param bool case_sensitive: passed directly to the constructor argument + of the same name. + :returns: An account from the given arguments + :rtype: :class:`Account` + """ + assert isinstance(address, unicode), 'address must be unicode' + username, domainname = address.split(u'@') + return cls(username, domainname, case_sensitive=case_sensitive) + + def __repr__(self): + return u'Address({!r}, {!r}, case_sensitive={})'.format( + self.username, + self.domainname, + unicode(self.case_sensitive)) + + def __unicode__(self): + return u'{}@{}'.format(self.username, self.domainname) + + def __str__(self): + return u'{}@{}'.format(self.username, self.domainname).encode('utf-8') + + def __cmp(self, other, comparitor): + """Shared helper for rich comparison operators. + + This allows the comparison operators to be relatively simple and share + the complex logic. + + If the username is not considered case sensitive then lower the + username of both self and the other, and handle that the other can be + either another :class:`~alot.account.Address`, or a `unicode` instance. + + :param other: The other address to compare against + :type other: unicode or ~alot.account.Address + :param callable comparitor: A function with the a signature + (unicode, unicode) -> bool that will compare the two instance. + The intention is to use functions from the operator module. + """ + if isinstance(other, unicode): + ouser, odomain = other.split(u'@') + elif isinstance(other, str): + ouser, odomain = other.decode('utf-8').split(u'@') + else: + ouser = other.username + odomain = other.domainname + + if not self.case_sensitive: + ouser = ouser.lower() + username = self.username.lower() + else: + username = self.username + + return (comparitor(username, ouser) and + comparitor(self.domainname.lower(), odomain.lower())) + + def __eq__(self, other): + if not isinstance(other, (Address, basestring)): + raise TypeError('Address must be compared to Address or basestring') + return self.__cmp(other, operator.eq) + + def __ne__(self, other): + if not isinstance(other, (Address, basestring)): + raise TypeError('Address must be compared to Address or basestring') + # != is the only rich comparitor that cannot be implemented using 'and' + # in self.__cmp, so it's implemented as not ==. + return not self.__cmp(other, operator.eq) + + def __hash__(self): + return hash((self.username.lower(), self.domainname.lower(), + self.case_sensitive)) + + class SendingMailFailed(RuntimeError): pass @@ -59,7 +201,7 @@ class Account(object): signature_filename=None, signature_as_attachment=False, sent_box=None, sent_tags=None, draft_box=None, draft_tags=None, abook=None, sign_by_default=False, - encrypt_by_default=u"none", + encrypt_by_default=u"none", case_sensitive_username=False, **_): sent_tags = sent_tags or [] if 'sent' not in sent_tags: @@ -68,8 +210,9 @@ class Account(object): if 'draft' not in draft_tags: draft_tags.append('draft') - self.address = address - self.aliases = aliases or [] + self.address = Address.from_string(address, case_sensitive=case_sensitive_username) + self.aliases = [Address.from_string(a, case_sensitive=case_sensitive_username) + for a in (aliases or [])] self.alias_regexp = alias_regexp self.realname = realname self.gpg_key = gpg_key diff --git a/alot/addressbook/abook.py b/alot/addressbook/abook.py index b620fbac..41b5103e 100644 --- a/alot/addressbook/abook.py +++ b/alot/addressbook/abook.py @@ -16,7 +16,8 @@ class AbookAddressBook(AddressBook): :type path: str """ AddressBook.__init__(self, **kwargs) - DEFAULTSPATH = os.path.join(os.path.dirname(__file__), '..', 'defaults') + DEFAULTSPATH = os.path.join(os.path.dirname(__file__), '..', + 'defaults') self._spec = os.path.join(DEFAULTSPATH, 'abook_contacts.spec') path = os.path.expanduser(path) self._config = read_config(path, self._spec) diff --git a/alot/commands/__init__.py b/alot/commands/__init__.py index 0638926a..bcd27c07 100644 --- a/alot/commands/__init__.py +++ b/alot/commands/__init__.py @@ -34,6 +34,7 @@ class CommandCanceled(Exception): """ pass + COMMANDS = { 'search': {}, 'envelope': {}, diff --git a/alot/commands/envelope.py b/alot/commands/envelope.py index 9681a020..f497ecc2 100644 --- a/alot/commands/envelope.py +++ b/alot/commands/envelope.py @@ -124,9 +124,8 @@ class SaveCommand(Command): return if account.draft_box is None: - ui.notify( - 'abort: account <%s> has no draft_box set.' % envelope.get('From'), - priority='error') + msg = 'abort: Account for {} has no draft_box' + ui.notify(msg.format(account.address), priority='error') return mail = envelope.construct_mail() @@ -511,8 +510,8 @@ class SignCommand(Command): return if not acc.gpg_key: envelope.sign = False - ui.notify('Account for {} has no gpg key'.format(acc.address), - priority='error') + msg = 'Account for {} has no gpg key' + ui.notify(msg.format(acc.address), priority='error') return envelope.sign_key = acc.gpg_key else: diff --git a/alot/commands/globals.py b/alot/commands/globals.py index 565bd742..57ddfcb6 100644 --- a/alot/commands/globals.py +++ b/alot/commands/globals.py @@ -78,7 +78,8 @@ class ExitCommand(Command): if ui.db_was_locked: msg = 'Database locked. Exit without saving?' - if (yield ui.choice(msg, msg_position='left', cancel='no')) == 'no': + response = yield ui.choice(msg, msg_position='left', cancel='no') + if response == 'no': return ui.exit() @@ -855,7 +856,8 @@ class ComposeCommand(Command): self.envelope.sign = account.sign_by_default self.envelope.sign_key = account.gpg_key else: - msg = 'Cannot find gpg key for account {}'.format(account.address) + msg = 'Cannot find gpg key for account {}' + msg = msg.format(account.address) logging.warning(msg) ui.notify(msg, priority='error') @@ -910,7 +912,8 @@ class ComposeCommand(Command): logging.debug("Trying to encrypt message because " "account.encrypt_by_default=%s", account.encrypt_by_default) - yield set_encrypt(ui, self.envelope, block_error=self.encrypt, signed_only=True) + yield set_encrypt(ui, self.envelope, block_error=self.encrypt, + signed_only=True) else: logging.debug("No encryption by default, encrypt_by_default=%s", account.encrypt_by_default) diff --git a/alot/commands/thread.py b/alot/commands/thread.py index 4c04664f..b0eff761 100644 --- a/alot/commands/thread.py +++ b/alot/commands/thread.py @@ -71,11 +71,13 @@ def determine_sender(mail, action='reply'): # pick the most important account that has an address in candidates # and use that accounts realname and the address found here for account in my_accounts: - acc_addresses = [re.escape(a) for a in account.get_addresses()] + acc_addresses = [re.escape(unicode(a)) for a in account.get_addresses()] if account.alias_regexp is not None: acc_addresses.append(account.alias_regexp) for alias in acc_addresses: - regex = re.compile('^' + alias + '$', flags=re.IGNORECASE) + regex = re.compile( + u'^' + unicode(alias) + u'$', + flags=re.IGNORECASE if not account.address.case_sensitive else 0) for seen_name, seen_address in candidate_addresses: if regex.match(seen_address): logging.debug("match!: '%s' '%s'", seen_address, alias) diff --git a/alot/crypto.py b/alot/crypto.py index 52eb8c58..6e3e8fa6 100644 --- a/alot/crypto.py +++ b/alot/crypto.py @@ -81,31 +81,22 @@ def get_key(keyid, validate=False, encrypt=False, sign=False, valid_key = None - # Catching exceptions for list_keys - try: - for k in list_keys(hint=keyid): - try: - validate_key(k, encrypt=encrypt, sign=sign) - except GPGProblem: - # if the key is invalid for given action skip it - continue - - if valid_key: - # we have already found one valid key and now we find - # another? We really received an ambiguous keyid - raise GPGProblem( - "More than one key found matching this filter. " - "Please be more specific " - "(use a key ID like 4AC8EE1D).", - code=GPGCode.AMBIGUOUS_NAME) - valid_key = k - except gpg.errors.GPGMEError as e: - # This if will be triggered if there is no key matching at all. - if e.getcode() == gpg.errors.AMBIGUOUS_NAME: + for k in list_keys(hint=keyid): + try: + validate_key(k, encrypt=encrypt, sign=sign) + except GPGProblem: + # if the key is invalid for given action skip it + continue + + if valid_key: + # we have already found one valid key and now we find + # another? We really received an ambiguous keyid raise GPGProblem( - 'Can not find any key for "{}".'.format(keyid), - code=GPGCode.NOT_FOUND) - raise + "More than one key found matching this filter. " + "Please be more specific " + "(use a key ID like 4AC8EE1D).", + code=GPGCode.AMBIGUOUS_NAME) + valid_key = k if not valid_key: # there were multiple keys found but none of them are valid for @@ -120,7 +111,7 @@ def get_key(keyid, validate=False, encrypt=False, sign=False, 'Can not find usable key for "{}".'.format(keyid), code=GPGCode.NOT_FOUND) else: - raise e + raise e # pragma: nocover if signed_only and not check_uid_validity(key, keyid): raise GPGProblem('Cannot find a trusworthy key for "{}".'.format(keyid), code=GPGCode.NOT_FOUND) @@ -161,7 +152,8 @@ def detached_signature_for(plaintext_str, keys): """ ctx = gpg.core.Context(armor=True) ctx.signers = keys - (sigblob, sign_result) = ctx.sign(plaintext_str, mode=gpg.constants.SIG_MODE_DETACH) + (sigblob, sign_result) = ctx.sign(plaintext_str, + mode=gpg.constants.SIG_MODE_DETACH) return sign_result.signatures, sigblob diff --git a/alot/db/manager.py b/alot/db/manager.py index d71812d1..dfd681d0 100644 --- a/alot/db/manager.py +++ b/alot/db/manager.py @@ -377,7 +377,8 @@ class DBManager(object): :param sort: Sort order. one of ['oldest_first', 'newest_first', 'message_id', 'unsorted'] :type query: str - :param exclude_tags: Tags to exclude by default unless included in the search + :param exclude_tags: Tags to exclude by default unless included in the + search :type exclude_tags: list of str :returns: a pipe together with the process that asynchronously writes to it. diff --git a/alot/db/message.py b/alot/db/message.py index f8dcb74f..9d7437eb 100644 --- a/alot/db/message.py +++ b/alot/db/message.py @@ -66,17 +66,17 @@ class Message(object): def __eq__(self, other): if isinstance(other, type(self)): - return self.get_message_id() == other.get_message_id() + return self._id == other.get_message_id() return NotImplemented def __ne__(self, other): if isinstance(other, type(self)): - return self.get_message_id() != other.get_message_id() + return self._id != other.get_message_id() return NotImplemented def __lt__(self, other): if isinstance(other, type(self)): - return self.get_message_id() < other.get_message_id() + return self._id < other.get_message_id() return NotImplemented def get_email(self): @@ -116,8 +116,7 @@ class Message(object): def get_tags(self): """returns tags attached to this message as list of strings""" - l = sorted(self._tags) - return l + return sorted(self._tags) def get_thread(self): """returns the :class:`~alot.db.Thread` this msg belongs to""" diff --git a/alot/db/utils.py b/alot/db/utils.py index 2db850ce..379fd6a2 100644 --- a/alot/db/utils.py +++ b/alot/db/utils.py @@ -45,6 +45,8 @@ def add_signature_headers(mail, sigs, error_msg): string indicating no error ''' sig_from = u'' + sig_known = True + uid_trusted = False if isinstance(error_msg, str): error_msg = error_msg.decode('utf-8') @@ -60,13 +62,11 @@ def add_signature_headers(mail, sigs, error_msg): uid_trusted = True break else: - # No trusted uid found, we did not break but drop from the - # for loop. - uid_trusted = False + # No trusted uid found, since we did not break from the loop. sig_from = key.uids[0].uid.decode('utf-8') - except: + except GPGProblem: sig_from = sigs[0].fpr.decode('utf-8') - uid_trusted = False + sig_known = False if error_msg: msg = u'Invalid: {}'.format(error_msg) @@ -75,7 +75,8 @@ def add_signature_headers(mail, sigs, error_msg): else: msg = u'Untrusted: {}'.format(sig_from) - mail.add_header(X_SIGNATURE_VALID_HEADER, 'False' if error_msg else 'True') + mail.add_header(X_SIGNATURE_VALID_HEADER, + 'False' if (error_msg or not sig_known) else 'True') mail.add_header(X_SIGNATURE_MESSAGE_HEADER, msg) @@ -297,16 +298,20 @@ def extract_headers(mail, headers=None): def extract_body(mail, types=None, field_key='copiousoutput'): - """ - returns a body text string for given mail. - If types is `None`, `text/*` is used: - The exact preferred type is specified by the prefer_plaintext config option - which defaults to text/html. + """Returns a string view of a Message. + + If the `types` argument is set then any encoding types there will be used + as the prefered encoding to extract. If `types` is None then + :ref:`prefer_plaintext <prefer-plaintext>` will be consulted; if it is True + then text/plain parts will be returned, if it is false then text/html will + be returned if present or text/plain if there are no text/html parts. :param mail: the mail to use :type mail: :class:`email.Message` :param types: mime content types to use for body string - :type types: list of str + :type types: list[str] + :returns: The combined text of any parts to be used + :rtype: str """ preferred = 'text/plain' if settings.get( diff --git a/alot/defaults/alot.rc.spec b/alot/defaults/alot.rc.spec index 97de329d..eeae4348 100644 --- a/alot/defaults/alot.rc.spec +++ b/alot/defaults/alot.rc.spec @@ -355,6 +355,14 @@ thread_focus_linewise = boolean(default=True) # use your default key. gpg_key = gpg_key_hint(default=None) + # Whether the server treats the address as case-senstive or + # case-insensitve (True for the former, False for the latter) + # + # .. note:: The vast majority (if not all) SMTP servers in modern use + # treat usernames as case insenstive, you should only set + # this if you know that you need it. + case_sensitive_username = boolean(default=False) + # address book for this account [[[abook]]] # type identifier for address book diff --git a/alot/settings/manager.py b/alot/settings/manager.py index cf26f941..c71fba42 100644 --- a/alot/settings/manager.py +++ b/alot/settings/manager.py @@ -25,7 +25,8 @@ from .theme import Theme DEFAULTSPATH = os.path.join(os.path.dirname(__file__), '..', 'defaults') -DATA_DIRS = os.environ.get('XDG_DATA_DIRS', '/usr/local/share:/usr/share').split(':') +DATA_DIRS = os.environ.get('XDG_DATA_DIRS', + '/usr/local/share:/usr/share').split(':') class SettingsManager(object): @@ -37,8 +38,10 @@ class SettingsManager(object): :param notmuch_rc: path to notmuch's config file :type notmuch_rc: str """ - assert alot_rc is None or (isinstance(alot_rc, basestring) and os.path.exists(alot_rc)) - assert notmuch_rc is None or (isinstance(notmuch_rc, basestring) and os.path.exists(notmuch_rc)) + assert alot_rc is None or (isinstance(alot_rc, basestring) and + os.path.exists(alot_rc)) + assert notmuch_rc is None or (isinstance(notmuch_rc, basestring) and + os.path.exists(notmuch_rc)) self.hooks = None self._mailcaps = mailcap.getcaps() self._notmuchconfig = None @@ -62,7 +65,8 @@ class SettingsManager(object): Implementation Detail: this is the same code called by the constructor to set bindings at alot startup. """ - self._bindings = ConfigObj(os.path.join(DEFAULTSPATH, 'default.bindings')) + self._bindings = ConfigObj(os.path.join(DEFAULTSPATH, + 'default.bindings')) self.read_notmuch_config() self.read_config() diff --git a/alot/settings/utils.py b/alot/settings/utils.py index ea56b264..d87157c3 100644 --- a/alot/settings/utils.py +++ b/alot/settings/utils.py @@ -9,6 +9,7 @@ from urwid import AttrSpec from .errors import ConfigError + def read_config(configpath=None, specpath=None, checks=None): """ get a (validated) config object for given config file path. diff --git a/alot/utils/cached_property.py b/alot/utils/cached_property.py index e6187283..680cd6f4 100644 --- a/alot/utils/cached_property.py +++ b/alot/utils/cached_property.py @@ -1,34 +1,34 @@ # verbatim from werkzeug.utils.cached_property # -#Copyright (c) 2014 by the Werkzeug Team, see AUTHORS for more details. +# Copyright (c) 2014 by the Werkzeug Team, see AUTHORS for more details. # -#Redistribution and use in source and binary forms, with or without -#modification, are permitted provided that the following conditions are -#met: +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: # -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. # -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. # -# * The names of the contributors may not be used to endorse or -# promote products derived from this software without specific -# prior written permission. +# * The names of the contributors may not be used to endorse or +# promote products derived from this software without specific +# prior written permission. # -#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -#"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -#LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -#A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -#OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -#SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -#LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -#DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -#THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -#(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -#OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. _missing = object() diff --git a/alot/widgets/globals.py b/alot/widgets/globals.py index 50ef80c6..3aea7622 100644 --- a/alot/widgets/globals.py +++ b/alot/widgets/globals.py @@ -86,8 +86,8 @@ class CompleteEdit(urwid.Edit): The interpretation of some keypresses is hard-wired: :enter: calls 'on_exit' callback with current value - :esc/ctrl g: calls 'on_exit' with value `None`, which can be interpreted - as cancellation + :esc/ctrl g: calls 'on_exit' with value `None`, which can be + interpreted as cancellation :tab: calls the completer and tabs forward in the result list :shift tab: tabs backward in the result list :up/down: move in the local input history diff --git a/alot/widgets/search.py b/alot/widgets/search.py index 905887cc..446a8964 100644 --- a/alot/widgets/search.py +++ b/alot/widgets/search.py @@ -115,8 +115,9 @@ class ThreadlineWidget(urwid.AttrMap): if self.thread: fallback_normal = struct[name]['normal'] fallback_focus = struct[name]['focus'] - tag_widgets = sorted(TagWidget(t, fallback_normal, fallback_focus) - for t in self.thread.get_tags()) + tag_widgets = sorted( + TagWidget(t, fallback_normal, fallback_focus) + for t in self.thread.get_tags()) else: tag_widgets = [] cols = [] |