diff options
40 files changed, 742 insertions, 736 deletions
diff --git a/.travis.yml b/.travis.yml index 84c05c4d..3e7b5035 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,34 +14,14 @@ dist: trusty python: # We can add more version strings here when we support other python # versions. - - "2.7" - "3.5" - "3.6" # We start two containers in parallel, one to check and build the docs and the # other to run the test suite. env: + - JOB=docs - JOB=tests - # This job is temporarily included in the build matrix directly in order to - # only check the docs on python2 for now. When we finished switching to - # python3 we can put this back here and remove matrix.include. - #- JOB=docs - -# Until the switch to python3 is complete we allow the tests to fail with -# python3. When merging a working python3 version wen can remove this and the -# python version 2.7 above. -jobs: - allow_failures: - - python: "3.5" - - python: "3.6" - -# Check the docs only on python2 until we really support python3. See "env" -# above and -# https://docs.travis-ci.com/user/customizing-the-build/#Explicitly-Including-Jobs -matrix: - include: - - python: 2.7 - env: JOB=docs addons: apt: @@ -1,3 +1,6 @@ +0.8: +* Port to python 3. Python 2.x no longer supported + 0.7: * info: missing html mailcap entry now reported as mail body text * feature: Allow regex special characters in tagstrings diff --git a/alot/account.py b/alot/account.py index 00c65753..461d2bbf 100644 --- a/alot/account.py +++ b/alot/account.py @@ -65,16 +65,16 @@ class Address(object): 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 str user: The "user name" portion of the address. + :param str 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' + assert isinstance(user, str), 'Username must be str' + assert isinstance(domain, str), 'Domain name must be str' self.username = user self.domainname = domain self.case_sensitive = case_sensitive @@ -83,27 +83,24 @@ class Address(object): 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 str 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'@') + assert isinstance(address, str), 'address must be str' + username, domainname = address.split('@') return cls(username, domainname, case_sensitive=case_sensitive) def __repr__(self): - return u'Address({!r}, {!r}, case_sensitive={})'.format( + return 'Address({!r}, {!r}, case_sensitive={})'.format( self.username, self.domainname, - unicode(self.case_sensitive)) - - def __unicode__(self): - return u'{}@{}'.format(self.username, self.domainname) + str(self.case_sensitive)) def __str__(self): - return u'{}@{}'.format(self.username, self.domainname).encode('utf-8') + return '{}@{}'.format(self.username, self.domainname) def __cmp(self, other, comparitor): """Shared helper for rich comparison operators. @@ -113,22 +110,17 @@ class Address(object): 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. + either another :class:`~alot.account.Address`, or a `str` instance. :param other: The other address to compare against - :type other: unicode or ~alot.account.Address + :type other: str or ~alot.account.Address :param callable comparitor: A function with the a signature - (unicode, unicode) -> bool that will compare the two instance. + (str, str) -> bool that will compare the two instance. The intention is to use functions from the operator module. """ - if isinstance(other, unicode): - try: - ouser, odomain = other.split(u'@') - except ValueError: - ouser, odomain = u'', u'' - elif isinstance(other, str): + if isinstance(other, str): try: - ouser, odomain = other.decode('utf-8').split(u'@') + ouser, odomain = other.split('@') except ValueError: ouser, odomain = '', '' else: @@ -145,13 +137,13 @@ class Address(object): 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') + if not isinstance(other, (Address, str)): + raise TypeError('Address must be compared to Address or str') 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') + if not isinstance(other, (Address, str)): + raise TypeError('Address must be compared to Address or str') # != 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) diff --git a/alot/buffers.py b/alot/buffers.py index d1c6583b..bfff3897 100644 --- a/alot/buffers.py +++ b/alot/buffers.py @@ -147,7 +147,7 @@ class EnvelopeBuffer(Buffer): hidden = settings.get('envelope_headers_blacklist') # build lines lines = [] - for (k, vlist) in self.envelope.headers.iteritems(): + for (k, vlist) in self.envelope.headers.items(): if (k not in hidden) or self.all_headers: for value in vlist: lines.append((k, value)) @@ -662,7 +662,7 @@ class TagListBuffer(Buffer): lines = list() displayedtags = sorted((t for t in self.tags if self.filtfun(t)), - key=unicode.lower) + key=str.lower) for (num, b) in enumerate(displayedtags): if (num % 2) == 0: attr = settings.get_theming_attribute('taglist', 'line_even') diff --git a/alot/commands/envelope.py b/alot/commands/envelope.py index 0aa95cf2..0485aa00 100644 --- a/alot/commands/envelope.py +++ b/alot/commands/envelope.py @@ -145,8 +145,8 @@ class SaveCommand(Command): ui.apply_command(globals.FlushCommand()) ui.apply_command(commands.globals.BufferCloseCommand()) except DatabaseError as e: - logging.error(e) - ui.notify('could not index message:\n%s' % e, + logging.error(str(e)) + ui.notify('could not index message:\n%s' % str(e), priority='error', block=True) else: @@ -629,13 +629,13 @@ class TagCommand(Command): def __init__(self, tags=u'', action='add', **kwargs): """ :param tags: comma separated list of tagstrings to set - :type tags: unicode + :type tags: str :param action: adds tags if 'add', removes them if 'remove', adds tags and removes all other if 'set' or toggle individually if 'toggle' :type action: str """ - assert isinstance(tags, unicode), 'tags should be a unicode string' + assert isinstance(tags, str), 'tags should be a unicode string' self.tagsstring = tags self.action = action Command.__init__(self, **kwargs) diff --git a/alot/commands/globals.py b/alot/commands/globals.py index 4e0ed506..d335e8cf 100644 --- a/alot/commands/globals.py +++ b/alot/commands/globals.py @@ -12,7 +12,7 @@ import glob import logging import os import subprocess -from io import StringIO +from io import BytesIO import urwid from twisted.internet.defer import inlineCallbacks @@ -211,7 +211,7 @@ class ExternalCommand(Command): """ logging.debug({'spawn': spawn}) # make sure cmd is a list of str - if isinstance(cmd, unicode): + if isinstance(cmd, str): # convert cmdstring to list: in case shell==True, # Popen passes only the first item in the list to $SHELL cmd = [cmd] if shell else split_commandstring(cmd) @@ -249,9 +249,11 @@ class ExternalCommand(Command): # set standard input for subcommand stdin = None if self.stdin is not None: - # wrap strings in StringIO so that they behave like files - if isinstance(self.stdin, unicode): - stdin = StringIO(self.stdin) + # wrap strings in StrinIO so that they behaves like a file + if isinstance(self.stdin, str): + # XXX: is utf-8 always safe to use here, or do we need to check + # the terminal encoding first? + stdin = BytesIO(self.stdin.encode('utf-8')) else: stdin = self.stdin @@ -278,7 +280,9 @@ class ExternalCommand(Command): _, err = proc.communicate(stdin.read() if stdin else None) if proc.returncode == 0: return 'success' - return err.strip() + if err: + return err.decode(urwid.util.detected_encoding) + return '' if self.in_thread: d = threads.deferToThread(thread_code) @@ -381,7 +385,7 @@ class CallCommand(Command): hooks = settings.hooks if hooks: env = {'ui': ui, 'settings': settings} - for k, v in env.iteritems(): + for k, v in env.items(): if k not in hooks.__dict__: hooks.__dict__[k] = v @@ -619,7 +623,7 @@ class HelpCommand(Command): globalmaps, modemaps = settings.get_keybindings(ui.mode) # build table - maxkeylength = len(max((modemaps).keys() + globalmaps.keys(), + maxkeylength = len(max(list(modemaps.keys()) + list(globalmaps.keys()), key=len)) keycolumnwidth = maxkeylength + 2 @@ -628,7 +632,7 @@ class HelpCommand(Command): if modemaps: txt = (section_att, '\n%s-mode specific maps' % ui.mode) linewidgets.append(urwid.Text(txt)) - for (k, v) in modemaps.iteritems(): + for (k, v) in modemaps.items(): line = urwid.Columns([('fixed', keycolumnwidth, urwid.Text((text_att, k))), urwid.Text((text_att, v))]) @@ -636,7 +640,7 @@ class HelpCommand(Command): # global maps linewidgets.append(urwid.Text((section_att, '\nglobal maps'))) - for (k, v) in globalmaps.iteritems(): + for (k, v) in globalmaps.items(): if k not in modemaps: line = urwid.Columns( [('fixed', keycolumnwidth, urwid.Text((text_att, k))), @@ -770,16 +774,14 @@ class ComposeCommand(Command): return try: with open(path, 'rb') as f: - blob = f.read() - encoding = helper.guess_encoding(blob) - logging.debug('template encoding: `%s`' % encoding) - self.envelope.parse_template(blob.decode(encoding)) + template = helper.try_decode(f.read()) + self.envelope.parse_template(template) except Exception as e: ui.notify(str(e), priority='error') return # set forced headers - for key, value in self.headers.iteritems(): + for key, value in self.headers.items(): self.envelope.add(key, value) # set forced headers for separate parameters @@ -844,10 +846,9 @@ class ComposeCommand(Command): else: with open(sig) as f: sigcontent = f.read() - enc = helper.guess_encoding(sigcontent) mimetype = helper.guess_mimetype(sigcontent) if mimetype.startswith('text'): - sigcontent = helper.string_decode(sigcontent, enc) + sigcontent = helper.try_decode(sigcontent) self.envelope.body += '\n' + sigcontent else: ui.notify('could not locate signature: %s' % sig, diff --git a/alot/commands/thread.py b/alot/commands/thread.py index 17fc6c3b..a6380ac9 100644 --- a/alot/commands/thread.py +++ b/alot/commands/thread.py @@ -14,7 +14,8 @@ from email.utils import getaddresses, parseaddr, formataddr from email.message import Message from twisted.internet.defer import inlineCallbacks -from io import BytesIO +import urwid +from io import StringIO from . import Command, registerCommand from .globals import ExternalCommand @@ -71,12 +72,12 @@ 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(unicode(a)) for a in account.get_addresses()] + acc_addresses = [re.escape(str(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( - u'^' + unicode(alias) + u'$', + u'^' + str(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): @@ -93,7 +94,7 @@ def determine_sender(mail, action='reply'): logging.debug('using realname: "%s"', realname) logging.debug('using address: %s', address) - from_value = formataddr((realname, address)) + from_value = formataddr((realname, str(address))) return from_value, account # revert to default account if nothing found @@ -103,7 +104,7 @@ def determine_sender(mail, action='reply'): logging.debug('using realname: "%s"', realname) logging.debug('using address: %s', address) - from_value = formataddr((realname, address)) + from_value = formataddr((realname, str(address))) return from_value, account @@ -301,7 +302,7 @@ class ReplyCommand(Command): new_value = [] for name, address in getaddresses(value): if address not in my_addresses: - new_value.append(formataddr((name, address))) + new_value.append(formataddr((name, str(address)))) return new_value @staticmethod @@ -313,7 +314,7 @@ class ReplyCommand(Command): res = dict() for name, address in getaddresses(recipients): res[address] = name - urecipients = [formataddr((n, a)) for a, n in res.iteritems()] + urecipients = [formataddr((n, str(a))) for a, n in res.items()] return sorted(urecipients) @@ -689,7 +690,7 @@ class PipeCommand(Command): :type field_key: str """ Command.__init__(self, **kwargs) - if isinstance(cmd, unicode): + if isinstance(cmd, str): cmd = split_commandstring(cmd) self.cmd = cmd self.whole_thread = all @@ -759,13 +760,14 @@ class PipeCommand(Command): # do the monkey for mail in pipestrings: + encoded_mail = mail.encode(urwid.util.detected_encoding) if self.background: logging.debug('call in background: %s', self.cmd) proc = subprocess.Popen(self.cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = proc.communicate(mail) + out, err = proc.communicate(encoded_mail) if self.notify_stdout: ui.notify(out) else: @@ -777,7 +779,7 @@ class PipeCommand(Command): stdin=subprocess.PIPE, # stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = proc.communicate(mail) + out, err = proc.communicate(encoded_mail) if err: ui.notify(err, priority='error') return @@ -993,7 +995,7 @@ class OpenAttachmentCommand(Command): def afterwards(): os.unlink(tempfile_name) else: - handler_stdin = BytesIO() + handler_stdin = StringIO() self.attachment.write(handler_stdin) # create handler command list diff --git a/alot/completion.py b/alot/completion.py index 48824909..e338ee0b 100644 --- a/alot/completion.py +++ b/alot/completion.py @@ -277,7 +277,7 @@ class AccountCompleter(StringlistCompleter): def __init__(self, **kwargs): accounts = settings.get_accounts() - resultlist = [email.utils.formataddr((a.realname, a.address)) + resultlist = [email.utils.formataddr((a.realname, str(a.address))) for a in accounts] StringlistCompleter.__init__(self, resultlist, match_anywhere=True, **kwargs) diff --git a/alot/crypto.py b/alot/crypto.py index 34bdccb5..247dbd19 100644 --- a/alot/crypto.py +++ b/alot/crypto.py @@ -1,6 +1,5 @@ -# encoding=utf-8 # Copyright (C) 2011-2012 Patrick Totzke <patricktotzke@gmail.com> -# Copyright © 2017 Dylan Baker <dylan@pnwbakers.com> +# Copyright © 2017-2018 Dylan Baker <dylan@pnwbakers.com> # 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 @@ -144,7 +143,7 @@ def detached_signature_for(plaintext_str, keys): A detached signature in GPG speak is a separate blob of data containing a signature for the specified plaintext. - :param str plaintext_str: text to sign + :param bytes plaintext_str: bytestring to sign :param keys: list of one or more key to sign with. :type keys: list[gpg.gpgme._gpgme_key] :returns: A list of signature and the signed blob of data @@ -160,7 +159,7 @@ def detached_signature_for(plaintext_str, keys): def encrypt(plaintext_str, keys): """Encrypt data and return the encrypted form. - :param str plaintext_str: the mail to encrypt + :param bytes plaintext_str: the mail to encrypt :param key: optionally, a list of keys to encrypt with :type key: list[gpg.gpgme.gpgme_key_t] or None :returns: encrypted mail @@ -192,8 +191,8 @@ def bad_signatures_to_str(error): def verify_detached(message, signature): """Verifies whether the message is authentic by checking the signature. - :param str message: The message to be verified, in canonical form. - :param str signature: the OpenPGP signature to verify + :param bytes message: The message to be verified, in canonical form. + :param bytes signature: the OpenPGP signature to verify :returns: a list of signatures :rtype: list[gpg.results.Signature] :raises: :class:`~alot.errors.GPGProblem` if the verification fails @@ -212,7 +211,7 @@ def decrypt_verify(encrypted): """Decrypts the given ciphertext string and returns both the signatures (if any) and the plaintext. - :param str encrypted: the mail to decrypt + :param bytes encrypted: the mail to decrypt :returns: the signatures and decrypted plaintext data :rtype: tuple[list[gpg.resuit.Signature], str] :raises: :class:`~alot.errors.GPGProblem` if the decryption fails diff --git a/alot/db/envelope.py b/alot/db/envelope.py index 90a58654..b742ba81 100644 --- a/alot/db/envelope.py +++ b/alot/db/envelope.py @@ -162,7 +162,7 @@ class Envelope(object): if isinstance(attachment, Attachment): self.attachments.append(attachment) - elif isinstance(attachment, basestring): + elif isinstance(attachment, str): path = os.path.expanduser(attachment) part = helper.mimewrap(path, filename, ctype) self.attachments.append(Attachment(part)) @@ -193,7 +193,7 @@ class Envelope(object): inner_msg = textpart if self.sign: - plaintext = helper.email_as_string(inner_msg) + plaintext = helper.email_as_bytes(inner_msg) logging.debug('signing plaintext: %s', plaintext) try: @@ -223,7 +223,7 @@ class Envelope(object): # wrap signature in MIMEcontainter stype = 'pgp-signature; name="signature.asc"' - signature_mime = MIMEApplication(_data=signature_str, + signature_mime = MIMEApplication(_data=signature_str.decode('ascii'), _subtype=stype, _encoder=encode_7or8bit) signature_mime['Content-Description'] = 'signature' @@ -237,12 +237,12 @@ class Envelope(object): unencrypted_msg = inner_msg if self.encrypt: - plaintext = helper.email_as_string(unencrypted_msg) + plaintext = helper.email_as_bytes(unencrypted_msg) logging.debug('encrypting plaintext: %s', plaintext) try: - encrypted_str = crypto.encrypt(plaintext, - self.encrypt_keys.values()) + encrypted_str = crypto.encrypt( + plaintext, list(self.encrypt_keys.values())) except gpg.errors.GPGMEError as e: raise GPGProblem(str(e), code=GPGCode.KEY_CANNOT_ENCRYPT) @@ -255,7 +255,7 @@ class Envelope(object): _encoder=encode_7or8bit) encryption_mime.set_charset('us-ascii') - encrypted_mime = MIMEApplication(_data=encrypted_str, + encrypted_mime = MIMEApplication(_data=encrypted_str.decode('ascii'), _subtype='octet-stream', _encoder=encode_7or8bit) encrypted_mime.set_charset('us-ascii') @@ -279,7 +279,7 @@ class Envelope(object): headers['User-Agent'] = [uastring] # copy headers from envelope to mail - for k, vlist in headers.iteritems(): + for k, vlist in headers.items(): for v in vlist: outer_msg[k] = encode_header(k, v) diff --git a/alot/db/manager.py b/alot/db/manager.py index 93a38b81..cbde9c88 100644 --- a/alot/db/manager.py +++ b/alot/db/manager.py @@ -145,8 +145,7 @@ class DBManager(object): msg.freeze() logging.debug('freeze') for tag in tags: - msg.add_tag(tag.encode(DB_ENC), - sync_maildir_flags=sync) + msg.add_tag(tag, sync_maildir_flags=sync) logging.debug('added tags ') msg.thaw() logging.debug('thaw') @@ -162,17 +161,14 @@ class DBManager(object): msg.freeze() if cmd == 'tag': for tag in tags: - msg.add_tag(tag.encode(DB_ENC), - sync_maildir_flags=sync) + msg.add_tag(tag, sync_maildir_flags=sync) if cmd == 'set': msg.remove_all_tags() for tag in tags: - msg.add_tag(tag.encode(DB_ENC), - sync_maildir_flags=sync) + msg.add_tag(tag, sync_maildir_flags=sync) elif cmd == 'untag': for tag in tags: - msg.remove_tag(tag.encode(DB_ENC), - sync_maildir_flags=sync) + msg.remove_tag(tag, sync_maildir_flags=sync) msg.thaw() logging.debug('ended atomic') @@ -195,7 +191,7 @@ class DBManager(object): except (XapianError, NotmuchError) as e: logging.exception(e) self.writequeue.appendleft(current_item) - raise DatabaseError(unicode(e)) + raise DatabaseError(str(e)) except DatabaseLockedError as e: logging.debug('index temporarily locked') self.writequeue.appendleft(current_item) diff --git a/alot/db/message.py b/alot/db/message.py index c4ea5f8a..2e1fef54 100644 --- a/alot/db/message.py +++ b/alot/db/message.py @@ -10,7 +10,8 @@ from datetime import datetime from notmuch import NullPointerError -from .utils import extract_body, message_from_file +from . import utils +from .utils import extract_body from .utils import decode_header from .attachment import Attachment from .. import helper @@ -64,7 +65,7 @@ class Message(object): self._from = sender elif 'draft' in self._tags: acc = settings.get_accounts()[0] - self._from = '"{}" <{}>'.format(acc.realname, unicode(acc.address)) + self._from = '"{}" <{}>'.format(acc.realname, str(acc.address)) else: self._from = '"Unknown" <>' @@ -101,8 +102,8 @@ class Message(object): "Message file is no longer accessible:\n%s" % path if not self._email: try: - with open(path) as f: - self._email = message_from_file(f) + with open(path, 'rb') as f: + self._email = utils.message_from_bytes(f.read()) except IOError: self._email = email.message_from_string(warning) return self._email diff --git a/alot/db/thread.py b/alot/db/thread.py index 3d9144db..25b75fb1 100644 --- a/alot/db/thread.py +++ b/alot/db/thread.py @@ -86,7 +86,7 @@ class Thread(object): """ tags = set(list(self._tags)) if intersection: - for m in self.get_messages().iterkeys(): + for m in self.get_messages().keys(): tags = tags.intersection(set(m.get_tags())) return tags @@ -157,7 +157,7 @@ class Thread(object): if self._authors is None: # Sort messages with date first (by date ascending), and those # without a date last. - msgs = sorted(self.get_messages().iterkeys(), + msgs = sorted(self.get_messages().keys(), key=lambda m: m.get_date() or datetime.max) orderby = settings.get('thread_authors_order_by') @@ -257,7 +257,7 @@ class Thread(object): """ mid = msg.get_message_id() msg_hash = self.get_messages() - for m in msg_hash.iterkeys(): + for m in msg_hash.keys(): if m.get_message_id() == mid: return msg_hash[m] return None diff --git a/alot/db/utils.py b/alot/db/utils.py index 14e6ebc9..5303320c 100644 --- a/alot/db/utils.py +++ b/alot/db/utils.py @@ -15,7 +15,7 @@ import tempfile import re import logging import mailcap -from io import BytesIO +import io from .. import crypto from .. import helper @@ -41,15 +41,14 @@ def add_signature_headers(mail, sigs, error_msg): :param mail: :class:`email.message.Message` the message to entitle :param sigs: list of :class:`gpg.results.Signature` - :param error_msg: `str` containing an error message, the empty - string indicating no error + :param error_msg: An error message if there is one, or None + :type error_msg: :class:`str` or `None` ''' - sig_from = u'' + sig_from = '' sig_known = True uid_trusted = False - if isinstance(error_msg, str): - error_msg = error_msg.decode('utf-8') + assert error_msg is None or isinstance(error_msg, str) if not sigs: error_msg = error_msg or u'no signature found' @@ -58,22 +57,22 @@ def add_signature_headers(mail, sigs, error_msg): key = crypto.get_key(sigs[0].fpr) for uid in key.uids: if crypto.check_uid_validity(key, uid.email): - sig_from = uid.uid.decode('utf-8') + sig_from = uid.uid uid_trusted = True break else: # No trusted uid found, since we did not break from the loop. - sig_from = key.uids[0].uid.decode('utf-8') + sig_from = key.uids[0].uid except GPGProblem: - sig_from = sigs[0].fpr.decode('utf-8') + sig_from = sigs[0].fpr sig_known = False if error_msg: - msg = u'Invalid: {}'.format(error_msg) + msg = 'Invalid: {}'.format(error_msg) elif uid_trusted: - msg = u'Valid: {}'.format(sig_from) + msg = 'Valid: {}'.format(sig_from) else: - msg = u'Untrusted: {}'.format(sig_from) + msg = 'Untrusted: {}'.format(sig_from) mail.add_header(X_SIGNATURE_VALID_HEADER, 'False' if (error_msg or not sig_known) else 'True') @@ -112,7 +111,7 @@ def _handle_signatures(original, message, params): :param params: the message parameters as returned by :func:`get_params` :type params: dict[str, str] """ - malformed = False + malformed = None if len(message.get_payload()) != 2: malformed = u'expected exactly two messages, got {0}'.format( len(message.get_payload())) @@ -133,10 +132,10 @@ def _handle_signatures(original, message, params): if not malformed: try: sigs = crypto.verify_detached( - helper.email_as_string(message.get_payload(0)), - message.get_payload(1).get_payload()) + helper.email_as_bytes(message.get_payload(0)), + message.get_payload(1).get_payload(decode=True)) except GPGProblem as e: - malformed = unicode(e) + malformed = str(e) add_signature_headers(original, sigs, malformed) @@ -170,15 +169,17 @@ def _handle_encrypted(original, message): malformed = u'expected Content-Type: {0}, got: {1}'.format(want, ct) if not malformed: + # This should be safe because PGP uses US-ASCII characters only + payload = message.get_payload(1).get_payload().encode('ascii') try: - sigs, d = crypto.decrypt_verify(message.get_payload(1).get_payload()) + sigs, d = crypto.decrypt_verify(payload) except GPGProblem as e: # signature verification failures end up here too if the combined # method is used, currently this prevents the interpretation of the # recovered plain text mail. maybe that's a feature. - malformed = unicode(e) + malformed = str(e) else: - n = message_from_string(d) + n = message_from_bytes(d) # add the decrypted message to message. note that n contains all # the attachments, no need to walk over n here. @@ -208,7 +209,7 @@ def _handle_encrypted(original, message): if malformed: msg = u'Malformed OpenPGP message: {0}'.format(malformed) - content = email.message_from_string(msg.encode('utf-8')) + content = email.message_from_string(msg) content.set_charset('utf-8') original.attach(content) @@ -265,14 +266,24 @@ def message_from_file(handle): def message_from_string(s): '''Reads a mail from the given string. This is the equivalent of :func:`email.message_from_string` which does nothing but to wrap - the given string in a BytesIO object and to call + the given string in a StringIO object and to call :func:`email.message_from_file`. Please refer to the documentation of :func:`message_from_file` for details. ''' - return message_from_file(BytesIO(s)) + return message_from_file(io.StringIO(s)) + + +def message_from_bytes(bytestring): + """Create a Message from bytes. + + Attempt to guess the encoding of the bytestring. + + :param bytes bytestring: an email message as raw bytes + """ + return message_from_file(io.StringIO(helper.try_decode(bytestring))) def extract_headers(mail, headers=None): @@ -344,8 +355,17 @@ def extract_body(mail, types=None, field_key='copiousoutput'): enc = part.get_content_charset() or 'ascii' raw_payload = part.get_payload(decode=True) + try: + raw_payload = raw_payload.decode(enc) + except UnicodeDecodeError: + # If the message is not formatted ascii then get_payload with + # decode=True will convert to raw-unicode-escape. if the encoding + # that the message specifies doesn't work try this. It might be + # better to handle the base64 and quoted-printable oursevles + # instead of having to clean up like this. + raw_payload = raw_payload.decode('raw-unicode-escape') + if ctype == 'text/plain': - raw_payload = string_decode(raw_payload, enc) body_parts.append(string_sanitize(raw_payload)) else: # get mime handler @@ -363,9 +383,10 @@ def extract_body(mail, types=None, field_key='copiousoutput'): nametemplate = entry.get('nametemplate', '%s') prefix, suffix = parse_mailcap_nametemplate(nametemplate) with tempfile.NamedTemporaryFile( - delete=False, prefix=prefix, suffix=suffix) \ + delete=False, prefix=prefix, + suffix=suffix) \ as tmpfile: - tmpfile.write(raw_payload) + tmpfile.write(raw_payload.encode(enc)) tempfile_name = tmpfile.name else: stdin = raw_payload @@ -403,22 +424,13 @@ def decode_header(header, normalize=False): :type header: str :param normalize: replace trailing spaces after newlines :type normalize: bool - :rtype: unicode + :rtype: str """ - - # If the value isn't ascii as RFC2822 prescribes, - # we just return the unicode bytestring as is - value = string_decode(header) # convert to unicode - try: - value = value.encode('ascii') - except UnicodeEncodeError: - return value - # some mailers send out incorrectly escaped headers # and double quote the escaped realname part again. remove those # RFC: 2047 regex = r'"(=\?.+?\?.+?\?[^ ?]+\?=)"' - value = re.sub(regex, r'\1', value) + value = re.sub(regex, r'\1', header) logging.debug("unquoted header: |%s|", value) # otherwise we interpret RFC2822 encoding escape sequences @@ -427,7 +439,7 @@ def decode_header(header, normalize=False): for v, enc in valuelist: v = string_decode(v, enc) decoded_list.append(string_sanitize(v)) - value = u' '.join(decoded_list) + value = ''.join(decoded_list) if normalize: value = re.sub(r'\n\s+', r' ', value) return value diff --git a/alot/helper.py b/alot/helper.py index 5fab4819..ba41986e 100644 --- a/alot/helper.py +++ b/alot/helper.py @@ -10,7 +10,7 @@ from datetime import timedelta from datetime import datetime from collections import deque from io import BytesIO -from cStringIO import StringIO +from io import StringIO import logging import mimetypes import os @@ -40,9 +40,6 @@ def split_commandline(s, comments=False, posix=True): s = s.replace('\\', '\\\\') s = s.replace('\'', '\\\'') s = s.replace('\"', '\\\"') - # encode s to utf-8 for shlex - if isinstance(s, unicode): - s = s.encode('utf-8') lex = shlex.shlex(s, posix=posix) lex.whitespace_split = True lex.whitespace = ';' @@ -57,8 +54,7 @@ def split_commandstring(cmdstring): and the like. This simply calls shlex.split but works also with unicode bytestrings. """ - if isinstance(cmdstring, unicode): - cmdstring = cmdstring.encode('utf-8', errors='ignore') + assert isinstance(cmdstring, str) return shlex.split(cmdstring) @@ -118,10 +114,10 @@ def string_decode(string, enc='ascii'): if enc is None: enc = 'ascii' try: - string = unicode(string, enc, errors='replace') + string = str(string, enc, errors='replace') except LookupError: # malformed enc string string = string.decode('ascii', errors='replace') - except TypeError: # already unicode + except TypeError: # already str pass return string @@ -280,8 +276,11 @@ def call_cmd(cmdlist, stdin=None): :param stdin: string to pipe to the process :type stdin: str :return: triple of stdout, stderr, return value of the shell command - :rtype: str, str, int + :rtype: str, str, intd """ + termenc = urwid.util.detected_encoding + if stdin: + stdin = stdin.encode(termenc) try: proc = subprocess.Popen( cmdlist, @@ -296,8 +295,8 @@ def call_cmd(cmdlist, stdin=None): out, err = proc.communicate(stdin) ret = proc.returncode - out = string_decode(out, urwid.util.detected_encoding) - err = string_decode(err, urwid.util.detected_encoding) + out = string_decode(out, termenc) + err = string_decode(err, termenc) return out, err, ret @@ -312,6 +311,8 @@ def call_cmd_async(cmdlist, stdin=None, env=None): return value of the shell command :rtype: `twisted.internet.defer.Deferred` """ + termenc = urwid.util.detected_encoding + cmdlist = [s.encode(termenc) for s in cmdlist] class _EverythingGetter(ProcessProtocol): def __init__(self, deferred): @@ -322,7 +323,6 @@ def call_cmd_async(cmdlist, stdin=None, env=None): self.errReceived = self.errBuf.write def processEnded(self, status): - termenc = urwid.util.detected_encoding out = string_decode(self.outBuf.getvalue(), termenc) err = string_decode(self.errBuf.getvalue(), termenc) if status.value.exitCode == 0: @@ -343,7 +343,7 @@ def call_cmd_async(cmdlist, stdin=None, env=None): args=cmdlist) if stdin: logging.debug('writing to stdin') - proc.write(stdin) + proc.write(stdin.encode(termenc)) proc.closeStdin() return d @@ -416,6 +416,17 @@ def guess_encoding(blob): raise Exception('Unknown magic API') +def try_decode(blob): + """Guess the encoding of blob and try to decode it into a str. + + :param bytes blob: The bytes to decode + :returns: the decoded blob + :rtype: str + """ + assert isinstance(blob, bytes), 'cannot decode a str or non-bytes object' + return blob.decode(guess_encoding(blob)) + + def libmagic_version_at_least(version): """ checks if the libmagic library installed is more recent than a given @@ -627,6 +638,32 @@ def email_as_string(mail): return as_string +def email_as_bytes(mail): + string = email_as_string(mail) + charset = mail.get_charset() + if charset: + charset = str(charset) + else: + charsets = set(mail.get_charsets()) + if None in charsets: + # None is equal to US-ASCII + charsets.discard(None) + charsets.add('ascii') + + if len(charsets) == 1: + charset = list(charsets)[0] + elif 'ascii' in charsets: + # If we get here and the assert triggers it means that different + # parts of the email are encoded differently. I don't think we're + # likely to see that, but it's possible + assert {'utf-8', 'ascii', 'us-ascii'}.issuperset(charsets), charsets + charset = 'utf-8' # It's a strict super-set + else: + charset = 'utf-8' + + return string.encode(charset) + + def get_xdg_env(env_name, fallback): """ Used for XDG_* env variables to return fallback if unset *or* empty """ env = os.environ.get(env_name) diff --git a/alot/settings/manager.py b/alot/settings/manager.py index 24d5c989..195f0ab0 100644 --- a/alot/settings/manager.py +++ b/alot/settings/manager.py @@ -38,9 +38,9 @@ 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 + assert alot_rc is None or (isinstance(alot_rc, str) and os.path.exists(alot_rc)) - assert notmuch_rc is None or (isinstance(notmuch_rc, basestring) and + assert notmuch_rc is None or (isinstance(notmuch_rc, str) and os.path.exists(notmuch_rc)) self.hooks = None self._mailcaps = mailcap.getcaps() @@ -176,12 +176,12 @@ class SettingsManager(object): value = section[key] - if isinstance(value, (str, unicode)): + if isinstance(value, str): section[key] = expand_environment_and_home(value) elif isinstance(value, (list, tuple)): new = list() for item in value: - if isinstance(item, (str, unicode)): + if isinstance(item, str): new.append(expand_environment_and_home(item)) else: new.append(item) @@ -395,7 +395,7 @@ class SettingsManager(object): def get_mapped_input_keysequences(self, mode='global', prefix=u''): # get all bindings in this mode globalmaps, modemaps = self.get_keybindings(mode) - candidates = globalmaps.keys() + modemaps.keys() + candidates = list(globalmaps.keys()) + list(modemaps.keys()) if prefix is not None: prefixes = prefix + ' ' cand = [c for c in candidates if c.startswith(prefixes)] @@ -433,7 +433,7 @@ class SettingsManager(object): if value and value != '': globalmaps[key] = value # get rid of empty commands left in mode bindings - for k, v in modemaps.items(): + for k, v in list(modemaps.items()): if not v: del modemaps[k] @@ -504,7 +504,7 @@ class SettingsManager(object): def get_addresses(self): """returns addresses of known accounts including all their aliases""" - return self._accountmap.keys() + return list(self._accountmap.keys()) def get_addressbooks(self, order=None, append_remaining=True): """returns list of all defined :class:`AddressBook` objects""" @@ -529,7 +529,7 @@ class SettingsManager(object): def represent_datetime(self, d): """ - turns a given datetime obj into a unicode string representation. + turns a given datetime obj into a string representation. This will: 1) look if a fixed 'timestamp_format' is given in the config @@ -317,7 +317,7 @@ class UI(object): def cerror(e): logging.error(e) - self.notify('completion error: %s' % e.message, + self.notify('completion error: %s' % str(e), priority='error') self.update() @@ -329,7 +329,7 @@ class UI(object): edit_text=text, history=history, on_error=cerror) - for _ in xrange(tab): # hit some tabs + for _ in range(tab): # hit some tabs editpart.keypress((0,), 'tab') # build promptwidget @@ -529,9 +529,8 @@ class UI(object): :rtype: :class:`twisted.defer.Deferred` """ choices = choices or {'y': 'yes', 'n': 'no'} - choices_to_return = choices_to_return or {} - assert select is None or select in choices.itervalues() - assert cancel is None or cancel in choices.itervalues() + assert select is None or select in choices.values() + assert cancel is None or cancel in choices.values() assert msg_position in ['left', 'above'] d = defer.Deferred() # create return deferred diff --git a/alot/utils/argparse.py b/alot/utils/argparse.py index ff19030c..9822882d 100644 --- a/alot/utils/argparse.py +++ b/alot/utils/argparse.py @@ -52,7 +52,7 @@ def _path_factory(check): @functools.wraps(check) def validator(paths): - if isinstance(paths, basestring): + if isinstance(paths, str): check(paths) elif isinstance(paths, collections.Sequence): for path in paths: diff --git a/alot/utils/configobj.py b/alot/utils/configobj.py index fa9de2ce..78201690 100644 --- a/alot/utils/configobj.py +++ b/alot/utils/configobj.py @@ -5,7 +5,7 @@ from __future__ import absolute_import import mailbox import re -from urlparse import urlparse +from urllib.parse import urlparse from validate import VdtTypeError from validate import is_list diff --git a/alot/widgets/globals.py b/alot/widgets/globals.py index e4253e15..67ff984b 100644 --- a/alot/widgets/globals.py +++ b/alot/widgets/globals.py @@ -54,7 +54,7 @@ class ChoiceWidget(urwid.Text): self.separator = separator items = [] - for k, v in choices.iteritems(): + for k, v in choices.items(): if v == select and select is not None: items += ['[', k, ']:', v] else: @@ -130,7 +130,7 @@ class CompleteEdit(urwid.Edit): self.historypos = None self.focus_in_clist = 0 - if not isinstance(edit_text, unicode): + if not isinstance(edit_text, str): edit_text = string_decode(edit_text) self.start_completion_pos = len(edit_text) self.completions = None diff --git a/docs/source/api/conf.py b/docs/source/api/conf.py index 101fce21..54b31e07 100644 --- a/docs/source/api/conf.py +++ b/docs/source/api/conf.py @@ -250,7 +250,7 @@ man_pages = [ autodoc_member_order = 'bysource' autoclass_content = 'both' intersphinx_mapping = { - 'python': ('http://docs.python.org/3.2', None), + 'python': ('http://docs.python.org/3.5', None), 'notmuch': ('http://packages.python.org/notmuch', None), 'urwid': ('http://urwid.readthedocs.org/en/latest', None), } diff --git a/docs/source/configuration/accounts_table b/docs/source/configuration/accounts_table index 2215df51..db727898 100644 --- a/docs/source/configuration/accounts_table +++ b/docs/source/configuration/accounts_table @@ -13,24 +13,6 @@ :type: string -.. _realname: - -.. describe:: realname - - used to format the (proposed) From-header in outgoing mails - - :type: string - -.. _aliases: - -.. describe:: aliases - - used to clear your addresses/ match account when formatting replies - - :type: string list - :default: , - - .. _alias-regexp: .. describe:: alias_regexp @@ -41,28 +23,29 @@ :default: None -.. _sendmail-command: +.. _aliases: -.. describe:: sendmail_command +.. describe:: aliases - sendmail command. This is the shell command used to send out mails via the sendmail protocol + used to clear your addresses/ match account when formatting replies - :type: string - :default: "sendmail -t" + :type: string list + :default: , -.. _sent-box: +.. _case-sensitive-username: -.. describe:: sent_box +.. describe:: case_sensitive_username - where to store outgoing mails, e.g. `maildir:///home/you/mail/Sent`. - You can use mbox, maildir, mh, babyl and mmdf in the protocol part of the URL. + Whether the server treats the address as case-senstive or + case-insensitve (True for the former, False for the latter) - .. note:: If you want to add outgoing mails automatically to the notmuch index - you must use maildir in a path within your notmuch database path. + .. 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. - :type: mail_container - :default: None + :type: boolean + :default: False .. _draft-box: @@ -80,34 +63,65 @@ :default: None -.. _sent-tags: +.. _draft-tags: -.. describe:: sent_tags +.. describe:: draft_tags - list of tags to automatically add to outgoing messages + list of tags to automatically add to draft messages :type: string list - :default: sent + :default: draft -.. _draft-tags: +.. _encrypt-by-default: -.. describe:: draft_tags +.. describe:: encrypt_by_default - list of tags to automatically add to draft messages + Alot will try to GPG encrypt outgoing messages by default when this + is set to `all` or `trusted`. If set to `all` the message will be + encrypted for all recipients for who a key is available in the key + ring. If set to `trusted` it will be encrypted to all + recipients if a trusted key is available for all recipients (one + where the user id for the key is signed with a trusted signature). - :type: string list - :default: draft + .. note:: If the message will not be encrypted by default you can + still use the :ref:`toggleencrypt + <cmd.envelope.toggleencrypt>`, :ref:`encrypt + <cmd.envelope.encrypt>` and :ref:`unencrypt + <cmd.envelope.unencrypt>` commands to encrypt it. + .. deprecated:: 0.4 + The values `True` and `False` are interpreted as `all` and + `none` respectively. `0`, `1`, `true`, `True`, `false`, + `False`, `yes`, `Yes`, `no`, `No`, will be removed before + 1.0, please move to `all`, `none`, or `trusted`. + :type: option, one of ['all', 'none', 'trusted', 'True', 'False', 'true', 'false', 'Yes', 'No', 'yes', 'no', '1', '0'] + :default: none -.. _replied-tags: -.. describe:: replied_tags +.. _encrypt-to-self: - list of tags to automatically add to replied messages +.. describe:: encrypt_to_self - :type: string list - :default: replied + If this is true when encrypting a message it will also be encrypted + with the key defined for this account. + + .. warning:: + + Before 0.6 this was controlled via gpg.conf. + + :type: boolean + :default: True + + +.. _gpg-key: + +.. describe:: gpg_key + + The GPG key ID you want to use with this account. + + :type: string + :default: None .. _passed-tags: @@ -120,111 +134,97 @@ :default: passed -.. _signature: +.. _realname: -.. describe:: signature +.. describe:: realname - path to signature file that gets attached to all outgoing mails from this account, optionally - renamed to :ref:`signature_filename <signature-filename>`. + used to format the (proposed) From-header in outgoing mails :type: string - :default: None - -.. _signature-as-attachment: +.. _replied-tags: -.. describe:: signature_as_attachment +.. describe:: replied_tags - attach signature file if set to True, append its content (mimetype text) - to the body text if set to False. + list of tags to automatically add to replied messages - :type: boolean - :default: False + :type: string list + :default: replied -.. _signature-filename: +.. _sendmail-command: -.. describe:: signature_filename +.. describe:: sendmail_command - signature file's name as it appears in outgoing mails if - :ref:`signature_as_attachment <signature-as-attachment>` is set to True + sendmail command. This is the shell command used to send out mails via the sendmail protocol :type: string - :default: None - - -.. _sign-by-default: + :default: "sendmail -t" -.. describe:: sign_by_default - Outgoing messages will be GPG signed by default if this is set to True. +.. _sent-box: - :type: boolean - :default: False +.. describe:: sent_box + where to store outgoing mails, e.g. `maildir:///home/you/mail/Sent`. + You can use mbox, maildir, mh, babyl and mmdf in the protocol part of the URL. -.. _encrypt-by-default: + .. note:: If you want to add outgoing mails automatically to the notmuch index + you must use maildir in a path within your notmuch database path. -.. describe:: encrypt_by_default + :type: mail_container + :default: None - Alot will try to GPG encrypt outgoing messages by default when this - is set to `all` or `trusted`. If set to `all` the message will be - encrypted for all recipients for who a key is available in the key - ring. If set to `trusted` it will be encrypted to all - recipients if a trusted key is available for all recipients (one - where the user id for the key is signed with a trusted signature). - .. note:: If the message will not be encrypted by default you can - still use the :ref:`toggleencrypt - <cmd.envelope.toggleencrypt>`, :ref:`encrypt - <cmd.envelope.encrypt>` and :ref:`unencrypt - <cmd.envelope.unencrypt>` commands to encrypt it. - .. deprecated:: 0.4 - The values `True` and `False` are interpreted as `all` and - `none` respectively. `0`, `1`, `true`, `True`, `false`, - `False`, `yes`, `Yes`, `no`, `No`, will be removed before - 1.0, please move to `all`, `none`, or `trusted`. +.. _sent-tags: - :type: option, one of ['all', 'none', 'trusted', 'True', 'False', 'true', 'false', 'Yes', 'No', 'yes', 'no', '1', '0'] - :default: none +.. describe:: sent_tags + list of tags to automatically add to outgoing messages -.. _encrypt-to-self: + :type: string list + :default: sent -.. describe:: encrypt_to_self - If this is true when encrypting a message it will also be encrypted - with the key defined for this account. +.. _sign-by-default: - .. warning:: +.. describe:: sign_by_default - Before 0.6 this was controlled via gpg.conf. + Outgoing messages will be GPG signed by default if this is set to True. :type: boolean - :default: True + :default: False -.. _gpg-key: +.. _signature: -.. describe:: gpg_key +.. describe:: signature - The GPG key ID you want to use with this account. + path to signature file that gets attached to all outgoing mails from this account, optionally + renamed to :ref:`signature_filename <signature-filename>`. :type: string :default: None -.. _case-sensitive-username: - -.. describe:: case_sensitive_username +.. _signature-as-attachment: - Whether the server treats the address as case-senstive or - case-insensitve (True for the former, False for the latter) +.. describe:: signature_as_attachment - .. 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. + attach signature file if set to True, append its content (mimetype text) + to the body text if set to False. :type: boolean :default: False + +.. _signature-filename: + +.. describe:: signature_filename + + signature file's name as it appears in outgoing mails if + :ref:`signature_as_attachment <signature-as-attachment>` is set to True + + :type: string + :default: None + diff --git a/docs/source/faq.rst b/docs/source/faq.rst index 05847347..9ad30a8b 100644 --- a/docs/source/faq.rst +++ b/docs/source/faq.rst @@ -67,7 +67,9 @@ FAQ .. _faq_7: -7. Why doesn't alot run on python3? +7. I thought alot ran on Python 2? - We're on it. Check out the `py3k milestone <https://github.com/pazz/alot/issues?q=is%3Aopen+is%3Aissue+milestone%3A%22full+py3k+compatibility%22>`_ + It used to. When we made the transition to Python 3 we didn't maintain + Python 2 support. If you still need Python 2 support the 0.7 release is your + best bet. diff --git a/docs/source/generate_commands.py b/docs/source/generate_commands.py index c3db693f..4ead8c11 100755 --- a/docs/source/generate_commands.py +++ b/docs/source/generate_commands.py @@ -100,7 +100,7 @@ def get_mode_docs(): if __name__ == "__main__": modes = [] - for mode, modecommands in COMMANDS.items(): + for mode, modecommands in sorted(COMMANDS.items()): modefilename = mode+'.rst' modefile = open(os.path.join(HERE, 'usage', 'modes', modefilename), 'w') @@ -115,7 +115,7 @@ if __name__ == "__main__": header = 'Global Commands' modefile.write('%s\n%s\n' % (header, '-' * len(header))) modefile.write('The following commands are available globally\n\n') - for cmdstring, struct in modecommands.items(): + for cmdstring, struct in sorted(modecommands.items()): cls, parser, forced_args = struct labelline = '.. _cmd.%s.%s:\n\n' % (mode, cmdstring.replace('_', '-')) diff --git a/docs/source/generate_configs.py b/docs/source/generate_configs.py index a5427792..88726060 100755 --- a/docs/source/generate_configs.py +++ b/docs/source/generate_configs.py @@ -22,15 +22,13 @@ NOTE = """ """ -def rewrite_entries(config, path, specpath, sec=None, sort=False): +def rewrite_entries(config, path, specpath, sec=None): file = open(path, 'w') file.write(NOTE % specpath) if sec is None: sec = config - if sort: - sec.scalars.sort() - for entry in sec.scalars: + for entry in sorted(sec.scalars): v = Validator() etype, eargs, ekwargs, default = v._parse_check(sec[entry]) if default is not None: @@ -72,7 +70,7 @@ if __name__ == "__main__": alotrc_table_file = os.path.join(HERE, 'configuration', 'alotrc_table') rewrite_entries(config.configspec, alotrc_table_file, - 'defaults/alot.rc.spec', sort=True) + 'defaults/alot.rc.spec') rewrite_entries(config, os.path.join(HERE, 'configuration', 'accounts_table'), diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 8e30de1a..2d3fe44c 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -4,14 +4,14 @@ Installation .. rubric:: dependencies Alot depends on recent versions of notmuch and urwid. Note that due to restrictions -on argparse and subprocess, you need to run *`3.0` > python ≥ `2.7`* (see :ref:`faq <faq_7>`). +on argparse and subprocess, you need to run *`python ≥ `3.5`* (see :ref:`faq <faq_7>`). A full list of dependencies is below: * `libmagic and python bindings <http://darwinsys.com/file/>`_, ≥ `5.04` * `configobj <http://www.voidspace.org.uk/python/configobj.html>`_, ≥ `4.7.0` * `twisted <http://twistedmatrix.com/trac/>`_, ≥ `10.2.0`: * `libnotmuch <http://notmuchmail.org/>`_ and it's python bindings, ≥ `0.13` -* `urwid <http://excess.org/urwid/>`_ toolkit, ≥ `1.1.0` +* `urwid <http://excess.org/urwid/>`_ toolkit, ≥ `1.3.0` * `urwidtrees <https://github.com/pazz/urwidtrees>`_, ≥ `1.0` * `gpg <http://www.gnupg.org/related_software/gpgme>`_ and it's python bindings, ≥ `1.9.0` diff --git a/docs/source/usage/modes/envelope.rst b/docs/source/usage/modes/envelope.rst index 8644a6c1..ee857cc4 100644 --- a/docs/source/usage/modes/envelope.rst +++ b/docs/source/usage/modes/envelope.rst @@ -5,26 +5,25 @@ Commands in `envelope` mode --------------------------- The following commands are available in envelope mode -.. _cmd.envelope.unencrypt: - -.. describe:: unencrypt +.. _cmd.envelope.attach: - remove request to encrypt message before sending +.. describe:: attach + attach files to the mail -.. _cmd.envelope.set: + argument + file(s) to attach (accepts wildcads) -.. describe:: set - set header value +.. _cmd.envelope.edit: - positional arguments - 0: header to refine - 1: value +.. describe:: edit + edit mail optional arguments - :---append: keep previous values. + :---spawn: spawn editor in new terminal. + :---refocus: refocus envelope after editing (Defaults to: 'True'). .. _cmd.envelope.encrypt: @@ -38,32 +37,15 @@ The following commands are available in envelope mode optional arguments :---trusted: only add trusted keys. -.. _cmd.envelope.togglesign: +.. _cmd.envelope.refine: -.. describe:: togglesign +.. describe:: refine - toggle sign status + prompt to change the value of a header argument - which key id to use - - -.. _cmd.envelope.toggleheaders: - -.. describe:: toggleheaders - - toggle display of all headers - - -.. _cmd.envelope.edit: - -.. describe:: edit - - edit mail + header to refine - optional arguments - :---spawn: spawn editor in new terminal. - :---refocus: refocus envelope after editing (Defaults to: 'True'). .. _cmd.envelope.retag: @@ -75,14 +57,21 @@ The following commands are available in envelope mode comma separated list of tags -.. _cmd.envelope.tag: +.. _cmd.envelope.rmencrypt: -.. describe:: tag +.. describe:: rmencrypt - add tags to message + do not encrypt to given recipient key argument - comma separated list of tags + keyid of the key to encrypt with + + +.. _cmd.envelope.save: + +.. describe:: save + + save draft .. _cmd.envelope.send: @@ -92,6 +81,20 @@ The following commands are available in envelope mode send mail +.. _cmd.envelope.set: + +.. describe:: set + + set header value + + positional arguments + 0: header to refine + 1: value + + + optional arguments + :---append: keep previous values. + .. _cmd.envelope.sign: .. describe:: sign @@ -102,99 +105,96 @@ The following commands are available in envelope mode which key id to use -.. _cmd.envelope.untag: +.. _cmd.envelope.tag: -.. describe:: untag +.. describe:: tag - remove tags from message + add tags to message argument comma separated list of tags -.. _cmd.envelope.attach: +.. _cmd.envelope.toggleencrypt: -.. describe:: attach +.. describe:: toggleencrypt - attach files to the mail + toggle if message should be encrypted before sendout argument - file(s) to attach (accepts wildcads) - + keyid of the key to encrypt with -.. _cmd.envelope.unattach: + optional arguments + :---trusted: only add trusted keys. -.. describe:: unattach +.. _cmd.envelope.toggleheaders: - remove attachments from current envelope +.. describe:: toggleheaders - argument - which attached file to remove + toggle display of all headers -.. _cmd.envelope.rmencrypt: +.. _cmd.envelope.togglesign: -.. describe:: rmencrypt +.. describe:: togglesign - do not encrypt to given recipient key + toggle sign status argument - keyid of the key to encrypt with + which key id to use -.. _cmd.envelope.refine: +.. _cmd.envelope.toggletags: -.. describe:: refine +.. describe:: toggletags - prompt to change the value of a header + flip presence of tags on message argument - header to refine + comma separated list of tags -.. _cmd.envelope.toggleencrypt: +.. _cmd.envelope.unattach: -.. describe:: toggleencrypt +.. describe:: unattach - toggle if message should be encrypted before sendout + remove attachments from current envelope argument - keyid of the key to encrypt with + which attached file to remove - optional arguments - :---trusted: only add trusted keys. -.. _cmd.envelope.save: +.. _cmd.envelope.unencrypt: -.. describe:: save +.. describe:: unencrypt - save draft + remove request to encrypt message before sending -.. _cmd.envelope.unsign: +.. _cmd.envelope.unset: -.. describe:: unsign +.. describe:: unset - mark mail not to be signed before sending + remove header field + argument + header to refine -.. _cmd.envelope.toggletags: -.. describe:: toggletags +.. _cmd.envelope.unsign: - flip presence of tags on message +.. describe:: unsign - argument - comma separated list of tags + mark mail not to be signed before sending -.. _cmd.envelope.unset: +.. _cmd.envelope.untag: -.. describe:: unset +.. describe:: untag - remove header field + remove tags from message argument - header to refine + comma separated list of tags diff --git a/docs/source/usage/modes/global.rst b/docs/source/usage/modes/global.rst index dcbe4040..6e79c67a 100644 --- a/docs/source/usage/modes/global.rst +++ b/docs/source/usage/modes/global.rst @@ -15,52 +15,18 @@ The following commands are available globally :---redraw: redraw current buffer after command has finished. :---force: never ask for confirmation. -.. _cmd.global.bprevious: - -.. describe:: bprevious - - focus previous buffer - - -.. _cmd.global.search: - -.. describe:: search - - open a new search buffer. Search obeys the notmuch - :ref:`search.exclude_tags <search.exclude_tags>` setting. - - argument - search string - - optional arguments - :---sort: sort order. Valid choices are: \`oldest_first\`,\`newest_first\`,\`message_id\`,\`unsorted\`. - -.. _cmd.global.repeat: - -.. describe:: repeat - - Repeats the command executed last time - - -.. _cmd.global.prompt: - -.. describe:: prompt - - prompts for commandline and interprets it upon select +.. _cmd.global.bnext: - argument - initial content +.. describe:: bnext + focus next buffer -.. _cmd.global.help: -.. describe:: help +.. _cmd.global.bprevious: - display help for a command. Use 'bindings' to display all keybings - interpreted in current mode.' +.. describe:: bprevious - argument - command or 'bindings' + focus previous buffer .. _cmd.global.buffer: @@ -73,49 +39,21 @@ The following commands are available globally buffer index to focus -.. _cmd.global.move: - -.. describe:: move +.. _cmd.global.bufferlist: - move focus in current buffer +.. describe:: bufferlist - argument - up, down, [half]page up, [half]page down, first, last + open a list of active buffers -.. _cmd.global.shellescape: +.. _cmd.global.call: -.. describe:: shellescape +.. describe:: call - run external command + Executes python code argument - command line to execute - - optional arguments - :---spawn: run in terminal window. - :---thread: run in separate thread. - :---refocus: refocus current buffer after command has finished. - -.. _cmd.global.refresh: - -.. describe:: refresh - - refresh the current buffer - - -.. _cmd.global.reload: - -.. describe:: reload - - Reload all configuration files - - -.. _cmd.global.pyshell: - -.. describe:: pyshell - - open an interactive python shell for introspection + python command string to call .. _cmd.global.compose: @@ -158,29 +96,91 @@ The following commands are available globally flush write operations or retry until committed -.. _cmd.global.bufferlist: +.. _cmd.global.help: -.. describe:: bufferlist +.. describe:: help - open a list of active buffers + display help for a command. Use 'bindings' to display all keybings + interpreted in current mode.' + + argument + command or 'bindings' -.. _cmd.global.call: +.. _cmd.global.move: -.. describe:: call +.. describe:: move - Executes python code + move focus in current buffer argument - python command string to call + up, down, [half]page up, [half]page down, first, last -.. _cmd.global.bnext: +.. _cmd.global.prompt: -.. describe:: bnext +.. describe:: prompt - focus next buffer + prompts for commandline and interprets it upon select + + argument + initial content + + +.. _cmd.global.pyshell: +.. describe:: pyshell + + open an interactive python shell for introspection + + +.. _cmd.global.refresh: + +.. describe:: refresh + + refresh the current buffer + + +.. _cmd.global.reload: + +.. describe:: reload + + Reload all configuration files + + +.. _cmd.global.repeat: + +.. describe:: repeat + + Repeats the command executed last time + + +.. _cmd.global.search: + +.. describe:: search + + open a new search buffer. Search obeys the notmuch + :ref:`search.exclude_tags <search.exclude_tags>` setting. + + argument + search string + + optional arguments + :---sort: sort order. Valid choices are: \`oldest_first\`,\`newest_first\`,\`message_id\`,\`unsorted\`. + +.. _cmd.global.shellescape: + +.. describe:: shellescape + + run external command + + argument + command line to execute + + optional arguments + :---spawn: run in terminal window. + :---thread: run in separate thread. + :---refocus: refocus current buffer after command has finished. .. _cmd.global.taglist: diff --git a/docs/source/usage/modes/search.rst b/docs/source/usage/modes/search.rst index 93a59eff..c95d1a01 100644 --- a/docs/source/usage/modes/search.rst +++ b/docs/source/usage/modes/search.rst @@ -5,37 +5,33 @@ Commands in `search` mode ------------------------- The following commands are available in search mode -.. _cmd.search.sort: +.. _cmd.search.move: -.. describe:: sort +.. describe:: move - set sort order + move focus in search buffer argument - sort order. valid choices are: \`oldest_first\`,\`newest_first\`,\`message_id\`,\`unsorted\`. + last -.. _cmd.search.untag: +.. _cmd.search.refine: -.. describe:: untag +.. describe:: refine - remove tags from all messages in the thread that match the query + refine query argument - comma separated list of tags + search string optional arguments - :---no-flush: postpone a writeout to the index (Defaults to: 'True'). - :---all: retag all messages in search result. - -.. _cmd.search.move: + :---sort: sort order. Valid choices are: \`oldest_first\`,\`newest_first\`,\`message_id\`,\`unsorted\`. -.. describe:: move +.. _cmd.search.refineprompt: - move focus in search buffer +.. describe:: refineprompt - argument - last + prompt to change this buffers querystring .. _cmd.search.retag: @@ -51,44 +47,42 @@ The following commands are available in search mode :---no-flush: postpone a writeout to the index (Defaults to: 'True'). :---all: retag all messages in search result. -.. _cmd.search.refineprompt: - -.. describe:: refineprompt +.. _cmd.search.retagprompt: - prompt to change this buffers querystring +.. describe:: retagprompt + prompt to retag selected threads' tags -.. _cmd.search.tag: -.. describe:: tag +.. _cmd.search.select: - add tags to all messages in the thread that match the current query +.. describe:: select - argument - comma separated list of tags + open thread in a new buffer - optional arguments - :---no-flush: postpone a writeout to the index (Defaults to: 'True'). - :---all: retag all messages in search result. -.. _cmd.search.refine: +.. _cmd.search.sort: -.. describe:: refine +.. describe:: sort - refine query + set sort order argument - search string + sort order. valid choices are: \`oldest_first\`,\`newest_first\`,\`message_id\`,\`unsorted\`. - optional arguments - :---sort: sort order. Valid choices are: \`oldest_first\`,\`newest_first\`,\`message_id\`,\`unsorted\`. -.. _cmd.search.retagprompt: +.. _cmd.search.tag: -.. describe:: retagprompt +.. describe:: tag - prompt to retag selected threads' tags + add tags to all messages in the thread that match the current query + + argument + comma separated list of tags + optional arguments + :---no-flush: postpone a writeout to the index (Defaults to: 'True'). + :---all: retag all messages in search result. .. _cmd.search.toggletags: @@ -102,10 +96,16 @@ The following commands are available in search mode optional arguments :---no-flush: postpone a writeout to the index (Defaults to: 'True'). -.. _cmd.search.select: +.. _cmd.search.untag: -.. describe:: select +.. describe:: untag - open thread in a new buffer + remove tags from all messages in the thread that match the query + + argument + comma separated list of tags + optional arguments + :---no-flush: postpone a writeout to the index (Defaults to: 'True'). + :---all: retag all messages in search result. diff --git a/docs/source/usage/modes/thread.rst b/docs/source/usage/modes/thread.rst index 29a7850a..447e543a 100644 --- a/docs/source/usage/modes/thread.rst +++ b/docs/source/usage/modes/thread.rst @@ -5,24 +5,12 @@ Commands in `thread` mode ------------------------- The following commands are available in thread mode -.. _cmd.thread.pipeto: - -.. describe:: pipeto +.. _cmd.thread.bounce: - pipe message(s) to stdin of a shellcommand +.. describe:: bounce - argument - shellcommand to pipe to + directly re-send selected message - optional arguments - :---all: pass all messages. - :---format: output format. Valid choices are: \`raw\`,\`decoded\`,\`id\`,\`filepath\` (Defaults to: 'raw'). - :---separately: call command once for each message. - :---background: don't stop the interface. - :---add_tags: add 'Tags' header to the message. - :---shell: let the shell interpret the command. - :---notify_stdout: display cmd's stdout as notification. - :---field_key: mailcap field key for decoding (Defaults to: 'copiousoutput'). .. _cmd.thread.editnew: @@ -33,15 +21,25 @@ The following commands are available in thread mode optional arguments :---spawn: open editor in new window. -.. _cmd.thread.move: +.. _cmd.thread.fold: -.. describe:: move +.. describe:: fold - move focus in current buffer + fold message(s) argument - up, down, [half]page up, [half]page down, first, last, parent, first reply, last reply, next sibling, previous sibling, next, previous, next unfolded, previous unfolded, next NOTMUCH_QUERY, previous NOTMUCH_QUERY + query used to filter messages to affect + +.. _cmd.thread.forward: + +.. describe:: forward + + forward message + + optional arguments + :---attach: attach original mail. + :---spawn: open editor in new window. .. _cmd.thread.indent: @@ -53,28 +51,34 @@ The following commands are available in thread mode None -.. _cmd.thread.toggleheaders: +.. _cmd.thread.move: -.. describe:: toggleheaders +.. describe:: move - display all headers + move focus in current buffer argument - query used to filter messages to affect + up, down, [half]page up, [half]page down, first, last, parent, first reply, last reply, next sibling, previous sibling, next, previous, next unfolded, previous unfolded, next NOTMUCH_QUERY, previous NOTMUCH_QUERY -.. _cmd.thread.retag: +.. _cmd.thread.pipeto: -.. describe:: retag +.. describe:: pipeto - set message(s) tags. + pipe message(s) to stdin of a shellcommand argument - comma separated list of tags + shellcommand to pipe to optional arguments - :---all: tag all messages in thread. - :---no-flush: postpone a writeout to the index (Defaults to: 'True'). + :---all: pass all messages. + :---format: output format. Valid choices are: \`raw\`,\`decoded\`,\`id\`,\`filepath\` (Defaults to: 'raw'). + :---separately: call command once for each message. + :---background: don't stop the interface. + :---add_tags: add 'Tags' header to the message. + :---shell: let the shell interpret the command. + :---notify_stdout: display cmd's stdout as notification. + :---field_key: mailcap field key for decoding (Defaults to: 'copiousoutput'). .. _cmd.thread.print: @@ -88,28 +92,31 @@ The following commands are available in thread mode :---separately: call print command once for each message. :---add_tags: add 'Tags' header to the message. -.. _cmd.thread.bounce: - -.. describe:: bounce +.. _cmd.thread.remove: - directly re-send selected message +.. describe:: remove + remove message(s) from the index -.. _cmd.thread.togglesource: + optional arguments + :---all: remove whole thread. -.. describe:: togglesource +.. _cmd.thread.reply: - display message source +.. describe:: reply - argument - query used to filter messages to affect + reply to message + optional arguments + :---all: reply to all. + :---list: reply to list. + :---spawn: open editor in new window. -.. _cmd.thread.untag: +.. _cmd.thread.retag: -.. describe:: untag +.. describe:: retag - remove tags from message(s) + set message(s) tags. argument comma separated list of tags @@ -118,14 +125,25 @@ The following commands are available in thread mode :---all: tag all messages in thread. :---no-flush: postpone a writeout to the index (Defaults to: 'True'). -.. _cmd.thread.fold: +.. _cmd.thread.save: -.. describe:: fold +.. describe:: save - fold message(s) + save attachment(s) argument - query used to filter messages to affect + path to save to + + optional arguments + :---all: save all attachments. + +.. _cmd.thread.select: + +.. describe:: select + + select focussed element. The fired action depends on the focus: + - if message summary, this toggles visibility of the message, + - if attachment line, this opens the attachment .. _cmd.thread.tag: @@ -141,63 +159,54 @@ The following commands are available in thread mode :---all: tag all messages in thread. :---no-flush: postpone a writeout to the index (Defaults to: 'True'). -.. _cmd.thread.remove: +.. _cmd.thread.toggleheaders: -.. describe:: remove +.. describe:: toggleheaders - remove message(s) from the index + display all headers - optional arguments - :---all: remove whole thread. + argument + query used to filter messages to affect -.. _cmd.thread.unfold: -.. describe:: unfold +.. _cmd.thread.togglesource: - unfold message(s) +.. describe:: togglesource + + display message source argument query used to filter messages to affect -.. _cmd.thread.forward: - -.. describe:: forward - - forward message - - optional arguments - :---attach: attach original mail. - :---spawn: open editor in new window. +.. _cmd.thread.toggletags: -.. _cmd.thread.reply: +.. describe:: toggletags -.. describe:: reply + flip presence of tags on message(s) - reply to message + argument + comma separated list of tags optional arguments - :---all: reply to all. - :---list: reply to list. - :---spawn: open editor in new window. + :---all: tag all messages in thread. + :---no-flush: postpone a writeout to the index (Defaults to: 'True'). -.. _cmd.thread.save: +.. _cmd.thread.unfold: -.. describe:: save +.. describe:: unfold - save attachment(s) + unfold message(s) argument - path to save to + query used to filter messages to affect - optional arguments - :---all: save all attachments. -.. _cmd.thread.toggletags: +.. _cmd.thread.untag: -.. describe:: toggletags +.. describe:: untag - flip presence of tags on message(s) + remove tags from message(s) argument comma separated list of tags @@ -206,12 +215,3 @@ The following commands are available in thread mode :---all: tag all messages in thread. :---no-flush: postpone a writeout to the index (Defaults to: 'True'). -.. _cmd.thread.select: - -.. describe:: select - - select focussed element. The fired action depends on the focus: - - if message summary, this toggles visibility of the message, - - if attachment line, this opens the attachment - - @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 from setuptools import setup, find_packages import alot @@ -19,8 +19,9 @@ setup( 'Intended Audience :: End Users/Desktop', 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 'Operating System :: POSIX', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 2 :: Only', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3 :: Only', 'Topic :: Communications :: Email :: Email Clients (MUA)', 'Topic :: Database :: Front-Ends', ], @@ -54,5 +55,5 @@ setup( ], provides=['alot'], test_suite="tests", - python_requires=">=2.7", + python_requires=">=3.5", ) diff --git a/tests/account_test.py b/tests/account_test.py index cfc51bf8..99df9632 100644 --- a/tests/account_test.py +++ b/tests/account_test.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -from __future__ import absolute_import + import unittest from alot import account @@ -32,138 +32,126 @@ class TestAccount(unittest.TestCase): def test_get_address(self): """Tests address without aliases.""" - acct = _AccountTestClass(address=u"foo@example.com") - self.assertListEqual(acct.get_addresses(), [u'foo@example.com']) + acct = _AccountTestClass(address="foo@example.com") + self.assertListEqual(acct.get_addresses(), ['foo@example.com']) def test_get_address_with_aliases(self): """Tests address with aliases.""" - acct = _AccountTestClass(address=u"foo@example.com", - aliases=[u'bar@example.com']) + acct = _AccountTestClass(address="foo@example.com", + aliases=['bar@example.com']) self.assertListEqual(acct.get_addresses(), - [u'foo@example.com', u'bar@example.com']) + ['foo@example.com', 'bar@example.com']) def test_deprecated_encrypt_by_default(self): """Tests that depreacted values are still accepted.""" - for each in [u'true', u'yes', u'1']: - acct = _AccountTestClass(address=u'foo@example.com', + for each in ['true', 'yes', '1']: + acct = _AccountTestClass(address='foo@example.com', encrypt_by_default=each) - self.assertEqual(acct.encrypt_by_default, u'all') - for each in [u'false', u'no', u'0']: - acct = _AccountTestClass(address=u'foo@example.com', + self.assertEqual(acct.encrypt_by_default, 'all') + for each in ['false', 'no', '0']: + acct = _AccountTestClass(address='foo@example.com', encrypt_by_default=each) - self.assertEqual(acct.encrypt_by_default, u'none') + self.assertEqual(acct.encrypt_by_default, 'none') class TestAddress(unittest.TestCase): """Tests for the Address class.""" - def test_constructor_bytes(self): - with self.assertRaises(AssertionError): - account.Address(b'username', b'domainname') - - def test_from_string_bytes(self): - with self.assertRaises(AssertionError): - account.Address.from_string(b'user@example.com') - def test_from_string(self): - addr = account.Address.from_string(u'user@example.com') - self.assertEqual(addr.username, u'user') - self.assertEqual(addr.domainname, u'example.com') - - def test_unicode(self): - addr = account.Address(u'ušer', u'example.com') - self.assertEqual(unicode(addr), u'ušer@example.com') + addr = account.Address.from_string('user@example.com') + self.assertEqual(addr.username, 'user') + self.assertEqual(addr.domainname, 'example.com') def test_str(self): - addr = account.Address(u'ušer', u'example.com') - self.assertEqual(str(addr), u'ušer@example.com'.encode('utf-8')) + addr = account.Address('ušer', 'example.com') + self.assertEqual(str(addr), 'ušer@example.com') def test_eq_unicode(self): - addr = account.Address(u'ušer', u'example.com') - self.assertEqual(addr, u'ušer@example.com') + addr = account.Address('ušer', 'example.com') + self.assertEqual(addr, 'ušer@example.com') def test_eq_address(self): - addr = account.Address(u'ušer', u'example.com') - addr2 = account.Address(u'ušer', u'example.com') + addr = account.Address('ušer', 'example.com') + addr2 = account.Address('ušer', 'example.com') self.assertEqual(addr, addr2) def test_ne_unicode(self): - addr = account.Address(u'ušer', u'example.com') - self.assertNotEqual(addr, u'user@example.com') + addr = account.Address('ušer', 'example.com') + self.assertNotEqual(addr, 'user@example.com') def test_ne_address(self): - addr = account.Address(u'ušer', u'example.com') - addr2 = account.Address(u'user', u'example.com') + addr = account.Address('ušer', 'example.com') + addr2 = account.Address('user', 'example.com') self.assertNotEqual(addr, addr2) def test_eq_unicode_case(self): - addr = account.Address(u'UŠer', u'example.com') - self.assertEqual(addr, u'ušer@example.com') + addr = account.Address('UŠer', 'example.com') + self.assertEqual(addr, 'ušer@example.com') def test_ne_unicode_case(self): - addr = account.Address(u'ušer', u'example.com') - self.assertEqual(addr, u'uŠer@example.com') + addr = account.Address('ušer', 'example.com') + self.assertEqual(addr, 'uŠer@example.com') def test_ne_address_case(self): - addr = account.Address(u'ušer', u'example.com') - addr2 = account.Address(u'uŠer', u'example.com') + addr = account.Address('ušer', 'example.com') + addr2 = account.Address('uŠer', 'example.com') self.assertEqual(addr, addr2) def test_eq_address_case(self): - addr = account.Address(u'UŠer', u'example.com') - addr2 = account.Address(u'ušer', u'example.com') + addr = account.Address('UŠer', 'example.com') + addr2 = account.Address('ušer', 'example.com') self.assertEqual(addr, addr2) def test_eq_unicode_case_sensitive(self): - addr = account.Address(u'UŠer', u'example.com', case_sensitive=True) - self.assertNotEqual(addr, u'ušer@example.com') + addr = account.Address('UŠer', 'example.com', case_sensitive=True) + self.assertNotEqual(addr, 'ušer@example.com') def test_eq_address_case_sensitive(self): - addr = account.Address(u'UŠer', u'example.com', case_sensitive=True) - addr2 = account.Address(u'ušer', u'example.com') + addr = account.Address('UŠer', 'example.com', case_sensitive=True) + addr2 = account.Address('ušer', 'example.com') self.assertNotEqual(addr, addr2) def test_eq_str(self): - addr = account.Address(u'user', u'example.com', case_sensitive=True) + addr = account.Address('user', 'example.com', case_sensitive=True) with self.assertRaises(TypeError): addr == 1 # pylint: disable=pointless-statement def test_ne_str(self): - addr = account.Address(u'user', u'example.com', case_sensitive=True) + addr = account.Address('user', 'example.com', case_sensitive=True) with self.assertRaises(TypeError): addr != 1 # pylint: disable=pointless-statement def test_repr(self): - addr = account.Address(u'user', u'example.com', case_sensitive=True) + addr = account.Address('user', 'example.com', case_sensitive=True) self.assertEqual( repr(addr), - "Address(u'user', u'example.com', case_sensitive=True)") + "Address('user', 'example.com', case_sensitive=True)") def test_domain_name_ne(self): - addr = account.Address(u'user', u'example.com') - self.assertNotEqual(addr, u'user@example.org') + addr = account.Address('user', 'example.com') + self.assertNotEqual(addr, 'user@example.org') def test_domain_name_eq_case(self): - addr = account.Address(u'user', u'example.com') - self.assertEqual(addr, u'user@Example.com') + addr = account.Address('user', 'example.com') + self.assertEqual(addr, 'user@Example.com') def test_domain_name_ne_unicode(self): - addr = account.Address(u'user', u'éxample.com') - self.assertNotEqual(addr, u'user@example.com') + addr = account.Address('user', 'éxample.com') + self.assertNotEqual(addr, 'user@example.com') def test_domain_name_eq_unicode(self): - addr = account.Address(u'user', u'éxample.com') - self.assertEqual(addr, u'user@Éxample.com') + addr = account.Address('user', 'éxample.com') + self.assertEqual(addr, 'user@Éxample.com') def test_domain_name_eq_case_sensitive(self): - addr = account.Address(u'user', u'example.com', case_sensitive=True) - self.assertEqual(addr, u'user@Example.com') + addr = account.Address('user', 'example.com', case_sensitive=True) + self.assertEqual(addr, 'user@Example.com') def test_domain_name_eq_unicode_sensitive(self): - addr = account.Address(u'user', u'éxample.com', case_sensitive=True) - self.assertEqual(addr, u'user@Éxample.com') + addr = account.Address('user', 'éxample.com', case_sensitive=True) + self.assertEqual(addr, 'user@Éxample.com') def test_cmp_empty(self): - addr = account.Address(u'user', u'éxample.com') - self.assertNotEqual(addr, u'') + addr = account.Address('user', 'éxample.com') + self.assertNotEqual(addr, '') diff --git a/tests/addressbook/abook_test.py b/tests/addressbook/abook_test.py index 32be2cbe..c5686caf 100644 --- a/tests/addressbook/abook_test.py +++ b/tests/addressbook/abook_test.py @@ -29,7 +29,7 @@ class TestAbookAddressBook(unittest.TestCase): name = you email = you@other.domain, you@example.com """ - with tempfile.NamedTemporaryFile(delete=False) as tmp: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as tmp: tmp.write(data) path = tmp.name self.addCleanup(os.unlink, path) diff --git a/tests/commands/envelope_test.py b/tests/commands/envelope_test.py index c48a0d91..ee1c0acc 100644 --- a/tests/commands/envelope_test.py +++ b/tests/commands/envelope_test.py @@ -315,7 +315,7 @@ class TestSignCommand(unittest.TestCase): """) # Allow settings.reload to work by not deleting the file until the end - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(config) self.addCleanup(os.unlink, f.name) diff --git a/tests/commands/utils_tests.py b/tests/commands/utils_tests.py index 5320aadd..487d0d3b 100644 --- a/tests/commands/utils_tests.py +++ b/tests/commands/utils_tests.py @@ -141,8 +141,8 @@ class TestSetEncrypt(unittest.TestCase): envelope['To'] = 'ambig@example.com, test@example.com' yield utils.set_encrypt(ui, envelope) self.assertTrue(envelope.encrypt) - self.assertEqual( - [f.fpr for f in envelope.encrypt_keys.itervalues()], + self.assertCountEqual( + [f.fpr for f in envelope.encrypt_keys.values()], [crypto.get_key(FPR).fpr, crypto.get_key(EXTRA_FPRS[0]).fpr]) @inlineCallbacks @@ -152,8 +152,8 @@ class TestSetEncrypt(unittest.TestCase): envelope['Cc'] = 'ambig@example.com, test@example.com' yield utils.set_encrypt(ui, envelope) self.assertTrue(envelope.encrypt) - self.assertEqual( - [f.fpr for f in envelope.encrypt_keys.itervalues()], + self.assertCountEqual( + [f.fpr for f in envelope.encrypt_keys.values()], [crypto.get_key(FPR).fpr, crypto.get_key(EXTRA_FPRS[0]).fpr]) @inlineCallbacks @@ -163,8 +163,8 @@ class TestSetEncrypt(unittest.TestCase): envelope['Cc'] = 'foo@example.com, test@example.com' yield utils.set_encrypt(ui, envelope) self.assertTrue(envelope.encrypt) - self.assertEqual( - [f.fpr for f in envelope.encrypt_keys.itervalues()], + self.assertCountEqual( + [f.fpr for f in envelope.encrypt_keys.values()], [crypto.get_key(FPR).fpr]) @inlineCallbacks diff --git a/tests/crypto_test.py b/tests/crypto_test.py index d481d64e..25cce8ba 100644 --- a/tests/crypto_test.py +++ b/tests/crypto_test.py @@ -12,6 +12,7 @@ import unittest import gpg import mock +import urwid from alot import crypto from alot.errors import GPGProblem, GPGCode @@ -57,7 +58,8 @@ def tearDownModule(): # Kill any gpg-agent's that have been opened lookfor = 'gpg-agent --homedir {}'.format(os.environ['GNUPGHOME']) - out = subprocess.check_output(['ps', 'xo', 'pid,cmd'], stderr=DEVNULL) + out = subprocess.check_output( + ['ps', 'xo', 'pid,cmd'], stderr=DEVNULL).decode(urwid.util.detected_encoding) for each in out.strip().split('\n'): pid, cmd = each.strip().split(' ', 1) if cmd.startswith(lookfor): @@ -109,7 +111,7 @@ class TestHashAlgorithmHelper(unittest.TestCase): class TestDetachedSignatureFor(unittest.TestCase): def test_valid_signature_generated(self): - to_sign = "this is some text.\nit is more than nothing.\n" + to_sign = b"this is some text.\nit is more than nothing.\n" with gpg.core.Context() as ctx: _, detached = crypto.detached_signature_for(to_sign, [ctx.get_key(FPR)]) @@ -131,7 +133,7 @@ class TestDetachedSignatureFor(unittest.TestCase): class TestVerifyDetached(unittest.TestCase): def test_verify_signature_good(self): - to_sign = "this is some text.\nIt's something\n." + to_sign = b"this is some text.\nIt's something\n." with gpg.core.Context() as ctx: _, detached = crypto.detached_signature_for(to_sign, [ctx.get_key(FPR)]) @@ -141,8 +143,8 @@ class TestVerifyDetached(unittest.TestCase): raise AssertionError def test_verify_signature_bad(self): - to_sign = "this is some text.\nIt's something\n." - similar = "this is some text.\r\n.It's something\r\n." + to_sign = b"this is some text.\nIt's something\n." + similar = b"this is some text.\r\n.It's something\r\n." with gpg.core.Context() as ctx: _, detached = crypto.detached_signature_for(to_sign, [ctx.get_key(FPR)]) @@ -360,7 +362,7 @@ class TestGetKey(unittest.TestCase): class TestEncrypt(unittest.TestCase): def test_encrypt(self): - to_encrypt = "this is a string\nof data." + to_encrypt = b"this is a string\nof data." encrypted = crypto.encrypt(to_encrypt, keys=[crypto.get_key(FPR)]) with tempfile.NamedTemporaryFile(delete=False) as f: @@ -368,15 +370,14 @@ class TestEncrypt(unittest.TestCase): enc_file = f.name self.addCleanup(os.unlink, enc_file) - dec = subprocess.check_output(['gpg', '--decrypt', enc_file], - stderr=DEVNULL) + dec = subprocess.check_output(['gpg', '--decrypt', enc_file], stderr=DEVNULL) self.assertEqual(to_encrypt, dec) class TestDecrypt(unittest.TestCase): def test_decrypt(self): - to_encrypt = "this is a string\nof data." + to_encrypt = b"this is a string\nof data." encrypted = crypto.encrypt(to_encrypt, keys=[crypto.get_key(FPR)]) _, dec = crypto.decrypt_verify(encrypted) self.assertEqual(to_encrypt, dec) diff --git a/tests/db/thread_test.py b/tests/db/thread_test.py index 678a5e59..8e4a5003 100644 --- a/tests/db/thread_test.py +++ b/tests/db/thread_test.py @@ -45,7 +45,7 @@ class TestThreadGetAuthor(unittest.TestCase): m.get_author = mock.Mock(return_value=a) get_messages.append(m) gm = mock.Mock() - gm.iterkeys = mock.Mock(return_value=get_messages) + gm.keys = mock.Mock(return_value=get_messages) cls.__patchers.extend([ mock.patch('alot.db.thread.Thread.get_messages', diff --git a/tests/db/utils_test.py b/tests/db/utils_test.py index 3e7ef9d3..b1187fe3 100644 --- a/tests/db/utils_test.py +++ b/tests/db/utils_test.py @@ -6,6 +6,7 @@ from __future__ import absolute_import import base64 +import codecs import email import email.header import email.mime.application @@ -171,6 +172,7 @@ class TestEncodeHeader(unittest.TestCase): expected = email.header.Header('value') self.assertEqual(actual, expected) + @unittest.expectedFailure def test_unicode_chars_are_encoded(self): actual = utils.encode_header('x-key', u'välüe') expected = email.header.Header('=?utf-8?b?dsOkbMO8ZQ==?=') @@ -243,10 +245,10 @@ class TestDecodeHeader(unittest.TestCase): :rtype: str """ string = unicode_string.encode(encoding) - output = '=?' + encoding + '?Q?' + output = b'=?' + encoding.encode('ascii') + b'?Q?' for byte in string: - output += '=' + byte.encode('hex').upper() - return output + '?=' + output += b'=' + codecs.encode(bytes([byte]), 'hex').upper() + return (output + b'?=').decode('ascii') @staticmethod def _base64(unicode_string, encoding): @@ -260,8 +262,8 @@ class TestDecodeHeader(unittest.TestCase): :rtype: str """ string = unicode_string.encode(encoding) - b64 = base64.encodestring(string).strip() - return '=?' + encoding + '?B?' + b64 + '?=' + b64 = base64.encodebytes(string).strip() + return (b'=?' + encoding.encode('utf-8') + b'?B?' + b64 + b'?=').decode('ascii') def _test(self, teststring, expected): @@ -382,8 +384,7 @@ class TestAddSignatureHeaders(unittest.TestCase): def test_unicode_as_bytes(self): mail = self.FakeMail() key = make_key() - key.uids = [make_uid('andreá@example.com', - uid=u'Andreá'.encode('utf-8'))] + key.uids = [make_uid('andreá@example.com', uid=u'Andreá')] mail = self.check(key, True) self.assertIn((utils.X_SIGNATURE_VALID_HEADER, u'True'), mail.headers) @@ -397,13 +398,6 @@ class TestAddSignatureHeaders(unittest.TestCase): (utils.X_SIGNATURE_MESSAGE_HEADER, u'Invalid: error message'), mail.headers) - def test_error_message_bytes(self): - mail = self.check(mock.Mock(), mock.Mock(), b'error message') - self.assertIn((utils.X_SIGNATURE_VALID_HEADER, u'False'), mail.headers) - self.assertIn( - (utils.X_SIGNATURE_MESSAGE_HEADER, u'Invalid: error message'), - mail.headers) - def test_get_key_fails(self): mail = self.FakeMail() with mock.patch('alot.db.utils.crypto.get_key', @@ -442,13 +436,13 @@ class TestMessageFromFile(TestCaseClassCleanup): """ m = email.message.Message() m.add_header(utils.X_SIGNATURE_VALID_HEADER, 'Bad') - message = utils.message_from_file(io.BytesIO(m.as_string())) + message = utils.message_from_file(io.StringIO(m.as_string())) self.assertIs(message.get(utils.X_SIGNATURE_VALID_HEADER), None) def test_erase_alot_header_message(self): m = email.message.Message() m.add_header(utils.X_SIGNATURE_MESSAGE_HEADER, 'Bad') - message = utils.message_from_file(io.BytesIO(m.as_string())) + message = utils.message_from_file(io.StringIO(m.as_string())) self.assertIs(message.get(utils.X_SIGNATURE_MESSAGE_HEADER), None) def test_plain_mail(self): @@ -456,15 +450,15 @@ class TestMessageFromFile(TestCaseClassCleanup): m['Subject'] = 'test' m['From'] = 'me' m['To'] = 'Nobody' - message = utils.message_from_file(io.BytesIO(m.as_string())) + message = utils.message_from_file(io.StringIO(m.as_string())) self.assertEqual(message.get_payload(), 'This is some text') def _make_signed(self): """Create a signed message that is multipart/signed.""" - text = 'This is some text' + text = b'This is some text' t = email.mime.text.MIMEText(text, 'plain', 'utf-8') _, sig = crypto.detached_signature_for( - helper.email_as_string(t), self.keys) + helper.email_as_bytes(t), self.keys) s = email.mime.application.MIMEApplication( sig, 'pgp-signature', email.encoders.encode_7or8bit) m = email.mime.multipart.MIMEMultipart('signed', None, [t, s]) @@ -475,34 +469,34 @@ class TestMessageFromFile(TestCaseClassCleanup): def test_signed_headers_included(self): """Headers are added to the message.""" m = self._make_signed() - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn(utils.X_SIGNATURE_VALID_HEADER, m) self.assertIn(utils.X_SIGNATURE_MESSAGE_HEADER, m) def test_signed_valid(self): """Test that the signature is valid.""" m = self._make_signed() - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertEqual(m[utils.X_SIGNATURE_VALID_HEADER], 'True') def test_signed_correct_from(self): """Test that the signature is valid.""" m = self._make_signed() - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) # Don't test for valid/invalid since that might change self.assertIn('ambig <ambig@example.com>', m[utils.X_SIGNATURE_MESSAGE_HEADER]) def test_signed_wrong_mimetype_second_payload(self): m = self._make_signed() m.get_payload(1).set_type('text/plain') - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn('expected Content-Type: ', m[utils.X_SIGNATURE_MESSAGE_HEADER]) def test_signed_wrong_micalg(self): m = self._make_signed() m.set_param('micalg', 'foo') - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn('expected micalg=pgp-...', m[utils.X_SIGNATURE_MESSAGE_HEADER]) @@ -524,7 +518,7 @@ class TestMessageFromFile(TestCaseClassCleanup): """ m = self._make_signed() m.set_param('micalg', 'PGP-SHA1') - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn('expected micalg=pgp-', m[utils.X_SIGNATURE_MESSAGE_HEADER]) @@ -539,7 +533,7 @@ class TestMessageFromFile(TestCaseClassCleanup): """ m = self._make_signed() m.attach(email.mime.text.MIMEText('foo')) - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn('expected exactly two messages, got 3', m[utils.X_SIGNATURE_MESSAGE_HEADER]) @@ -551,9 +545,9 @@ class TestMessageFromFile(TestCaseClassCleanup): if signed: t = self._make_signed() else: - text = 'This is some text' + text = b'This is some text' t = email.mime.text.MIMEText(text, 'plain', 'utf-8') - enc = crypto.encrypt(t.as_string(), self.keys) + enc = crypto.encrypt(helper.email_as_bytes(t), self.keys) e = email.mime.application.MIMEApplication( enc, 'octet-stream', email.encoders.encode_7or8bit) @@ -570,12 +564,12 @@ class TestMessageFromFile(TestCaseClassCleanup): # of the mail, rather than replacing the whole encrypted payload with # it's unencrypted equivalent m = self._make_encrypted() - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertEqual(len(m.get_payload()), 3) def test_encrypted_unsigned_is_decrypted(self): m = self._make_encrypted() - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) # Check using m.walk, since we're not checking for ordering, just # existence. self.assertIn('This is some text', [n.get_payload() for n in m.walk()]) @@ -585,13 +579,13 @@ class TestMessageFromFile(TestCaseClassCleanup): that there is a signature. """ m = self._make_encrypted() - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertNotIn(utils.X_SIGNATURE_VALID_HEADER, m) self.assertNotIn(utils.X_SIGNATURE_MESSAGE_HEADER, m) def test_encrypted_signed_is_decrypted(self): m = self._make_encrypted(True) - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn('This is some text', [n.get_payload() for n in m.walk()]) def test_encrypted_signed_headers(self): @@ -599,7 +593,7 @@ class TestMessageFromFile(TestCaseClassCleanup): there is a signature. """ m = self._make_encrypted(True) - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn(utils.X_SIGNATURE_MESSAGE_HEADER, m) self.assertIn('ambig <ambig@example.com>', m[utils.X_SIGNATURE_MESSAGE_HEADER]) @@ -608,14 +602,14 @@ class TestMessageFromFile(TestCaseClassCleanup): def test_encrypted_wrong_mimetype_first_payload(self): m = self._make_encrypted() m.get_payload(0).set_type('text/plain') - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn('Malformed OpenPGP message:', m.get_payload(2).get_payload()) def test_encrypted_wrong_mimetype_second_payload(self): m = self._make_encrypted() m.get_payload(1).set_type('text/plain') - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn('Malformed OpenPGP message:', m.get_payload(2).get_payload()) @@ -625,7 +619,7 @@ class TestMessageFromFile(TestCaseClassCleanup): """ s = self._make_signed() m = email.mime.multipart.MIMEMultipart('mixed', None, [s]) - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn(utils.X_SIGNATURE_VALID_HEADER, m) self.assertIn(utils.X_SIGNATURE_MESSAGE_HEADER, m) @@ -635,7 +629,7 @@ class TestMessageFromFile(TestCaseClassCleanup): """ s = self._make_encrypted() m = email.mime.multipart.MIMEMultipart('mixed', None, [s]) - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn('This is some text', [n.get_payload() for n in m.walk()]) self.assertNotIn(utils.X_SIGNATURE_VALID_HEADER, m) self.assertNotIn(utils.X_SIGNATURE_MESSAGE_HEADER, m) @@ -647,7 +641,7 @@ class TestMessageFromFile(TestCaseClassCleanup): """ s = self._make_encrypted(True) m = email.mime.multipart.MIMEMultipart('mixed', None, [s]) - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn('This is some text', [n.get_payload() for n in m.walk()]) self.assertIn(utils.X_SIGNATURE_VALID_HEADER, m) self.assertIn(utils.X_SIGNATURE_MESSAGE_HEADER, m) diff --git a/tests/helper_test.py b/tests/helper_test.py index 4d003921..1d20caa8 100644 --- a/tests/helper_test.py +++ b/tests/helper_test.py @@ -143,13 +143,13 @@ class TestSplitCommandstring(unittest.TestCase): self.assertListEqual(actual, expected) def test_bytes(self): - base = b'echo "foo bar"' - expected = [b'echo', b'foo bar'] + base = 'echo "foo bar"' + expected = ['echo', 'foo bar'] self._test(base, expected) def test_unicode(self): - base = u'echo "foo €"' - expected = [b'echo', u'foo €'.encode('utf-8')] + base = 'echo "foo €"' + expected = ['echo', 'foo €'] self._test(base, expected) @@ -221,20 +221,20 @@ class TestPrettyDatetime(unittest.TestCase): p.stop() def test_just_now(self): - for i in (self.random.randint(0, 60) for _ in xrange(5)): + for i in (self.random.randint(0, 60) for _ in range(5)): test = self.now - datetime.timedelta(seconds=i) actual = helper.pretty_datetime(test) self.assertEquals(actual, u'just now') def test_x_minutes_ago(self): - for i in (self.random.randint(60, 3600) for _ in xrange(10)): + for i in (self.random.randint(60, 3600) for _ in range(10)): test = self.now - datetime.timedelta(seconds=i) actual = helper.pretty_datetime(test) self.assertEquals( actual, u'{}min ago'.format((self.now - test).seconds // 60)) def test_x_hours_ago(self): - for i in (self.random.randint(3600, 3600 * 6) for _ in xrange(10)): + for i in (self.random.randint(3600, 3600 * 6) for _ in range(10)): test = self.now - datetime.timedelta(seconds=i) actual = helper.pretty_datetime(test) self.assertEquals( @@ -251,7 +251,7 @@ class TestPrettyDatetime(unittest.TestCase): expected = test.strftime('%I:%M%p').lower() else: expected = test.strftime('%H:%M') - expected = expected.decode('utf-8') + expected = expected return expected def test_future_seconds(self): diff --git a/tests/settings/manager_test.py b/tests/settings/manager_test.py index 42f944db..d04e244d 100644 --- a/tests/settings/manager_test.py +++ b/tests/settings/manager_test.py @@ -22,7 +22,7 @@ from .. import utilities class TestSettingsManager(unittest.TestCase): def test_reading_synchronize_flags_from_notmuch_config(self): - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(textwrap.dedent("""\ [maildir] synchronize_flags = true @@ -34,7 +34,7 @@ class TestSettingsManager(unittest.TestCase): self.assertTrue(actual) def test_parsing_notmuch_config_with_non_bool_synchronize_flag_fails(self): - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(textwrap.dedent("""\ [maildir] synchronize_flags = not bool @@ -45,7 +45,7 @@ class TestSettingsManager(unittest.TestCase): SettingsManager(notmuch_rc=f.name) def test_reload_notmuch_config(self): - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(textwrap.dedent("""\ [maildir] synchronize_flags = false @@ -53,7 +53,7 @@ class TestSettingsManager(unittest.TestCase): self.addCleanup(os.unlink, f.name) manager = SettingsManager(notmuch_rc=f.name) - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(textwrap.dedent("""\ [maildir] synchronize_flags = true @@ -72,7 +72,7 @@ class TestSettingsManager(unittest.TestCase): defaults not being loaded if there isn't an alot config files, and thus calls like `get_theming_attribute` fail with strange exceptions. """ - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(textwrap.dedent("""\ [maildir] synchronize_flags = true @@ -86,7 +86,7 @@ class TestSettingsManager(unittest.TestCase): # todo: For py3, don't mock the logger, use assertLogs unknown_settings = ['templates_dir', 'unknown_section', 'unknown_1', 'unknown_2'] - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(textwrap.dedent("""\ {x[0]} = /templates/dir [{x[1]}] @@ -110,7 +110,7 @@ class TestSettingsManager(unittest.TestCase): unknown_settings, mock_logger.info.call_args_list)) def test_read_notmuch_config_doesnt_exist(self): - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(textwrap.dedent("""\ [accounts] [[default]] @@ -125,7 +125,7 @@ class TestSettingsManager(unittest.TestCase): def test_dont_choke_on_regex_special_chars_in_tagstring(self): tag = 'to**do' - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(textwrap.dedent("""\ [tags] [[{tag}]] @@ -171,7 +171,7 @@ class TestSettingsManagerExpandEnvironment(unittest.TestCase): user_setting = '/path/to/template/dir' with mock.patch.dict('os.environ', {self.xdg_name: self.xdg_custom}): - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write('template_dir = {}'.format(user_setting)) self.addCleanup(os.unlink, f.name) @@ -188,7 +188,7 @@ class TestSettingsManagerExpandEnvironment(unittest.TestCase): with mock.patch.dict('os.environ', {self.xdg_name: self.xdg_custom, 'foo': foo_env}): foo_in_config = 'foo_set_in_config' - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(textwrap.dedent("""\ foo = {} template_dir = ${{XDG_CONFIG_HOME}}/$foo/%(foo)s/${{foo}} @@ -221,7 +221,7 @@ class TestSettingsManagerGetAccountByAddress(utilities.TestCaseClassCleanup): """) # Allow settings.reload to work by not deleting the file until the end - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(config) cls.addClassCleanup(os.unlink, f.name) @@ -244,7 +244,7 @@ class TestSettingsManagerGetAccountByAddress(utilities.TestCaseClassCleanup): def test_doesnt_exist_no_default(self): with tempfile.NamedTemporaryFile() as f: - f.write('') + f.write(b'') settings = SettingsManager(alot_rc=f.name) with self.assertRaises(NoMatchingAccount): settings.get_account_by_address('that_guy@example.com', |