Skip to content

Commit

Permalink
Remove support for Python 2
Browse files Browse the repository at this point in the history
  * 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

Closes: #401
  • Loading branch information
JohnVillalovos committed Jun 17, 2022
1 parent 5bccbad commit 60a2463
Show file tree
Hide file tree
Showing 19 changed files with 82 additions and 147 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 0 additions & 7 deletions doc/src/concepts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion doc/src/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------------
Expand Down
16 changes: 8 additions & 8 deletions imapclient/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@
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

Expand Down Expand Up @@ -45,7 +44,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)

Expand All @@ -62,7 +61,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:
Expand All @@ -80,7 +79,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
Expand Down Expand Up @@ -133,7 +132,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"]


Expand Down
13 changes: 6 additions & 7 deletions imapclient/imap_utf7.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from __future__ import unicode_literals

import binascii
from six import binary_type, text_type, byte2int, iterbytes, unichr


def encode(s):
Expand All @@ -18,7 +17,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()
Expand Down Expand Up @@ -56,8 +55,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):
Expand All @@ -67,13 +66,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)
Expand All @@ -90,7 +89,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:
Expand Down
59 changes: 24 additions & 35 deletions imapclient/imapclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,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
Expand All @@ -28,8 +26,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

Expand All @@ -38,9 +34,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__)

Expand Down Expand Up @@ -744,11 +737,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)

Expand Down Expand Up @@ -838,7 +831,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
Expand Down Expand Up @@ -1190,7 +1183,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
Expand Down Expand Up @@ -1276,7 +1269,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):
Expand Down Expand Up @@ -1405,10 +1398,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(
Expand Down Expand Up @@ -1528,7 +1518,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):
Expand Down Expand Up @@ -1730,8 +1720,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))
Expand All @@ -1749,7 +1738,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
)
Expand All @@ -1772,17 +1761,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]

Expand All @@ -1796,7 +1785,7 @@ def welcome(self):


def _quote(arg):
if isinstance(arg, text_type):
if isinstance(arg, str):
arg = arg.replace("\\", "\\\\")
arg = arg.replace('"', '\\"')
q = '"'
Expand All @@ -1813,7 +1802,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 = []
Expand All @@ -1834,7 +1823,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")"

Expand All @@ -1845,7 +1834,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.
Expand Down Expand Up @@ -1892,7 +1881,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)

Expand All @@ -1901,14 +1890,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)


Expand Down Expand Up @@ -1943,7 +1932,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):
Expand All @@ -1964,7 +1953,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.
Expand Down Expand Up @@ -1998,7 +1987,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)
Expand Down Expand Up @@ -2037,7 +2026,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"):
Expand Down
4 changes: 1 addition & 3 deletions imapclient/interact.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
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


Expand Down Expand Up @@ -93,7 +91,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 + ": ")
Expand Down
Loading

0 comments on commit 60a2463

Please sign in to comment.