From 9c502002d68e75b200d3373e9552a68453626a8e Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Fri, 17 Jun 2022 19:39:18 -0700 Subject: [PATCH] Remove support for Python 2 Python 2.7 (the last Python 2 release) was End-Of-Life on 1-Jan-2020. * Add requirement to `setup.py` for python_requires=">=3.4.0" * Remove all usage of the `six` library * Remove declared support for Python 2 * Update tox.ini to remove `py27` environment * Update Github workflows to no longer test against Python 2.7 * Remove `from __future__ import print_function` * Remove `from __future__ unicode_literals` * Update tests to import `unittest` and `unittest.mock` directly Closes: #401 --- .github/workflows/main.yml | 2 - README.rst | 2 +- doc/src/concepts.rst | 7 ---- doc/src/index.rst | 2 +- imapclient/__init__.py | 2 - imapclient/config.py | 18 ++++----- imapclient/datetime_util.py | 2 - imapclient/fixed_offset.py | 2 - imapclient/imap_utf7.py | 14 +++---- imapclient/imapclient.py | 61 ++++++++++++------------------- imapclient/interact.py | 7 +--- imapclient/response_lexer.py | 10 ++--- imapclient/response_parser.py | 10 +---- imapclient/response_types.py | 6 +-- imapclient/testable_imapclient.py | 2 - imapclient/util.py | 10 ++--- imapclient/version.py | 2 - interact.py | 2 - livetest.py | 30 ++++++--------- setup.py | 19 +++------- tests/imapclient_test.py | 2 +- tests/test_auth.py | 2 - tests/test_datetime_util.py | 5 +-- tests/test_enable.py | 4 +- tests/test_fixed_offset.py | 5 +-- tests/test_folder_status.py | 3 +- tests/test_imap_utf7.py | 13 +++---- tests/test_imapclient.py | 33 ++++++----------- tests/test_init.py | 4 +- tests/test_response_lexer.py | 5 +-- tests/test_response_parser.py | 6 +-- tests/test_search.py | 4 +- tests/test_sort.py | 3 +- tests/test_starttls.py | 3 +- tests/test_store.py | 7 +--- tests/test_thread.py | 3 +- tests/test_util_functions.py | 3 +- tests/test_version.py | 3 +- tests/util.py | 41 --------------------- tox.ini | 9 +---- 40 files changed, 106 insertions(+), 262 deletions(-) delete mode 100644 tests/util.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 08820fb9..eb16eb18 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,14 +15,12 @@ jobs: fail-fast: false matrix: python-version: - - "2.7" - "3.4" - "3.5" - "3.6" - "3.7" - "3.8" - "3.9" - - "pypy2" - "pypy3" steps: - uses: actions/checkout@v2 diff --git a/README.rst b/README.rst index c64c4a87..692b8935 100644 --- a/README.rst +++ b/README.rst @@ -5,7 +5,7 @@ library. ========================= ======================================== Current version 2.2.0 -Supported Python versions 2.7, 3.4 - 3.9 +Supported Python versions 3.4 - 3.9 License New BSD Project home https://github.com/mjs/imapclient/ PyPI https://pypi.python.org/pypi/IMAPClient diff --git a/doc/src/concepts.rst b/doc/src/concepts.rst index 66f878b5..c69297ea 100644 --- a/doc/src/concepts.rst +++ b/doc/src/concepts.rst @@ -102,13 +102,6 @@ When constructing a custom context it is usually best to start with the default context, created by the ``ssl`` module, and modify it to suit your needs. -.. warning:: - - Users of Python 2.7.0 - 2.7.8 can use TLS but cannot configure - the settings via an ``ssl.SSLContext``. These Python versions are - also not capable of proper certification verification. It is highly - encouraged to upgrade to a more recent version of Python. - The following example shows how to to disable certification verification and certificate host name checks if required. diff --git a/doc/src/index.rst b/doc/src/index.rst index f0847b76..11d9c7cf 100644 --- a/doc/src/index.rst +++ b/doc/src/index.rst @@ -30,7 +30,7 @@ explains IMAP in detail. Other RFCs also apply to various extensions to the base protocol. These are referred to in the documentation below where relevant. -Python versions 2.7 and 3.4 through 3.9 are officially supported. +Python versions 3.4 through 3.9 are officially supported. Getting Started --------------- diff --git a/imapclient/__init__.py b/imapclient/__init__.py index 459f85a3..3c40228d 100644 --- a/imapclient/__init__.py +++ b/imapclient/__init__.py @@ -5,8 +5,6 @@ # version_info provides the version number in programmer friendly way. # The 4th part will be either alpha, beta or final. -from __future__ import unicode_literals - from .imapclient import * from .response_parser import * from .tls import * diff --git a/imapclient/config.py b/imapclient/config.py index 78a02043..0f5ae495 100644 --- a/imapclient/config.py +++ b/imapclient/config.py @@ -2,16 +2,13 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals - import json from os import environ, path import ssl -from six import iteritems -from six.moves.configparser import SafeConfigParser, NoOptionError -from six.moves.urllib.request import urlopen -from six.moves.urllib.parse import urlencode +import configparser +import urllib.parse +import urllib.request import imapclient @@ -45,7 +42,7 @@ def parse_config_file(filename): Used by livetest.py and interact.py """ - parser = SafeConfigParser(get_string_config_defaults()) + parser = configparser.SafeConfigParser(get_string_config_defaults()) with open(filename, "r") as fh: parser.readfp(fh) @@ -62,7 +59,7 @@ def parse_config_file(filename): def get_string_config_defaults(): out = {} - for k, v in iteritems(get_config_defaults()): + for k, v in get_config_defaults().items(): if v is True: v = "true" elif v is False: @@ -80,7 +77,7 @@ def _read_config_section(parser, section): def get_allowing_none(name, typefunc): try: v = parser.get(section, name) - except NoOptionError: + except configparser.NoOptionError: return None if not v: return None @@ -133,7 +130,8 @@ def refresh_oauth2_token(hostname, client_id, client_secret, refresh_token): refresh_token=refresh_token.encode("ascii"), grant_type=b"refresh_token", ) - response = urlopen(url, urlencode(post).encode("ascii")).read() + response = urllib.request.urlopen( + url, urllib.parse.urlencode(post).encode("ascii")).read() return json.loads(response.decode("ascii"))["access_token"] diff --git a/imapclient/datetime_util.py b/imapclient/datetime_util.py index 426a24d7..a10ea0de 100644 --- a/imapclient/datetime_util.py +++ b/imapclient/datetime_util.py @@ -2,8 +2,6 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals - import re from datetime import datetime from email.utils import parsedate_tz diff --git a/imapclient/fixed_offset.py b/imapclient/fixed_offset.py index ac073093..dd6d9b17 100644 --- a/imapclient/fixed_offset.py +++ b/imapclient/fixed_offset.py @@ -2,8 +2,6 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals - import time from datetime import tzinfo, timedelta diff --git a/imapclient/imap_utf7.py b/imapclient/imap_utf7.py index 1e1780fc..e516195b 100644 --- a/imapclient/imap_utf7.py +++ b/imapclient/imap_utf7.py @@ -6,10 +6,8 @@ # base64 encoding context), which is & in this modified UTF-7 convention, # since + is considered as mainly used in mailbox names. # Other variations and examples can be found in the RFC 3501, section 5.1.3. -from __future__ import unicode_literals import binascii -from six import binary_type, text_type, byte2int, iterbytes, unichr def encode(s): @@ -18,7 +16,7 @@ def encode(s): Input is unicode; output is bytes (Python 3) or str (Python 2). If non-unicode input is provided, the input is returned unchanged. """ - if not isinstance(s, text_type): + if not isinstance(s, str): return s res = bytearray() @@ -56,8 +54,8 @@ def consume_b64_buffer(buf): return bytes(res) -AMPERSAND_ORD = byte2int(b"&") -DASH_ORD = byte2int(b"-") +AMPERSAND_ORD = ord("&") +DASH_ORD = ord("-") def decode(s): @@ -67,13 +65,13 @@ def decode(s): unicode. If non-bytes/str input is provided, the input is returned unchanged. """ - if not isinstance(s, binary_type): + if not isinstance(s, bytes): return s res = [] # Store base64 substring that will be decoded once stepping on end shift character b64_buffer = bytearray() - for c in iterbytes(s): + for c in s: # Shift character without anything in buffer -> starts storing base64 substring if c == AMPERSAND_ORD and not b64_buffer: b64_buffer.append(c) @@ -90,7 +88,7 @@ def decode(s): b64_buffer.append(c) # No buffer initialized yet, should be an ASCII printable char else: - res.append(unichr(c)) + res.append(chr(c)) # Decode the remaining buffer if any if b64_buffer: diff --git a/imapclient/imapclient.py b/imapclient/imapclient.py index 029da4d8..e52a6112 100644 --- a/imapclient/imapclient.py +++ b/imapclient/imapclient.py @@ -2,8 +2,6 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals - import functools import imaplib import itertools @@ -17,8 +15,6 @@ from operator import itemgetter from logging import LoggerAdapter, getLogger -from six import moves, iteritems, text_type, integer_types, PY3, binary_type, iterbytes - from . import exceptions from . import imap4 from . import response_lexer @@ -28,8 +24,6 @@ from .response_parser import parse_response, parse_message_list, parse_fetch_response from .util import to_bytes, to_unicode, assert_imap_protocol, chunk -xrange = moves.xrange - try: from select import poll @@ -38,9 +32,6 @@ # Fallback to select() on systems that don't support poll() POLL_SUPPORT = False -if PY3: - long = int # long is just int in python3 - logger = getLogger(__name__) @@ -744,11 +735,11 @@ def _proc_folder_list(self, folder_data): ret = [] parsed = parse_response(folder_data) for flags, delim, name in chunk(parsed, size=3): - if isinstance(name, (int, long)): + if isinstance(name, int): # Some IMAP implementations return integer folder names # with quotes. These get parsed to ints so convert them # back to strings. - name = text_type(name) + name = str(name) elif self.folder_encode: name = decode_utf7(name) @@ -838,7 +829,7 @@ def _process_select_response(self, resp): if key == b"PERMANENTFLAGS": out[key] = tuple(match.group("data").split()) - for key, value in iteritems(untagged): + for key, value in untagged.items(): key = key.upper() if key in (b"OK", b"PERMANENTFLAGS"): continue # already handled above @@ -1190,7 +1181,7 @@ def sort(self, sort_criteria, criteria="ALL", charset="UTF-8"): ] args.extend(_normalise_search_criteria(criteria, charset)) ids = self._raw_command_untagged(b"SORT", args, unpack=True) - return [long(i) for i in ids.split()] + return [int(i) for i in ids.split()] def thread(self, algorithm="REFERENCES", criteria="ALL", charset="UTF-8"): """Return a list of messages threads from the currently @@ -1276,7 +1267,7 @@ def get_gmail_labels(self, messages): response = self.fetch(messages, [b"X-GM-LABELS"]) response = self._filter_fetch_dict(response, b"X-GM-LABELS") return { - msg: utf7_decode_sequence(labels) for msg, labels in iteritems(response) + msg: utf7_decode_sequence(labels) for msg, labels in response.items() } def add_gmail_labels(self, messages, labels, silent=False): @@ -1405,10 +1396,7 @@ def append(self, folder, msg, flags=(), msg_time=None): """ if msg_time: time_val = '"%s"' % datetime_to_INTERNALDATE(msg_time) - if PY3: - time_val = to_unicode(time_val) - else: - time_val = to_bytes(time_val) + time_val = to_unicode(time_val) else: time_val = None return self._command_and_check( @@ -1528,7 +1516,7 @@ def getacl(self, folder): data = self._command_and_check("getacl", self._normalise_folder(folder)) parts = list(response_lexer.TokenSource(data)) parts = parts[1:] # First item is folder name - return [(parts[i], parts[i + 1]) for i in xrange(0, len(parts), 2)] + return [(parts[i], parts[i + 1]) for i in range(0, len(parts), 2)] @require_capability("ACL") def setacl(self, folder, who, what): @@ -1730,8 +1718,7 @@ def _command_and_check(self, command, *args, **kwargs): assert not kwargs, "unexpected keyword args: " + ", ".join(kwargs) if uid and self.use_uid: - if PY3: - command = to_unicode(command) # imaplib must die + command = to_unicode(command) # imaplib must die typ, data = self._imap.uid(command, *args) else: meth = getattr(self._imap, to_unicode(command)) @@ -1749,7 +1736,7 @@ def _gm_label_store(self, cmd, messages, labels, silent): cmd, messages, self._normalise_labels(labels), b"X-GM-LABELS", silent=silent ) return ( - {msg: utf7_decode_sequence(labels) for msg, labels in iteritems(response)} + {msg: utf7_decode_sequence(labels) for msg, labels in response.items()} if response else None ) @@ -1772,17 +1759,17 @@ def _store(self, cmd, messages, flags, fetch_key, silent): return self._filter_fetch_dict(parse_fetch_response(data), fetch_key) def _filter_fetch_dict(self, fetch_dict, key): - return dict((msgid, data[key]) for msgid, data in iteritems(fetch_dict)) + return dict((msgid, data[key]) for msgid, data in fetch_dict.items()) def _normalise_folder(self, folder_name): - if isinstance(folder_name, binary_type): + if isinstance(folder_name, bytes): folder_name = folder_name.decode("ascii") if self.folder_encode: folder_name = encode_utf7(folder_name) return _quote(folder_name) def _normalise_labels(self, labels): - if isinstance(labels, (text_type, binary_type)): + if isinstance(labels, (str, bytes)): labels = (labels,) return [_quote(encode_utf7(l)) for l in labels] @@ -1796,7 +1783,7 @@ def welcome(self): def _quote(arg): - if isinstance(arg, text_type): + if isinstance(arg, str): arg = arg.replace("\\", "\\\\") arg = arg.replace('"', '\\"') q = '"' @@ -1813,7 +1800,7 @@ def _normalise_search_criteria(criteria, charset=None): if not charset: charset = "us-ascii" - if isinstance(criteria, (text_type, binary_type)): + if isinstance(criteria, (str, bytes)): return [to_bytes(criteria, charset)] out = [] @@ -1834,7 +1821,7 @@ def _normalise_search_criteria(criteria, charset=None): def _normalise_sort_criteria(criteria, charset=None): - if isinstance(criteria, (text_type, binary_type)): + if isinstance(criteria, (str, bytes)): criteria = [criteria] return b"(" + b" ".join(to_bytes(item).upper() for item in criteria) + b")" @@ -1845,7 +1832,7 @@ class _literal(bytes): pass -class _quoted(binary_type): +class _quoted(bytes): """ This class holds a quoted bytes value which provides access to the unquoted value via the *original* attribute. @@ -1892,7 +1879,7 @@ def _join_and_paren(items): def _normalise_text_list(items): - if isinstance(items, (text_type, binary_type)): + if isinstance(items, (str, bytes)): items = (items,) return (to_unicode(c) for c in items) @@ -1901,14 +1888,14 @@ def join_message_ids(messages): """Convert a sequence of messages ids or a single integer message id into an id byte string for use with IMAP commands """ - if isinstance(messages, (text_type, binary_type, integer_types)): + if isinstance(messages, (str, bytes, int)): messages = (to_bytes(messages),) return b",".join(_maybe_int_to_bytes(m) for m in messages) def _maybe_int_to_bytes(val): - if isinstance(val, integer_types): - return str(val).encode("us-ascii") if PY3 else str(val) + if isinstance(val, int): + return str(val).encode("us-ascii") return to_bytes(val) @@ -1943,7 +1930,7 @@ def as_triplets(items): def _is8bit(data): - return isinstance(data, _literal) or any(b > 127 for b in iterbytes(data)) + return isinstance(data, _literal) or any(b > 127 for b in data) def _iter_with_last(items): @@ -1964,7 +1951,7 @@ def __init__(self, d): self._d = d def iteritems(self): - for key, value in iteritems(self._d): + for key, value in self._d.items(): yield to_bytes(key), value # For Python 3 compatibility. @@ -1998,7 +1985,7 @@ def pop(self, ink, default=_not_present): def _gen_keys(self, k): yield k - if isinstance(k, binary_type): + if isinstance(k, bytes): yield to_unicode(k) else: yield to_bytes(k) @@ -2037,7 +2024,7 @@ class IMAPlibLoggerAdapter(LoggerAdapter): def process(self, msg, kwargs): # msg is usually unicode but see #367. Convert bytes to # unicode if required. - if isinstance(msg, binary_type): + if isinstance(msg, bytes): msg = msg.decode("ascii", "ignore") for command in ("LOGIN", "AUTHENTICATE"): diff --git a/imapclient/interact.py b/imapclient/interact.py index 33e40005..791a30c2 100644 --- a/imapclient/interact.py +++ b/imapclient/interact.py @@ -4,14 +4,9 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals - - from getpass import getpass from optparse import OptionParser -from six import iteritems - from .config import parse_config_file, create_client_from_config, get_config_defaults @@ -93,7 +88,7 @@ def command_line(): # Scan through options, filling in defaults and prompting when # a compulsory option wasn't provided. compulsory_opts = ("host", "username", "password") - for name, default_value in iteritems(get_config_defaults()): + for name, default_value in get_config_defaults().items(): value = getattr(opts, name, default_value) if name in compulsory_opts and value is None: value = getpass(name + ": ") diff --git a/imapclient/response_lexer.py b/imapclient/response_lexer.py index bb1ffcaa..0f82ddd2 100644 --- a/imapclient/response_lexer.py +++ b/imapclient/response_lexer.py @@ -9,19 +9,15 @@ external callers. """ -from __future__ import unicode_literals - -import six - from .util import assert_imap_protocol __all__ = ["TokenSource"] CTRL_CHARS = frozenset(c for c in range(32)) ALL_CHARS = frozenset(c for c in range(256)) -SPECIALS = frozenset(c for c in six.iterbytes(b' ()%"[')) +SPECIALS = frozenset(c for c in b' ()%"[') NON_SPECIALS = ALL_CHARS - SPECIALS - CTRL_CHARS -WHITESPACE = frozenset(c for c in six.iterbytes(b" \t\r\n")) +WHITESPACE = frozenset(c for c in b" \t\r\n") BACKSLASH = ord("\\") OPEN_SQUARE = ord("[") @@ -146,7 +142,7 @@ def __init__(self, resp_record): self.literal = None def __iter__(self): - return PushableIterator(six.iterbytes(self.src_text)) + return PushableIterator(self.src_text) class PushableIterator(object): diff --git a/imapclient/response_parser.py b/imapclient/response_parser.py index 9fbfeec8..8d48fd9f 100644 --- a/imapclient/response_parser.py +++ b/imapclient/response_parser.py @@ -11,21 +11,15 @@ # TODO more exact error reporting -from __future__ import unicode_literals - import re import sys from collections import defaultdict -import six - from .datetime_util import parse_to_datetime from .response_lexer import TokenSource from .response_types import BodyData, Envelope, Address, SearchIds from .exceptions import ProtocolError -xrange = six.moves.xrange - __all__ = ["parse_response", "parse_message_list"] @@ -60,7 +54,7 @@ def parse_message_list(data): if not data: return SearchIds() - if six.PY3 and isinstance(data, six.binary_type): + if isinstance(data, bytes): data = data.decode("ascii") m = _msg_id_pattern.match(data) @@ -133,7 +127,7 @@ def parse_fetch_response(text, normalise_times=True, uid_is_key=True): # always return the sequence of the message, so it is available # even if we return keyed by UID. msg_data = {b"SEQ": seq} - for i in xrange(0, len(msg_response), 2): + for i in range(0, len(msg_response), 2): word = msg_response[i].upper() value = msg_response[i + 1] diff --git a/imapclient/response_types.py b/imapclient/response_types.py index b634d2aa..f15d7624 100644 --- a/imapclient/response_types.py +++ b/imapclient/response_types.py @@ -2,13 +2,9 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals - from collections import namedtuple from email.utils import formataddr -import six - from .util import to_unicode @@ -120,7 +116,7 @@ def create(cls, response): if isinstance(response[0], tuple): # Multipart, find where the message part tuples stop for i, part in enumerate(response): - if isinstance(part, six.binary_type): + if isinstance(part, bytes): break return cls(([cls.create(part) for part in response[:i]],) + response[i:]) else: diff --git a/imapclient/testable_imapclient.py b/imapclient/testable_imapclient.py index fa546a15..71c79acc 100644 --- a/imapclient/testable_imapclient.py +++ b/imapclient/testable_imapclient.py @@ -2,8 +2,6 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals - from .imapclient import IMAPClient try: diff --git a/imapclient/util.py b/imapclient/util.py index 3773ac83..76e8f9ee 100644 --- a/imapclient/util.py +++ b/imapclient/util.py @@ -2,11 +2,7 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals - -import six import logging -from six import binary_type, text_type from . import exceptions @@ -14,7 +10,7 @@ def to_unicode(s): - if isinstance(s, binary_type): + if isinstance(s, bytes): try: return s.decode("ascii") except UnicodeDecodeError: @@ -28,7 +24,7 @@ def to_unicode(s): def to_bytes(s, charset="ascii"): - if isinstance(s, text_type): + if isinstance(s, str): return s.encode(charset) return s @@ -42,5 +38,5 @@ def assert_imap_protocol(condition, message=None): def chunk(lst, size): - for i in six.moves.range(0, len(lst), size): + for i in range(0, len(lst), size): yield lst[i : i + size] diff --git a/imapclient/version.py b/imapclient/version.py index b7d627d8..2a4a7813 100644 --- a/imapclient/version.py +++ b/imapclient/version.py @@ -2,8 +2,6 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals - version_info = (2, 2, 0, "final") diff --git a/interact.py b/interact.py index 3dc431f7..4544ec5e 100755 --- a/interact.py +++ b/interact.py @@ -4,8 +4,6 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals - from imapclient.interact import main main() diff --git a/livetest.py b/livetest.py index 61f3c876..db78f1d9 100644 --- a/livetest.py +++ b/livetest.py @@ -5,8 +5,6 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import print_function, unicode_literals - import copy import imp import os @@ -19,8 +17,6 @@ from datetime import datetime from email.utils import make_msgid -from six import binary_type, text_type, PY3, iteritems - from imapclient.config import parse_config_file, create_client_from_config from imapclient.exceptions import IMAPClientError from imapclient.fixed_offset import FixedOffset @@ -170,7 +166,7 @@ def clear_folder(self, folder): self.client.expunge() def add_prefix_to_folder(self, folder): - if isinstance(folder, binary_type): + if isinstance(folder, bytes): return ( self.base_folder.encode("ascii") + self.folder_delimiter.encode("ascii") @@ -219,7 +215,7 @@ class TestGeneral(_TestBase): def test_alternates(self): # Check alternate connection/login configurations. - for name, conf in iteritems(self.conf.alternates): + for name, conf in self.conf.alternates.items(): try: client = create_client_from_config(conf) client.logout() @@ -446,13 +442,13 @@ def test_rename_folder(self): for folder in folders: self.client.create_folder(folder) - if isinstance(folder, binary_type): + if isinstance(folder, bytes): new_folder = folder + b"x" else: new_folder = folder + "x" resp = self.client.rename_folder(folder, new_folder) - self.assertIsInstance(resp, binary_type) + self.assertIsInstance(resp, bytes) self.assertTrue(len(resp) > 0) self.assertFalse(self.client.folder_exists(folder)) @@ -510,7 +506,7 @@ def test_idle(self): finally: text, more_responses = self.client.idle_done() self.assertIn((1, b"EXISTS"), responses) - self.assertTrue(isinstance(text, binary_type)) + self.assertTrue(isinstance(text, bytes)) self.assertGreater(len(text), 0) self.assertTrue(isinstance(more_responses, list)) @@ -529,7 +525,7 @@ def test_idle(self): return self.assertIn((2, b"EXISTS"), responses) - self.assertTrue(isinstance(text, binary_type)) + self.assertTrue(isinstance(text, bytes)) self.assertGreater(len(text), 0) def test_noop(self): @@ -537,7 +533,7 @@ def test_noop(self): # Initially there should be no responses text, resps = self.client.noop() - self.assertTrue(isinstance(text, binary_type)) + self.assertTrue(isinstance(text, bytes)) self.assertGreater(len(text), 0) self.assertEqual(resps, []) @@ -549,7 +545,7 @@ def test_noop(self): # Check for this addition in the NOOP data msg, resps = self.client.noop() - self.assertTrue(isinstance(text, binary_type)) + self.assertTrue(isinstance(text, bytes)) self.assertGreater(len(text), 0) self.assertTrue(isinstance(resps, list)) self.assertIn((1, b"EXISTS"), resps) @@ -614,7 +610,7 @@ def check_append(self, in_message, out_message): resp = self.client.append( self.base_folder, in_message, ("abc", "def"), msg_time ) - self.assertIsInstance(resp, binary_type) + self.assertIsInstance(resp, bytes) # Retrieve the just added message and check that all looks well self.assertEqual(self.client.select_folder(self.base_folder)[b"EXISTS"], 1) @@ -1068,7 +1064,7 @@ def test_expunge(self): # Test empty mailbox text, resps = self.client.expunge() - self.assertTrue(isinstance(text, binary_type)) + self.assertTrue(isinstance(text, bytes)) self.assertGreater(len(text), 0) # Some servers return nothing while others (e.g. Exchange) return (0, 'EXISTS') self.assertIn(resps, ([], [(0, b"EXISTS")])) @@ -1078,7 +1074,7 @@ def test_expunge(self): msg, resps = self.client.expunge() - self.assertTrue(isinstance(text, binary_type)) + self.assertTrue(isinstance(text, bytes)) self.assertGreater(len(text), 0) self.assertTrue(isinstance(resps, list)) if not self.is_gmail(): @@ -1138,7 +1134,7 @@ def quiet_logout(client): def maybe_lower(val): - if isinstance(val, (text_type, binary_type)): + if isinstance(val, (str, bytes)): return val.lower() return val @@ -1197,8 +1193,6 @@ def add_test_class(klass, name=None): if name is None: name = klass.__name__ else: - if not PY3: - name = name.encode("ascii") klass.__name__ = name setattr(live_test_mod, name, klass) diff --git a/setup.py b/setup.py index cc0efb6e..5c5bed2d 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # Copyright (c) 2017, Menno Smits # Released subject to the New BSD License @@ -9,17 +9,11 @@ from setuptools import setup -MAJ_MIN_MIC = sys.version_info[:3] -IS_PY3 = MAJ_MIN_MIC >= (3, 0, 0) - # Read version info here = path.dirname(__file__) version_file = path.join(here, "imapclient", "version.py") info = {} -if IS_PY3: - exec(open(version_file).read(), {}, info) -else: - execfile(version_file, {}, info) +exec(open(version_file).read(), {}, info) desc = """\ IMAPClient is an easy-to-use, Pythonic and complete IMAP client library. @@ -33,14 +27,12 @@ * Convenience methods are provided for commonly used functionality. * Exceptions are raised when errors occur. -Python versions 2.7 and 3.4 through 3.9 are officially supported. +Python versions 3.4 through 3.9 are officially supported. IMAPClient includes comprehensive units tests and automated functional tests that can be run against a live IMAP server. """ -main_deps = ["six"] -test_deps = ['mock>=1.3.0; python_version < "3.4"'] doc_deps = ["sphinx"] setup( @@ -57,10 +49,9 @@ download_url="http://menno.io/projects/IMAPClient/IMAPClient-%s.zip" % info["version"], packages=["imapclient"], package_data=dict(imapclient=["examples/*.py"]), - install_requires=main_deps, - tests_require=test_deps, - extras_require={"test": test_deps, "doc": doc_deps}, + extras_require={"doc": doc_deps}, long_description=desc, + python_requires=">=3.4.0", classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", diff --git a/tests/imapclient_test.py b/tests/imapclient_test.py index f15956c6..337c1948 100644 --- a/tests/imapclient_test.py +++ b/tests/imapclient_test.py @@ -1,5 +1,5 @@ from imapclient.testable_imapclient import TestableIMAPClient as IMAPClient -from .util import unittest +import unittest class IMAPClientTest(unittest.TestCase): diff --git a/tests/test_auth.py b/tests/test_auth.py index ccae2df4..5691634e 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -2,8 +2,6 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals - from imapclient import IMAPClient from imapclient.exceptions import LoginError from .imapclient_test import IMAPClientTest diff --git a/tests/test_datetime_util.py b/tests/test_datetime_util.py index 1a082aac..47816346 100644 --- a/tests/test_datetime_util.py +++ b/tests/test_datetime_util.py @@ -2,9 +2,9 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals - from datetime import datetime, date +import unittest +from unittest.mock import patch from imapclient.datetime_util import ( @@ -14,7 +14,6 @@ parse_to_datetime, ) from imapclient.fixed_offset import FixedOffset -from .util import unittest, patch class TestParsing(unittest.TestCase): diff --git a/tests/test_enable.py b/tests/test_enable.py index c1d86d54..1f1c1ca5 100644 --- a/tests/test_enable.py +++ b/tests/test_enable.py @@ -2,13 +2,11 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals - +from unittest.mock import Mock from imapclient import IMAPClient from imapclient.exceptions import IllegalStateError from .imapclient_test import IMAPClientTest -from .util import Mock class TestEnable(IMAPClientTest): diff --git a/tests/test_fixed_offset.py b/tests/test_fixed_offset.py index b8890d5b..7e32d121 100644 --- a/tests/test_fixed_offset.py +++ b/tests/test_fixed_offset.py @@ -2,12 +2,11 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals - from datetime import timedelta +import unittest +from unittest.mock import Mock, patch, DEFAULT from imapclient.fixed_offset import FixedOffset -from .util import unittest, Mock, patch, DEFAULT class TestFixedOffset(unittest.TestCase): diff --git a/tests/test_folder_status.py b/tests/test_folder_status.py index 98f83d02..c57e5c24 100644 --- a/tests/test_folder_status.py +++ b/tests/test_folder_status.py @@ -2,10 +2,9 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals +from unittest.mock import Mock from .imapclient_test import IMAPClientTest -from .util import Mock class TestFolderStatus(IMAPClientTest): diff --git a/tests/test_imap_utf7.py b/tests/test_imap_utf7.py index 295aada1..14ca41dc 100644 --- a/tests/test_imap_utf7.py +++ b/tests/test_imap_utf7.py @@ -2,12 +2,9 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals - -from six import text_type, binary_type, int2byte, unichr +import unittest from imapclient.imap_utf7 import decode, encode -from tests.util import unittest class IMAP4UTF7TestCase(unittest.TestCase): @@ -30,13 +27,13 @@ class IMAP4UTF7TestCase(unittest.TestCase): def test_encode(self): for (input, output) in self.tests: encoded = encode(input) - self.assertIsInstance(encoded, binary_type) + self.assertIsInstance(encoded, bytes) self.assertEqual(encoded, output) def test_decode(self): for (input, output) in self.tests: decoded = decode(output) - self.assertIsInstance(decoded, text_type) + self.assertIsInstance(decoded, str) self.assertEqual(input, decoded) def test_printable_singletons(self): @@ -46,8 +43,8 @@ def test_printable_singletons(self): """ # All printables represent themselves for o in list(range(0x20, 0x26)) + list(range(0x27, 0x7F)): - self.assertEqual(int2byte(o), encode(unichr(o))) - self.assertEqual(unichr(o), decode(int2byte(o))) + self.assertEqual(bytes((o,)), encode(chr(o))) + self.assertEqual(chr(o), decode(bytes((o,)))) self.assertEqual(encode("&"), b"&-") self.assertEqual(encode("&"), b"&-") self.assertEqual(decode(b"&-"), "&") diff --git a/tests/test_imapclient.py b/tests/test_imapclient.py index f3b8e51b..d1d00580 100644 --- a/tests/test_imapclient.py +++ b/tests/test_imapclient.py @@ -2,16 +2,15 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals - +import io import itertools import socket import sys import warnings from datetime import datetime import logging +from unittest.mock import patch, sentinel, Mock -import six from select import POLLIN from imapclient.exceptions import CapabilityError, IMAPClientError, ProtocolError @@ -27,7 +26,6 @@ from imapclient.testable_imapclient import TestableIMAPClient as IMAPClient from .imapclient_test import IMAPClientTest -from .util import patch, sentinel, Mock class TestListFolders(IMAPClientTest): @@ -738,7 +736,7 @@ def test_IMAP_is_patched(self): # prevent basicConfig from being executed for handler in logging.root.handlers[:]: logging.root.removeHandler(handler) - log_stream = six.StringIO() + log_stream = io.StringIO() logging.basicConfig(stream=log_stream, level=logging.DEBUG) self.client._imap._mesg("two") @@ -750,23 +748,16 @@ def test_redacted_password(self): logger_mock.getEffectiveLevel.return_value = logging.DEBUG adapter = IMAPlibLoggerAdapter(logger_mock, dict()) - if six.PY3: - adapter.info("""> b'ICHH1 LOGIN foo@bar.org "secret"'""") - if sys.version_info >= (3, 6, 4): - # LoggerAdapter in Python 3.6.4+ calls logger.log() - logger_mock.log.assert_called_once_with( - logging.INFO, "> b'ICHH1 LOGIN **REDACTED**", extra={} - ) - else: - # LoggerAdapter in Python 3.4 to 3.6 calls logger._log() - logger_mock._log.assert_called_once_with( - logging.INFO, "> b'ICHH1 LOGIN **REDACTED**", (), extra={} - ) + adapter.info("""> b'ICHH1 LOGIN foo@bar.org "secret"'""") + if sys.version_info >= (3, 6, 4): + # LoggerAdapter in Python 3.6.4+ calls logger.log() + logger_mock.log.assert_called_once_with( + logging.INFO, "> b'ICHH1 LOGIN **REDACTED**", extra={} + ) else: - # LoggerAdapter in Python 2.7 calls logger.info() - adapter.info('> ICHH1 LOGIN foo@bar.org "secret"') - logger_mock.info.assert_called_once_with( - "> ICHH1 LOGIN **REDACTED**", extra={} + # LoggerAdapter in Python 3.4 to 3.6 calls logger._log() + logger_mock._log.assert_called_once_with( + logging.INFO, "> b'ICHH1 LOGIN **REDACTED**", (), extra={} ) diff --git a/tests/test_init.py b/tests/test_init.py index 2a87cb17..8ecb5744 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -2,8 +2,10 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses +import unittest +from unittest.mock import patch, sentinel, Mock + from imapclient.imapclient import IMAPClient, SocketTimeout -from .util import unittest, patch, sentinel, Mock class TestInit(unittest.TestCase): diff --git a/tests/test_response_lexer.py b/tests/test_response_lexer.py index b89a0646..4914323f 100644 --- a/tests/test_response_lexer.py +++ b/tests/test_response_lexer.py @@ -2,12 +2,9 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals - -from six import next +import unittest from imapclient.response_lexer import TokenSource -from tests.util import unittest class TestTokenSource(unittest.TestCase): diff --git a/tests/test_response_parser.py b/tests/test_response_parser.py index 3c05f0a9..830f8654 100644 --- a/tests/test_response_parser.py +++ b/tests/test_response_parser.py @@ -6,9 +6,9 @@ Unit tests for the FetchTokeniser and FetchParser classes """ -from __future__ import unicode_literals - +import unittest from datetime import datetime +from unittest.mock import patch from imapclient.datetime_util import datetime_to_native from imapclient.fixed_offset import FixedOffset @@ -19,8 +19,6 @@ ) from imapclient.response_types import Envelope, Address from imapclient.exceptions import ProtocolError -from tests.util import unittest -from .util import patch # TODO: test invalid dates and times diff --git a/tests/test_search.py b/tests/test_search.py index 1937bd83..9fa5a546 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -2,16 +2,14 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals - from datetime import date, datetime +from unittest.mock import Mock import imaplib from imapclient.exceptions import InvalidCriteriaError from imapclient.imapclient import _quoted from .imapclient_test import IMAPClientTest -from .util import Mock class TestSearchBase(IMAPClientTest): diff --git a/tests/test_sort.py b/tests/test_sort.py index fc91f696..0cf91491 100644 --- a/tests/test_sort.py +++ b/tests/test_sort.py @@ -2,12 +2,11 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals +from unittest.mock import Mock from imapclient.exceptions import CapabilityError from .imapclient_test import IMAPClientTest -from .util import Mock class TestSort(IMAPClientTest): diff --git a/tests/test_starttls.py b/tests/test_starttls.py index b343cbce..556d380c 100644 --- a/tests/test_starttls.py +++ b/tests/test_starttls.py @@ -2,13 +2,12 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals +from unittest.mock import Mock, patch, sentinel from imapclient.imapclient import IMAPClient from imapclient.exceptions import IMAPClientError from .imapclient_test import IMAPClientTest -from .util import Mock, patch, sentinel class TestStarttls(IMAPClientTest): diff --git a/tests/test_store.py b/tests/test_store.py index 66cd8e9c..439ccdf5 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -3,19 +3,16 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals - -import six +from unittest.mock import patch, sentinel, Mock from imapclient.imapclient import DELETED, SEEN, ANSWERED, FLAGGED, DRAFT, RECENT from .imapclient_test import IMAPClientTest -from .util import patch, sentinel, Mock class TestFlagsConsts(IMAPClientTest): def test_flags_are_bytes(self): for flag in DELETED, SEEN, ANSWERED, FLAGGED, DRAFT, RECENT: - if not isinstance(flag, six.binary_type): + if not isinstance(flag, bytes): self.fail("%r flag is not bytes" % flag) diff --git a/tests/test_thread.py b/tests/test_thread.py index e85a1edf..71cf8059 100644 --- a/tests/test_thread.py +++ b/tests/test_thread.py @@ -2,12 +2,11 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals +from unittest.mock import Mock from imapclient.exceptions import CapabilityError from .imapclient_test import IMAPClientTest -from .util import Mock class TestThread(IMAPClientTest): diff --git a/tests/test_util_functions.py b/tests/test_util_functions.py index f58c2534..d9ab7cf6 100644 --- a/tests/test_util_functions.py +++ b/tests/test_util_functions.py @@ -2,7 +2,7 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals +import unittest from imapclient.exceptions import InvalidCriteriaError, ProtocolError from imapclient.imapclient import ( @@ -15,7 +15,6 @@ _parse_untagged_response, ) from imapclient.util import assert_imap_protocol -from tests.util import unittest class Test_normalise_text_list(unittest.TestCase): diff --git a/tests/test_version.py b/tests/test_version.py index fb7d9a68..9936bc0b 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -2,10 +2,9 @@ # Released subject to the New BSD License # Please see http://en.wikipedia.org/wiki/BSD_licenses -from __future__ import unicode_literals +import unittest from imapclient.version import _imapclient_version_string -from .util import unittest class TestVersionString(unittest.TestCase): diff --git a/tests/util.py b/tests/util.py deleted file mode 100644 index 024d8e22..00000000 --- a/tests/util.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (c) 2014, Menno Smits -# Released subject to the New BSD License -# Please see http://en.wikipedia.org/wiki/BSD_licenses - -from __future__ import unicode_literals - -try: - from unittest.mock import Mock, patch, sentinel, DEFAULT -except ImportError: - from mock import Mock, patch, sentinel, DEFAULT - - -def find_unittest2(): - import unittest - - if hasattr(unittest, "skip") and hasattr(unittest, "loader"): - return unittest # unittest from stdlib is unittest2, use that - try: - import unittest2 # try for a separately installed unittest2 package - except ImportError: - raise ImportError( - "unittest2 not installed and unittest in standard library is not unittest2" - ) - else: - return unittest2 - - -unittest = find_unittest2() - - -def patch_TestCase(): - TestCase = unittest.TestCase - # Older versions of unittest2 don't have - # TestCase.assertRaisesRegex and newer version raises warnings - # when you use assertRaisesRegexp. This helps deal with the - # mismatch. - if not hasattr(TestCase, "assertRaisesRegex"): - TestCase.assertRaisesRegex = TestCase.assertRaisesRegexp - - -patch_TestCase() diff --git a/tox.ini b/tox.ini index cad2c2bf..ac9c7576 100644 --- a/tox.ini +++ b/tox.ini @@ -1,17 +1,10 @@ [tox] -envlist=py27,py34,py35,py36,py37,py38,py39 +envlist=py34,py35,py36,py37,py38,py39 requires=sphinx [testenv] commands=python -m unittest -[testenv:py27] -setenv= - VIRTUALENV_PIP=19.0.1 - VIRTUALENV_SETUPTOOLS=43.0.0 -deps=mock -commands=python -m unittest discover - [testenv:py34] setenv= VIRTUALENV_PIP=19.0.1