From 9fba58237d28e910eeba134cd07be4cefb9ec185 Mon Sep 17 00:00:00 2001 From: Mike Edmunds Date: Tue, 2 May 2023 13:59:01 -0700 Subject: [PATCH] Drop support for Python 3.6 and old urllib3 --- .github/workflows/test.yml | 3 +-- CHANGELOG.rst | 12 +++++++++ anymail/backends/mailgun.py | 52 ------------------------------------- pyproject.toml | 4 +-- setup.py | 10 ++++--- tox.ini | 17 +++++------- 6 files changed, 28 insertions(+), 70 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 235027b..5fff326 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -57,8 +57,7 @@ jobs: run: | set -x python -VV - # Must pin virtualenv for tox py36 testenv: - python -m pip install 'tox<4' 'virtualenv<20.22.0' + python -m pip install 'tox<4' python -m tox --version - name: Test ${{ matrix.tox.name }} run: | diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dad971d..9a91ae5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,6 +25,18 @@ Release history ^^^^^^^^^^^^^^^ .. This extra heading level keeps the ToC from becoming unmanageably long +vNext +----- + +*unreleased changes* + +Breaking changes +~~~~~~~~~~~~~~~~ + +* Require Python 3.7 or later. +* Require urllib3 1.25 or later (released 2019-04-29). + + v9.2 ----- diff --git a/anymail/backends/mailgun.py b/anymail/backends/mailgun.py index 7fe6409..09ef77e 100644 --- a/anymail/backends/mailgun.py +++ b/anymail/backends/mailgun.py @@ -1,33 +1,12 @@ from datetime import datetime -from email.utils import encode_rfc2231 from urllib.parse import quote -from requests import Request - from ..exceptions import AnymailError, AnymailRequestsAPIError from ..message import AnymailRecipientStatus from ..utils import get_anymail_setting, rfc2822date from .base_requests import AnymailRequestsBackend, RequestsPayload -# Feature-detect whether requests (urllib3) correctly uses RFC 7578 encoding for non- -# ASCII filenames in Content-Disposition headers. (This was fixed in urllib3 v1.25.) -# See MailgunPayload.get_request_params for info (and a workaround on older versions). -# (Note: when this workaround is removed, please also remove "old_urllib3" tox envs.) -def is_requests_rfc_5758_compliant(): - request = Request( - method="POST", - url="https://www.example.com", - files=[("attachment", ("\N{NOT SIGN}.txt", "test", "text/plain"))], - ) - prepared = request.prepare() - form_data = prepared.body # bytes - return b"filename*=" not in form_data - - -REQUESTS_IS_RFC_7578_COMPLIANT = is_requests_rfc_5758_compliant() - - class EmailBackend(AnymailRequestsBackend): """ Mailgun API Email Backend @@ -163,37 +142,6 @@ class MailgunPayload(RequestsPayload): ) return "%s/messages" % quote(self.sender_domain, safe="") - def get_request_params(self, api_url): - params = super().get_request_params(api_url) - non_ascii_filenames = [ - filename - for (field, (filename, content, mimetype)) in params["files"] - if filename is not None and not isascii(filename) - ] - if non_ascii_filenames and not REQUESTS_IS_RFC_7578_COMPLIANT: - # Workaround https://github.com/requests/requests/issues/4652: - # Mailgun expects RFC 7578 compliant multipart/form-data, and is confused - # by Requests/urllib3's improper use of RFC 2231 encoded filename parameters - # ("filename*=utf-8''...") in Content-Disposition headers. - # The workaround is to pre-generate the (non-compliant) form-data body, and - # replace 'filename*={RFC 2231 encoded}' with 'filename="{UTF-8 bytes}"'. - # Replace _only_ filenames that will be problems (not all "filename*=...") - # to minimize potential side effects--e.g., in attached messages that might - # have their own attachments with (correctly) RFC 2231 encoded filenames. - prepared = Request(**params).prepare() - form_data = prepared.body # bytes - for filename in non_ascii_filenames: # text - rfc2231_filename = encode_rfc2231(filename, charset="utf-8") - form_data = form_data.replace( - b"filename*=" + rfc2231_filename.encode("utf-8"), - b'filename="' + filename.encode("utf-8") + b'"', - ) - params["data"] = form_data - # Content-Type: multipart/form-data; boundary=... - params["headers"]["Content-Type"] = prepared.headers["Content-Type"] - params["files"] = None # these are now in the form_data body - return params - def serialize_data(self): self.populate_recipient_variables() return self.data diff --git a/pyproject.toml b/pyproject.toml index caffbd9..3cb6736 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.black] force-exclude = '^/tests/test_settings/settings_.*\.py' max-line-length = 88 -target-version = ["py36"] +target-version = ["py37"] [tool.doc8] # ignore very long lines in ESP support table: @@ -16,4 +16,4 @@ max-line-length = 120 combine_as_imports = true known_first_party = "anymail" profile = "black" -py_version = "36" +py_version = "37" diff --git a/setup.py b/setup.py index ae3f219..c5d463b 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,6 @@ requirements_dev = [ "sphinx-rtd-theme", "tox", "twine", - "virtualenv<20.22.0", # tox dependency, pinned for Python 3.6 tox testenv "wheel", ] @@ -71,8 +70,12 @@ setup( license="BSD License", packages=["anymail"], zip_safe=False, - python_requires=">=3.6", - install_requires=["django>=2.0", "requests>=2.4.3"], + python_requires=">=3.7", + install_requires=[ + "django>=2.0", + "requests>=2.4.3", + "urllib3>=1.25.0", # requests dependency: fixes RFC 7578 header encoding + ], extras_require={ # This can be used if particular backends have unique dependencies. # For simplicity, requests is included in the base requirements. @@ -100,7 +103,6 @@ setup( "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", diff --git a/tox.ini b/tox.ini index 382942f..90e17a7 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ envlist = # Test lint, docs, earliest/latest Django first, to catch most errors early... lint django42-py311-all - django30-py36-all + django30-py37-all docs # ... then test all the other supported combinations: # Django 4.2: Python 3.8, 3.9, 3.10, 3.11 @@ -15,12 +15,12 @@ envlist = django41-py{38,39,310,py38,py39}-all # Django 4.0: Python 3.8, 3.9, 3.10 django40-py{38,39,310,py38,py39}-all - # Django 3.2: Python 3.6, 3.7, 3.8, 3.9 - django32-py{36,37,38,39,py38,py39}-all - # Django 3.1: Python 3.6, 3.7, 3.8, 3.9 (added in 3.1.3) - django31-py{36,37,38,39,py38,py39}-all - # Django 3.0: Python 3.6, 3.7, 3.8, 3.9 (added in 3.0.11) - django30-py{37,38,39,py38,py39}-all + # Django 3.2: Python 3.6 (eol 2021-12-23), 3.7, 3.8, 3.9 + django32-py{37,38,39,py38,py39}-all + # Django 3.1: Python 3.6 (eol 2021-12-23), 3.7, 3.8, 3.9 (added in 3.1.3) + django31-py{37,38,39,py38,py39}-all + # Django 3.0: Python 3.6 (eol 2021-12-23), 3.7, 3.8, 3.9 (added in 3.0.11) + django30-py{38,39,py38,py39}-all # ... then prereleases (if available) and current development: # Django 5.0 alpha: Python 3.10+ # [not yet in alpha] django50-py{310,311,py310,py311}-all @@ -28,8 +28,6 @@ envlist = djangoDev-py{310,311}-all # ... then partial installation (limit extras): django42-py311-{none,amazon_ses,postal} - # ... then older versions of some dependencies: - django32-py37-all-old_urllib3 [testenv] deps = @@ -41,7 +39,6 @@ deps = django42: django~=4.2.0 django50: django~=5.0.0a0 djangoDev: https://github.com/django/django/tarball/main - old_urllib3: urllib3<1.25 extras = # install [test] extras, unconditionally test