From ca0afa5f4d8a8ce89f5fbb55b618abfcb8ec0971 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Fri, 19 Jul 2024 10:47:23 +0200 Subject: [PATCH] drop support for Python 2 --- .github/workflows/tests.yml | 20 ------- schwarz/__init__.py | 1 - schwarz/mailqueue/app_helpers.py | 4 +- schwarz/mailqueue/compat.py | 90 ----------------------------- schwarz/mailqueue/maildir_utils.py | 6 +- schwarz/mailqueue/mailflow_check.py | 4 +- schwarz/mailqueue/queue_runner.py | 5 +- schwarz/mailqueue/smtpclient.py | 6 +- setup.cfg | 21 ++----- tests/dates_test.py | 34 ----------- tests/message_handler_test.py | 7 +-- tests/mq_run_test.py | 7 +-- tests/queue_runner_test.py | 11 ++-- tests/smtp_mailer_fullstack_test.py | 4 -- tests/smtp_mailer_test.py | 4 -- 15 files changed, 22 insertions(+), 202 deletions(-) delete mode 100644 schwarz/__init__.py delete mode 100644 tests/dates_test.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9a0f21c..078889f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,26 +11,6 @@ on: jobs: - # --- Python 2 tests in separate container ---------------------------------- - tests-py2: - runs-on: ubuntu-latest - container: - image: python:2.7.18-buster - timeout-minutes: 10 - - steps: - - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - pip install --upgrade pip setuptools wheel - pip install -e .[testing] - - - name: Run test suite - run: | - pytest - - # --- tests running on Ubuntu 20.04 ----------------------------------------- tests-py36: # "ubuntu-latest" does not have Python 3.6 diff --git a/schwarz/__init__.py b/schwarz/__init__.py deleted file mode 100644 index de40ea7..0000000 --- a/schwarz/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__import__('pkg_resources').declare_namespace(__name__) diff --git a/schwarz/mailqueue/app_helpers.py b/schwarz/mailqueue/app_helpers.py index c7ed978..535d23d 100644 --- a/schwarz/mailqueue/app_helpers.py +++ b/schwarz/mailqueue/app_helpers.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, print_function, unicode_literals +import configparser import logging import logging.config import os @@ -10,7 +11,6 @@ from schwarz.puzzle_plugins import PluginLoader, parse_list_str -from .compat import SafeConfigParser, configparser from .mailer import SMTPMailer from .plugins import registry @@ -80,7 +80,7 @@ def parse_config(config_path, section_name=None): sys.stderr.write('config file "%s" not found.\n' % filename) sys.exit(20) - parser = SafeConfigParser() + parser = configparser.ConfigParser() exc_msg = None try: parser.read(config_path) diff --git a/schwarz/mailqueue/compat.py b/schwarz/mailqueue/compat.py index 46bf480..2d2f2d3 100644 --- a/schwarz/mailqueue/compat.py +++ b/schwarz/mailqueue/compat.py @@ -1,101 +1,11 @@ # -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT -from __future__ import absolute_import, print_function, unicode_literals - -import email.utils import os -import sys __all__ = [ - 'configparser', - 'format_datetime_rfc2822', - 'make_msgid', - 'os_makedirs', - 'queue', - 'FileNotFoundError', - 'SafeConfigParser', - 'IS_PYTHON3', 'IS_WINDOWS', ] -IS_PYTHON3 = (sys.version_info >= (3,0)) IS_WINDOWS = (os.name == 'nt') - -def os_makedirs(name, mode, exist_ok=False): - if IS_PYTHON3: - os.makedirs(name, mode, exist_ok=exist_ok) - return - # Python 2 version with "exist_ok=True" is racy but that is ok with my - # as Python 2 won't last that long. - if exist_ok is False: - os.makedirs(name, mode) - elif not os.path.exists(name): - os.makedirs(name, mode) - -try: - import configparser - - # in Python 3.2 "SafeConfigParser" was renamed to "ConfigParser" - from configparser import ConfigParser as SafeConfigParser -except ImportError: - import ConfigParser as configparser - from ConfigParser import SafeConfigParser - -try: - import queue -except ImportError: - import Queue as queue - -try: - FileNotFoundError -except NameError: - FileNotFoundError = OSError -else: - FileNotFoundError = FileNotFoundError - - -def format_datetime_rfc2822(dt): - try: - date_str = email.utils.format_datetime(dt) - except AttributeError: - # Python 2 - # email.utils.formatdate() in Python always uses UTC timezone ("-0000") - # so the resulting string points to the same time but possibly in a - # different timezone. - # The naive version might something like - # now_time = time.mktime(dt.timetuple()) - # date_str = email.utils.formatdate(now_time) - date_str = format_datetime_py2(dt) - return date_str - - -WEEKDAYS = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun') -MONTHS = (None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec') - -def format_datetime_py2(dt): - tt = dt.timetuple() - utc_offset_str = dt.strftime('%z') - pattern = '%s, %02d %s %d %02d:%02d:%02d %s' - return pattern % ( - WEEKDAYS[tt.tm_wday], - tt.tm_mday, - MONTHS[tt.tm_mon], - tt.tm_year, - tt.tm_hour, - tt.tm_min, - tt.tm_sec, - utc_offset_str - ) - - -def make_msgid(domain=None): - if not domain: - return email.utils.make_msgid() - if sys.version_info >= (3, 2): - return email.utils.make_msgid(domain=domain) - - msg_id_str = email.utils.make_msgid('@'+domain) - msg_id = msg_id_str.rsplit('@', 1)[0] + '>' - return msg_id diff --git a/schwarz/mailqueue/maildir_utils.py b/schwarz/mailqueue/maildir_utils.py index 8ef496f..38eb02f 100644 --- a/schwarz/mailqueue/maildir_utils.py +++ b/schwarz/mailqueue/maildir_utils.py @@ -8,7 +8,7 @@ import portalocker from boltons.fileutils import atomic_rename, atomic_save -from .compat import IS_WINDOWS, FileNotFoundError, os_makedirs +from .compat import IS_WINDOWS __all__ = ['create_maildir_directories', 'lock_file', 'move_message'] @@ -47,11 +47,11 @@ def write(self, data): def create_maildir_directories(basedir, is_folder=False): - os_makedirs(basedir, 0o700, exist_ok=True) + os.makedirs(basedir, 0o700, exist_ok=True) new_path = None for subdir_name in ('tmp', 'cur', 'new'): subdir_path = os.path.join(basedir, subdir_name) - os_makedirs(subdir_path, 0o700, exist_ok=True) + os.makedirs(subdir_path, 0o700, exist_ok=True) if subdir_name == 'new': new_path = subdir_path diff --git a/schwarz/mailqueue/mailflow_check.py b/schwarz/mailqueue/mailflow_check.py index 0d8f7ce..01bada7 100644 --- a/schwarz/mailqueue/mailflow_check.py +++ b/schwarz/mailqueue/mailflow_check.py @@ -5,11 +5,11 @@ from datetime import datetime as DateTime from email.message import Message +from email.utils import format_datetime, make_msgid from boltons.timeutils import LocalTZ from .app_helpers import init_app, init_smtp_mailer -from .compat import format_datetime_rfc2822, make_msgid from .message_handler import InMemoryMsg, MessageHandler from .message_utils import msg_as_bytes @@ -22,7 +22,7 @@ def build_check_message(recipient, sender=None): mail['From'] = sender mail['To'] = recipient now = DateTime.now(tz=LocalTZ) - mail['Date'] = format_datetime_rfc2822(now) + mail['Date'] = format_datetime(now) # if no domain is specified for ".make_msgid()" the function can take # a long time in case "socket.getfqdn()" must make some network # requests (e.g. flaky network connection). diff --git a/schwarz/mailqueue/queue_runner.py b/schwarz/mailqueue/queue_runner.py index 11f1385..d3a5c7b 100644 --- a/schwarz/mailqueue/queue_runner.py +++ b/schwarz/mailqueue/queue_runner.py @@ -1,18 +1,17 @@ # -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT -from __future__ import absolute_import, print_function, unicode_literals - import email.utils import logging import os +import queue import time from mailbox import Maildir, _sync_close from boltons.timeutils import dt_to_timestamp from .app_helpers import init_app, init_smtp_mailer -from .compat import IS_WINDOWS, queue +from .compat import IS_WINDOWS from .maildir_utils import create_maildir_directories, find_messages, move_message from .message_handler import BaseMsg, MessageHandler from .message_utils import SendResult, dt_now, msg_as_bytes, parse_message_envelope diff --git a/schwarz/mailqueue/smtpclient.py b/schwarz/mailqueue/smtpclient.py index 5b5b1dd..aea89d9 100644 --- a/schwarz/mailqueue/smtpclient.py +++ b/schwarz/mailqueue/smtpclient.py @@ -11,8 +11,6 @@ import socket from contextlib import contextmanager -import six - from .lib.smtplib_py37 import ( CRLF, SMTP, @@ -49,7 +47,7 @@ def __init__(self, *args, **kwargs): def sendmail(self, from_addr, to_addrs, msg, mail_options=(), rcpt_options=()): self.ehlo_or_helo_if_needed() esmtp_opts = [] - if isinstance(msg, six.string_types): + if isinstance(msg, str): msg = _fix_eols(msg).encode('ascii') if self.does_esmtp: if self.has_extn('size'): @@ -63,7 +61,7 @@ def sendmail(self, from_addr, to_addrs, msg, mail_options=(), rcpt_options=()): else: self._rset() raise SMTPSenderRefused(code, resp, from_addr) - if isinstance(to_addrs, six.string_types): + if isinstance(to_addrs, str): to_addrs = [to_addrs] for each in to_addrs: (code, resp) = self.rcpt(each, rcpt_options) diff --git a/setup.cfg b/setup.cfg index aaf1449..de6b51a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,20 +12,16 @@ long_description_content_type = text/markdown [options] -python_requires = >= 2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* -packages = find: -namespace_packages = schwarz +python_requires = >= 3.6 +packages = find_namespace: zip_safe = false include_package_data = true install_requires = boltons docopt - # portalocker 2.0 dropped Python 2 support - portalocker < 2.0 ; python_version < '3.0' - portalocker ; python_version > '3.0' + portalocker PuzzlePluginSystem >= 0.6.0 # >= 0.6.0: .terminate_all_activated_plugins() - six [options.extras_require] # "testutils" provides helpers to simplify testing (also usable by 3rd party code) @@ -36,15 +32,8 @@ testutils = testing = %(testutils)s - # dotmap 1.3.25 started using f-strings without declaring a minimum Python - # version: https://github.com/drgrib/dotmap/issues/83 - dotmap <= 1.3.24 ; python_version < '3.0' - dotmap; python_version >= '3.0' - # Even the oldest versions of "time-machine" support only Python 3.6+ - freezegun < 1.0 ; python_version < '3.0' - freezegun ; python_version > '3.0' and implementation_name == 'pypy' - time-machine ; python_version > '3.0' and implementation_name != 'pypy' - mock ; python_version < '3.0' + dotmap + time-machine; implementation_name != 'pypy' pytest setuptools testfixtures diff --git a/tests/dates_test.py b/tests/dates_test.py deleted file mode 100644 index c23568a..0000000 --- a/tests/dates_test.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -# SPDX-License-Identifier: MIT - -from __future__ import absolute_import, print_function, unicode_literals - -from datetime import datetime as DateTime, timedelta as TimeDelta - -import pytest -from boltons.timeutils import ZERO, ConstantTZInfo - -from schwarz.mailqueue.compat import format_datetime_rfc2822 -from schwarz.mailqueue.message_utils import parse_datetime - - -@pytest.mark.parametrize('dt', [ - DateTime(2020, 3, 1, hour=11, minute=42, second=23, tzinfo=ConstantTZInfo(offset=TimeDelta(hours=1))), # noqa: E501 (line too long) - DateTime(2020, 7, 21, hour=23, minute=2, second=59, tzinfo=ConstantTZInfo(offset=TimeDelta(hours=2))), # noqa: E501 (line too long) - DateTime(2020, 7, 21, hour=23, minute=2, second=59, tzinfo=ConstantTZInfo(offset=TimeDelta(hours=6))), # noqa: E501 (line too long) - DateTime(2020, 7, 21, hour=23, minute=2, second=59, tzinfo=ConstantTZInfo(offset=ZERO)), -]) -def test_parse_datetime(dt): - dt_str = format_datetime_rfc2822(dt) - assert parse_datetime(dt_str) == dt - -@pytest.mark.parametrize('offset_str, offset', [ - ('-0500', -(5*60)), - ('-0445', -(4*60 + 45)), -]) -def test_format_datetime_rfc2822(offset_str, offset): - offset_td = TimeDelta(minutes=offset) - tz = ConstantTZInfo(offset=offset_td) - dt = DateTime(2020, 7, 21, hour=23, minute=2, second=59, tzinfo=tz) - expected_str = 'Tue, 21 Jul 2020 23:02:59 ' + offset_str - assert format_datetime_rfc2822(dt) == expected_str diff --git a/tests/message_handler_test.py b/tests/message_handler_test.py index ef21948..39b33a9 100644 --- a/tests/message_handler_test.py +++ b/tests/message_handler_test.py @@ -5,13 +5,8 @@ import os import shutil - - -try: - from unittest.mock import MagicMock -except ImportError: - from mock import MagicMock import uuid +from unittest.mock import MagicMock import pytest from schwarz.log_utils import l_ diff --git a/tests/mq_run_test.py b/tests/mq_run_test.py index 139f925..07aef45 100644 --- a/tests/mq_run_test.py +++ b/tests/mq_run_test.py @@ -4,12 +4,7 @@ from __future__ import absolute_import, print_function, unicode_literals import os - - -try: - from unittest import mock -except ImportError: - import mock +from unittest import mock from pkg_resources import Distribution, EntryPoint, WorkingSet from schwarz.log_utils import l_ diff --git a/tests/queue_runner_test.py b/tests/queue_runner_test.py index 03cdebe..ef065ec 100644 --- a/tests/queue_runner_test.py +++ b/tests/queue_runner_test.py @@ -4,7 +4,6 @@ from __future__ import absolute_import, print_function, unicode_literals import os -import sys from datetime import datetime as DateTime, timedelta as TimeDelta import pytest @@ -12,9 +11,9 @@ try: - from time_machine import travel as freeze_time + import time_machine except ImportError: - from freezegun import freeze_time + time_machine = None from testfixtures import LogCapture from schwarz.mailqueue import ( @@ -34,9 +33,7 @@ def path_maildir(tmp_path): return _path_maildir -IS_PYPY3 = (sys.version_info >= (3, 0)) and (sys.implementation.name == 'pypy') - -@pytest.mark.skipif(IS_PYPY3, reason='"time-machine" does not work on PyPy') +@pytest.mark.skipif(time_machine is None, reason='"time-machine" dependency not installed') # https://github.com/adamchainz/time-machine/issues/305 def test_can_move_stale_messages_back_to_new(path_maildir): mailer = DebugMailer() @@ -50,7 +47,7 @@ def test_can_move_stale_messages_back_to_new(path_maildir): dt_stale = DateTime.now() + TimeDelta(hours=1) # LogCapture: no logged warning about stale message on the command line with LogCapture(): - with freeze_time(dt_stale): + with time_machine.travel(dt_stale): send_all_queued_messages(path_maildir, mailer) assert len(msg_files(path_maildir, folder='new')) == 0 assert len(msg_files(path_maildir, folder='cur')) == 0 diff --git a/tests/smtp_mailer_fullstack_test.py b/tests/smtp_mailer_fullstack_test.py index 4409d50..a709608 100644 --- a/tests/smtp_mailer_fullstack_test.py +++ b/tests/smtp_mailer_fullstack_test.py @@ -8,7 +8,6 @@ from pymta.test_util import SMTPTestHelper from schwarz.mailqueue import SMTPMailer -from schwarz.mailqueue.compat import IS_PYTHON3 @pytest.fixture @@ -42,7 +41,4 @@ def test_can_send_message(ctx): assert received_message.username is None # pymta converts this to a string automatically expected_message = message.decode('ASCII') - # in Python 2 the received message lacks the final '\n' (unknown reason) - if not IS_PYTHON3: - expected_message = expected_message.rstrip('\n') assert received_message.msg_data == expected_message diff --git a/tests/smtp_mailer_test.py b/tests/smtp_mailer_test.py index 2e963d8..fe2035e 100644 --- a/tests/smtp_mailer_test.py +++ b/tests/smtp_mailer_test.py @@ -11,7 +11,6 @@ from schwarz.log_utils.testutils import build_collecting_logger from schwarz.mailqueue import SMTPMailer -from schwarz.mailqueue.compat import IS_PYTHON3 from schwarz.mailqueue.testutils import SocketMock, fake_smtp_client, stub_socket_creation @@ -32,9 +31,6 @@ def test_can_send_message_via_smtpmailer(): assert received_message.username is None # pymta converts this to a string automatically expected_message = message.decode('ASCII') - # in Python 2 the received message lacks the final '\n' (unknown reason) - if not IS_PYTHON3: - expected_message = expected_message.rstrip('\n') assert received_message.msg_data == expected_message @pytest.mark.parametrize('exc', [