From 84c2dfb4199ca7f227a489422cb2bb3edcdd9c34 Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <hugovk@users.noreply.github.com>
Date: Tue, 23 Nov 2021 17:14:37 +0200
Subject: [PATCH 01/12] Add support for Python 3.8-3.10

---
 .github/workflows/test.yaml | 2 +-
 setup.py                    | 3 +++
 tox.ini                     | 2 +-
 3 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 19741b8..dded674 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -8,7 +8,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [3.6, 3.7, 3.8, 3.9]
+        python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
 
     steps:
       - uses: actions/checkout@v2
diff --git a/setup.py b/setup.py
index aefc52c..834811a 100755
--- a/setup.py
+++ b/setup.py
@@ -53,6 +53,9 @@
         'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: 3.7',
+        'Programming Language :: Python :: 3.8',
+        'Programming Language :: Python :: 3.9',
+        'Programming Language :: Python :: 3.10',
         'Topic :: Software Development :: Libraries :: Python Modules'
       ],
       test_suite = 'multipart.tests.suite',
diff --git a/tox.ini b/tox.ini
index 1aa01f1..fc58f96 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py27,py31,py32,py33
+envlist = py27,py31,py32,py33,py36,py37,py38,py39,py310
 
 [testenv]
 deps=

From 44c111ec4daf8f83951ae57a733df987cf9224a4 Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <hugovk@users.noreply.github.com>
Date: Tue, 23 Nov 2021 17:15:52 +0200
Subject: [PATCH 02/12] Remove redundant Travis CI config, GHA is used for CI

---
 .travis.yml | 20 --------------------
 1 file changed, 20 deletions(-)
 delete mode 100644 .travis.yml

diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 45068e0..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-sudo: false
-language: python
-
-python:
-  - "2.7"
-  - "3.4"
-  - "3.5"
-  - "3.6"
-  - "3.7"
-  - "pypy"
-
-install:
-  - pip install -r requirements.txt
-
-script:
-  - py.test
-
-branches:
-    only:
-        - master

From 85d84d7431b0c21e4265a626f07af09f3d56798a Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <hugovk@users.noreply.github.com>
Date: Tue, 23 Nov 2021 17:17:40 +0200
Subject: [PATCH 03/12] Update build badge for GHA

---
 README.rst | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.rst b/README.rst
index 898fc9d..508b14e 100644
--- a/README.rst
+++ b/README.rst
@@ -2,8 +2,8 @@
  Python-Multipart
 ==================
 
-.. image:: https://secure.travis-ci.org/andrew-d/python-multipart.png?branch=master
-        :target: http://travis-ci.org/andrew-d/python-multipart
+.. image:: https://github.com/andrew-d/python-multipart/actions/workflows/test.yaml/badge.svg
+        :target: https://github.com/andrew-d/python-multipart/actions
 
 
 python-multipart is an Apache2 licensed streaming multipart parser for Python.

From 3a4a0295f300d22787813ab4cdf7654e1a21ee82 Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <hugovk@users.noreply.github.com>
Date: Tue, 23 Nov 2021 17:20:25 +0200
Subject: [PATCH 04/12] Drop the dot
 https://twitter.com/pytestdotorg/status/753767547866972160

---
 README.rst                | 2 +-
 multipart/tests/compat.py | 2 +-
 tasks.py                  | 2 +-
 tox.ini                   | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/README.rst b/README.rst
index 508b14e..aceb6e2 100644
--- a/README.rst
+++ b/README.rst
@@ -25,4 +25,4 @@ If you want to test:
 .. code-block:: bash
 
     $ pip install -r requirements.txt
-    $ py.test
+    $ pytest
diff --git a/multipart/tests/compat.py b/multipart/tests/compat.py
index 7a3479e..c7193e5 100644
--- a/multipart/tests/compat.py
+++ b/multipart/tests/compat.py
@@ -58,7 +58,7 @@ def xfail(*args, **kwargs):
         return lambda x: x
 
 
-# We don't use the py.test parametrizing function, since it seems to break
+# We don't use the pytest parametrizing function, since it seems to break
 # with unittest.TestCase subclasses.
 def parametrize(field_names, field_values):
     # If we're not given a list of field names, we make it.
diff --git a/tasks.py b/tasks.py
index 727b950..77934d1 100644
--- a/tasks.py
+++ b/tasks.py
@@ -18,7 +18,7 @@ class g(object):
 @task
 def test(all=False):
     test_cmd = [
-        'py.test',                      # Test command
+        'pytest',                       # Test command
         '--cov-report term-missing',    # Print only uncovered lines to stdout
         '--cov-config .coveragerc',     # Use this file for configuration
         '--cov multipart',              # Test only this module
diff --git a/tox.ini b/tox.ini
index fc58f96..00f9bff 100644
--- a/tox.ini
+++ b/tox.ini
@@ -9,7 +9,7 @@ deps=
     mock
     PyYAML
 commands=
-    py.test --cov-report term-missing --cov-config .coveragerc --cov multipart --timeout=30 multipart/tests
+    pytest --cov-report term-missing --cov-config .coveragerc --cov multipart --timeout=30 multipart/tests
 
 [testenv:py31]
 basepython=/usr/local/Cellar/python31/3.1.5/bin/python3.1

From b2117c113704b14253301a06d7c96f5a7a77fd38 Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <hugovk@users.noreply.github.com>
Date: Tue, 23 Nov 2021 17:25:08 +0200
Subject: [PATCH 05/12] Bump attrs, pluggy and pytest pins to support Python
 3.10

---
 docs_requirements.txt | 2 +-
 requirements.txt      | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/docs_requirements.txt b/docs_requirements.txt
index 0be06a5..55b9125 100644
--- a/docs_requirements.txt
+++ b/docs_requirements.txt
@@ -10,7 +10,7 @@ invoke==0.2.0
 mock==1.0.1
 pexpect-u==2.5.1
 py==1.10.0
-pytest==2.3.4
+pytest==6.2.5
 pytest-capturelog==0.7
 pytest-cov==1.6
 pytest-timeout==0.3
diff --git a/requirements.txt b/requirements.txt
index 8c7c701..37dc501 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,11 +1,11 @@
 atomicwrites==1.2.1
-attrs==18.2.0
+attrs==19.2.0
 coverage==4.5.1
 mock==2.0.0
 more-itertools==4.3.0
 pbr==4.3.0
-pluggy==0.7.1
+pluggy==1.0.0
 py==1.10.0
-pytest==3.8.2
+pytest==6.2.5
 PyYAML==5.1
 six==1.11.0

From 763ce7f2145533a34704cc53ee00a0f12a68f9f9 Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <hugovk@users.noreply.github.com>
Date: Tue, 23 Nov 2021 17:29:52 +0200
Subject: [PATCH 06/12] Drop support or EOL Python 2.7-3.5

---
 setup.py | 15 +++------------
 tox.ini  |  5 +----
 2 files changed, 4 insertions(+), 16 deletions(-)

diff --git a/setup.py b/setup.py
index 834811a..6f02a8d 100755
--- a/setup.py
+++ b/setup.py
@@ -3,15 +3,11 @@
 
 import os
 import re
-import sys
 from setuptools import setup
 
 version_file = os.path.join('multipart', '_version.py')
 with open(version_file, 'rb') as f:
-    version_data = f.read().strip()
-
-if sys.version_info[0] >= 3:
-    version_data = version_data.decode('ascii')
+    version_data = f.read().strip().decode('ascii')
 
 version_re = re.compile(r'((?:\d+)\.(?:\d+)\.(?:\d+))')
 version = version_re.search(version_data).group(0)
@@ -22,9 +18,6 @@
     'PyYAML'
 ]
 
-if sys.version_info[0:2] < (3, 3):
-    tests_require.append('mock')
-
 setup(name='python-multipart',
       version=version,
       description='A streaming multipart parser for Python',
@@ -47,10 +40,8 @@
         'Intended Audience :: Developers',
         'License :: OSI Approved :: Apache Software License',
         'Operating System :: OS Independent',
-        'Programming Language :: Python :: 2.7',
-        'Programming Language :: Python :: 3.3',
-        'Programming Language :: Python :: 3.4',
-        'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: 3 :: Only',
+        'Programming Language :: Python :: 3',
         'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
diff --git a/tox.ini b/tox.ini
index 00f9bff..c62cdb1 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py27,py31,py32,py33,py36,py37,py38,py39,py310
+envlist = py36,py37,py38,py39,py310
 
 [testenv]
 deps=
@@ -10,6 +10,3 @@ deps=
     PyYAML
 commands=
     pytest --cov-report term-missing --cov-config .coveragerc --cov multipart --timeout=30 multipart/tests
-
-[testenv:py31]
-basepython=/usr/local/Cellar/python31/3.1.5/bin/python3.1

From 2301ca92066163a2576bc8dce07a41b908b0f8ba Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <hugovk@users.noreply.github.com>
Date: Tue, 23 Nov 2021 17:33:28 +0200
Subject: [PATCH 07/12] Upgrade Python syntax with pyupgrade --py36-plus

---
 docs/source/conf.py               | 17 ++++-----
 multipart/__init__.py             |  1 -
 multipart/decoders.py             |  8 ++--
 multipart/multipart.py            | 63 ++++++++++++++-----------------
 multipart/tests/compat.py         |  2 +-
 multipart/tests/test_multipart.py | 14 +++----
 setup.py                          |  1 -
 tasks.py                          | 10 ++---
 8 files changed, 51 insertions(+), 65 deletions(-)

diff --git a/docs/source/conf.py b/docs/source/conf.py
index a260547..8851bc7 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 #
 # python-multipart documentation build configuration file, created by
 # sphinx-quickstart on Fri Apr  5 20:24:27 2013.
@@ -42,8 +41,8 @@
 master_doc = 'index'
 
 # General information about the project.
-project = u'python-multipart'
-copyright = u'2013, Andrew Dunham'
+project = 'python-multipart'
+copyright = '2013, Andrew Dunham'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
@@ -192,8 +191,8 @@
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title, author, documentclass [howto/manual]).
 latex_documents = [
-  ('index', 'python-multipart.tex', u'python-multipart Documentation',
-   u'Andrew Dunham', 'manual'),
+  ('index', 'python-multipart.tex', 'python-multipart Documentation',
+   'Andrew Dunham', 'manual'),
 ]
 
 # The name of an image file (relative to this directory) to place at the top of
@@ -222,8 +221,8 @@
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
 man_pages = [
-    ('index', 'python-multipart', u'python-multipart Documentation',
-     [u'Andrew Dunham'], 1)
+    ('index', 'python-multipart', 'python-multipart Documentation',
+     ['Andrew Dunham'], 1)
 ]
 
 # If true, show URL addresses after external links.
@@ -236,8 +235,8 @@
 # (source start file, target name, title, author,
 #  dir menu entry, description, category)
 texinfo_documents = [
-  ('index', 'python-multipart', u'python-multipart Documentation',
-   u'Andrew Dunham', 'python-multipart', 'One line description of project.',
+  ('index', 'python-multipart', 'python-multipart Documentation',
+   'Andrew Dunham', 'python-multipart', 'One line description of project.',
    'Miscellaneous'),
 ]
 
diff --git a/multipart/__init__.py b/multipart/__init__.py
index cbe9211..1f04bca 100644
--- a/multipart/__init__.py
+++ b/multipart/__init__.py
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
 import sys
 
 # This is the canonical package information.
diff --git a/multipart/decoders.py b/multipart/decoders.py
index 06cb23f..6dac55e 100644
--- a/multipart/decoders.py
+++ b/multipart/decoders.py
@@ -4,7 +4,7 @@
 from .exceptions import Base64Error, DecodeError
 
 
-class Base64Decoder(object):
+class Base64Decoder:
     """This object provides an interface to decode a stream of Base64 data.  It
     is instantiated with an "underlying object", and whenever a write()
     operation is performed, it will decode the incoming data as Base64, and
@@ -99,10 +99,10 @@ def finalize(self):
             self.underlying.finalize()
 
     def __repr__(self):
-        return "%s(underlying=%r)" % (self.__class__.__name__, self.underlying)
+        return f"{self.__class__.__name__}(underlying={self.underlying!r})"
 
 
-class QuotedPrintableDecoder(object):
+class QuotedPrintableDecoder:
     """This object provides an interface to decode a stream of quoted-printable
     data.  It is instantiated with an "underlying object", in the same manner
     as the :class:`multipart.decoders.Base64Decoder` class.  This class behaves
@@ -168,4 +168,4 @@ def finalize(self):
             self.underlying.finalize()
 
     def __repr__(self):
-        return "%s(underlying=%r)" % (self.__class__.__name__, self.underlying)
+        return f"{self.__class__.__name__}(underlying={self.underlying!r})"
diff --git a/multipart/multipart.py b/multipart/multipart.py
index 11c062e..6fac9d1 100644
--- a/multipart/multipart.py
+++ b/multipart/multipart.py
@@ -1,5 +1,3 @@
-from __future__ import with_statement, absolute_import, print_function
-
 from six import (
     binary_type,
     text_type,
@@ -74,14 +72,9 @@
 # str on Py2, and bytes on Py3.  Same with getting the ordinal value of a byte,
 # and joining a list of bytes together.
 # These functions abstract that.
-if PY3:                         # pragma: no cover
-    lower_char = lambda c: c | 0x20
-    ord_char = lambda c: c
-    join_bytes = lambda b: bytes(list(b))
-else:                           # pragma: no cover
-    lower_char = lambda c: c.lower()
-    ord_char = lambda c: ord(c)
-    join_bytes = lambda b: b''.join(list(b))
+lower_char = lambda c: c | 0x20
+ord_char = lambda c: c
+join_bytes = lambda b: bytes(list(b))
 
 # These are regexes for parsing header values.
 SPECIAL_CHARS = re.escape(b'()<>@,;:\\"/[]?={} \t')
@@ -104,7 +97,7 @@ def parse_options_header(value):
 
     # If we are passed a string, we assume that it conforms to WSGI and does
     # not contain any code point that's not in latin-1.
-    if isinstance(value, text_type):            # pragma: no cover
+    if isinstance(value, str):            # pragma: no cover
         value = value.encode('latin-1')
 
     # If we have no options, return the string as-is.
@@ -135,7 +128,7 @@ def parse_options_header(value):
     return ctype, options
 
 
-class Field(object):
+class Field:
     """A Field object represents a (parsed) form field.  It represents a single
     field with a corresponding name and value.
 
@@ -252,14 +245,14 @@ def __repr__(self):
         else:
             v = repr(self.value)
 
-        return "%s(field_name=%r, value=%s)" % (
+        return "{}(field_name={!r}, value={})".format(
             self.__class__.__name__,
             self.field_name,
             v
         )
 
 
-class File(object):
+class File:
     """This class represents an uploaded file.  It handles writing file data to
     either an in-memory file or a temporary file on-disk, if the optional
     threshold is passed.
@@ -442,7 +435,7 @@ def _get_disk_file(self):
             try:
                 self.logger.info("Opening file: %r", path)
                 tmp_file = open(path, 'w+b')
-            except (IOError, OSError) as e:
+            except OSError as e:
                 tmp_file = None
 
                 self.logger.exception("Error opening temporary file")
@@ -454,13 +447,13 @@ def _get_disk_file(self):
             options = {}
             if keep_extensions:
                 ext = self._ext
-                if isinstance(ext, binary_type):
+                if isinstance(ext, bytes):
                     ext = ext.decode(sys.getfilesystemencoding())
 
                 options['suffix'] = ext
             if file_dir is not None:
                 d = file_dir
-                if isinstance(d, binary_type):
+                if isinstance(d, bytes):
                     d = d.decode(sys.getfilesystemencoding())
 
                 options['dir'] = d
@@ -471,14 +464,14 @@ def _get_disk_file(self):
                              options)
             try:
                 tmp_file = tempfile.NamedTemporaryFile(**options)
-            except (IOError, OSError):
+            except OSError:
                 self.logger.exception("Error creating named temporary file")
                 raise FileError("Error creating named temporary file")
 
             fname = tmp_file.name
 
             # Encode filename as bytes.
-            if isinstance(fname, text_type):
+            if isinstance(fname, str):
                 fname = fname.encode(sys.getfilesystemencoding())
 
         self._actual_file_name = fname
@@ -543,14 +536,14 @@ def close(self):
         self._fileobj.close()
 
     def __repr__(self):
-        return "%s(file_name=%r, field_name=%r)" % (
+        return "{}(file_name={!r}, field_name={!r})".format(
             self.__class__.__name__,
             self.file_name,
             self.field_name
         )
 
 
-class BaseParser(object):
+class BaseParser:
     """This class is the base class for all parsers.  It contains the logic for
     calling and adding callbacks.
 
@@ -657,7 +650,7 @@ class OctetStreamParser(BaseParser):
                      i.e. unbounded.
     """
     def __init__(self, callbacks={}, max_size=float('inf')):
-        super(OctetStreamParser, self).__init__()
+        super().__init__()
         self.callbacks = callbacks
         self._started = False
 
@@ -750,7 +743,7 @@ class QuerystringParser(BaseParser):
     """
     def __init__(self, callbacks={}, strict_parsing=False,
                  max_size=float('inf')):
-        super(QuerystringParser, self).__init__()
+        super().__init__()
         self.state = STATE_BEFORE_FIELD
         self._found_sep = False
 
@@ -949,7 +942,7 @@ def finalize(self):
         self.callback('end')
 
     def __repr__(self):
-        return "%s(strict_parsing=%r, max_size=%r)" % (
+        return "{}(strict_parsing={!r}, max_size={!r})".format(
             self.__class__.__name__,
             self.strict_parsing, self.max_size
         )
@@ -1012,7 +1005,7 @@ class MultipartParser(BaseParser):
 
     def __init__(self, boundary, callbacks={}, max_size=float('inf')):
         # Initialize parser state.
-        super(MultipartParser, self).__init__()
+        super().__init__()
         self.state = STATE_START
         self.index = self.flags = 0
 
@@ -1037,7 +1030,7 @@ def __init__(self, boundary, callbacks={}, max_size=float('inf')):
         # self.skip = tuple(skip)
 
         # Save our boundary.
-        if isinstance(boundary, text_type):         # pragma: no cover
+        if isinstance(boundary, str):         # pragma: no cover
             boundary = boundary.encode('latin-1')
         self.boundary = b'\r\n--' + boundary
 
@@ -1283,7 +1276,7 @@ def data_callback(name, remaining=False):
                 # a CR at the beginning of a header, so our next character
                 # should be a LF, or it's an error.
                 if c != LF:
-                    msg = "Did not find LF at end of headers (found %r)" % (c,)
+                    msg = f"Did not find LF at end of headers (found {c!r})"
                     self.logger.warning(msg)
                     e = MultipartParseError(msg)
                     e.offset = i
@@ -1483,10 +1476,10 @@ def finalize(self):
         pass
 
     def __repr__(self):
-        return "%s(boundary=%r)" % (self.__class__.__name__, self.boundary)
+        return f"{self.__class__.__name__}(boundary={self.boundary!r})"
 
 
-class FormParser(object):
+class FormParser:
     """This class is the all-in-one form parser.  Given all the information
     necessary to parse a form, it will instantiate the correct parser, create
     the proper :class:`Field` and :class:`File` classes to store the data that
@@ -1579,7 +1572,7 @@ def __init__(self, content_type, on_field, on_file, on_end=None,
         # Depending on the Content-Type, we instantiate the correct parser.
         if content_type == 'application/octet-stream':
             # Work around the lack of 'nonlocal' in Py2
-            class vars(object):
+            class vars:
                 f = None
 
             def on_start():
@@ -1614,7 +1607,7 @@ def on_end():
 
             name_buffer = []
 
-            class vars(object):
+            class vars:
                 f = None
 
             def on_field_start():
@@ -1671,7 +1664,7 @@ def on_end():
             headers = {}
 
             # No 'nonlocal' on Python 2 :-(
-            class vars(object):
+            class vars:
                 f = None
                 writer = None
                 is_file = False
@@ -1745,7 +1738,7 @@ def on_headers_finished():
                                         "%r", transfer_encoding)
                     if self.config['UPLOAD_ERROR_ON_BAD_CTE']:
                         raise FormParserError(
-                            'Unknown Content-Transfer-Encoding "{0}"'.format(
+                            'Unknown Content-Transfer-Encoding "{}"'.format(
                                 transfer_encoding
                             )
                         )
@@ -1777,7 +1770,7 @@ def on_end():
 
         else:
             self.logger.warning("Unknown Content-Type: %r", content_type)
-            raise FormParserError("Unknown Content-Type: {0}".format(
+            raise FormParserError("Unknown Content-Type: {}".format(
                 content_type
             ))
 
@@ -1804,7 +1797,7 @@ def close(self):
             self.parser.close()
 
     def __repr__(self):
-        return "%s(content_type=%r, parser=%r)" % (
+        return "{}(content_type={!r}, parser={!r})".format(
             self.__class__.__name__,
             self.content_type,
             self.parser,
diff --git a/multipart/tests/compat.py b/multipart/tests/compat.py
index c7193e5..0fb84c3 100644
--- a/multipart/tests/compat.py
+++ b/multipart/tests/compat.py
@@ -20,7 +20,7 @@ def ensure_in_path(path):
     def _samefile(x, y):
         try:
             return os.path.samefile(x, y)
-        except (IOError, OSError):
+        except OSError:
             return False
         except AttributeError:
             # Probably on Windows.
diff --git a/multipart/tests/test_multipart.py b/multipart/tests/test_multipart.py
index 0e38c9c..7e49e16 100644
--- a/multipart/tests/test_multipart.py
+++ b/multipart/tests/test_multipart.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 import os
 import sys
 import glob
@@ -19,7 +17,7 @@
 try:
     from unittest.mock import MagicMock, Mock, patch
 except ImportError:
-    from mock import MagicMock, Mock, patch
+    from unittest.mock import MagicMock, Mock, patch
 
 from ..multipart import *
 
@@ -29,7 +27,7 @@
 
 
 def force_bytes(val):
-    if isinstance(val, text_type):
+    if isinstance(val, str):
         val = val.encode(sys.getfilesystemencoding())
 
     return val
@@ -427,7 +425,7 @@ def test_strict_parsing_pass(self):
             self.reset()
             self.p.strict_parsing = True
 
-            print("%r / %r" % (first, last))
+            print(f"{first!r} / {last!r}")
 
             self.p.write(first)
             self.p.write(last)
@@ -453,7 +451,7 @@ def test_strict_parsing_fail_double_sep(self):
     def test_double_sep(self):
         data = b'foo=bar&&another=asdf'
         for first, last in split_all(data):
-            print(" %r / %r " % (first, last))
+            print(f" {first!r} / {last!r} ")
             self.reset()
 
             cnt = 0
@@ -699,7 +697,7 @@ def test_not_aligned(self):
 http_tests_dir = os.path.join(curr_dir, 'test_data', 'http')
 
 # Read in all test cases and load them.
-NON_PARAMETRIZED_TESTS = set(['single_field_blocks'])
+NON_PARAMETRIZED_TESTS = {'single_field_blocks'}
 http_tests = []
 for f in os.listdir(http_tests_dir):
     # Only load the HTTP test cases.
@@ -803,7 +801,7 @@ def assert_field(self, name, value):
     def test_http(self, param):
         # Firstly, create our parser with the given boundary.
         boundary = param['result']['boundary']
-        if isinstance(boundary, text_type):
+        if isinstance(boundary, str):
             boundary = boundary.encode('latin-1')
         self.make(boundary)
 
diff --git a/setup.py b/setup.py
index 6f02a8d..dd7542e 100755
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,4 @@
 #!/usr/bin/env python
-from __future__ import print_function
 
 import os
 import re
diff --git a/tasks.py b/tasks.py
index 77934d1..b099df3 100644
--- a/tasks.py
+++ b/tasks.py
@@ -1,5 +1,3 @@
-from __future__ import print_function
-
 import os
 import re
 import sys
@@ -11,7 +9,7 @@
 version_regex = re.compile(r'((?:\d+)\.(?:\d+)\.(?:\d+))')
 
 # Get around Python 2.X's lack of 'nonlocal' keyword
-class g(object):
+class g:
     test_success = False
 
 
@@ -46,7 +44,7 @@ def bump(type):
 
     m = version_regex.search(file_data)
     if m is None:
-        print("Could not find version in '{0}'!".format(version_file), file=sys.stderr)
+        print(f"Could not find version in '{version_file}'!", file=sys.stderr)
         return
 
     version = m.group(0)
@@ -63,7 +61,7 @@ def bump(type):
     elif type == 'major':
         ver_nums[0] += 1
     else:
-        print("Invalid version type: '%s'" % (type,), file=sys.stderr)
+        print(f"Invalid version type: '{type}'", file=sys.stderr)
         return
 
     # Construct new data and write to file.
@@ -74,7 +72,7 @@ def bump(type):
         f.write(new_data)
 
     # Print information.
-    print("Bumped version from: %s --> %s" % (version, new_ver))
+    print(f"Bumped version from: {version} --> {new_ver}")
 
 
 @task(pre=['test'])

From 051e79d89e7fc12541eb59e2c6c88ae2a17208a3 Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <hugovk@users.noreply.github.com>
Date: Tue, 23 Nov 2021 17:37:58 +0200
Subject: [PATCH 08/12] Drop support for EOL Python 2.7

---
 docs/source/conf.py     |  1 -
 multipart/decoders.py   |  4 ++--
 multipart/exceptions.py | 12 ------------
 3 files changed, 2 insertions(+), 15 deletions(-)

diff --git a/docs/source/conf.py b/docs/source/conf.py
index 8851bc7..8f1e0ae 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -1,4 +1,3 @@
-#
 # python-multipart documentation build configuration file, created by
 # sphinx-quickstart on Fri Apr  5 20:24:27 2013.
 #
diff --git a/multipart/decoders.py b/multipart/decoders.py
index 6dac55e..0d7ab32 100644
--- a/multipart/decoders.py
+++ b/multipart/decoders.py
@@ -1,7 +1,7 @@
 import base64
 import binascii
 
-from .exceptions import Base64Error, DecodeError
+from .exceptions import DecodeError
 
 
 class Base64Decoder:
@@ -58,7 +58,7 @@ def write(self, data):
         if len(val) > 0:
             try:
                 decoded = base64.b64decode(val)
-            except Base64Error:
+            except binascii.Error:
                 raise DecodeError('There was an error raised while decoding '
                                   'base64-encoded data.')
 
diff --git a/multipart/exceptions.py b/multipart/exceptions.py
index 98c5867..3264b82 100644
--- a/multipart/exceptions.py
+++ b/multipart/exceptions.py
@@ -1,8 +1,3 @@
-import binascii
-
-from six import PY3
-
-
 class FormParserError(ValueError):
     """Base error class for our form parser."""
     pass
@@ -49,10 +44,3 @@ class FileError(FormParserError, IOError, OSError):
     class FileError(FormParserError, OSError):
         """Exception class for problems with the File class."""
         pass
-
-# We check which version of Python we're on to figure out what error we need
-# to catch for invalid Base64.
-if PY3:                         # pragma: no cover
-    Base64Error = binascii.Error
-else:                           # pragma: no cover
-    Base64Error = TypeError

From c54ad6006bacc77623864ec8e5c96bfd32230e01 Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <hugovk@users.noreply.github.com>
Date: Tue, 23 Nov 2021 17:40:08 +0200
Subject: [PATCH 09/12] Remove redundant six

---
 .coveragerc                       | 4 ----
 multipart/multipart.py            | 6 ------
 multipart/tests/test_multipart.py | 1 -
 requirements.txt                  | 1 -
 setup.py                          | 3 ---
 5 files changed, 15 deletions(-)

diff --git a/.coveragerc b/.coveragerc
index 685c6e2..0f1b93d 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -5,7 +5,6 @@
 branch = False
 # branch = True
 omit =
-    *six.py
     multipart/tests/*
 
 [report]
@@ -33,6 +32,3 @@ exclude_lines =
     # Ignore import exceptions
     except ImportError:
 
-    # Ignore python compatibility.
-    if PY3
-
diff --git a/multipart/multipart.py b/multipart/multipart.py
index 6fac9d1..e66c538 100644
--- a/multipart/multipart.py
+++ b/multipart/multipart.py
@@ -1,9 +1,3 @@
-from six import (
-    binary_type,
-    text_type,
-    PY3,
-)
-
 from .decoders import *
 from .exceptions import *
 
diff --git a/multipart/tests/test_multipart.py b/multipart/tests/test_multipart.py
index 7e49e16..811a640 100644
--- a/multipart/tests/test_multipart.py
+++ b/multipart/tests/test_multipart.py
@@ -12,7 +12,6 @@
     unittest,
 )
 from io import BytesIO
-from six import binary_type, text_type
 
 try:
     from unittest.mock import MagicMock, Mock, patch
diff --git a/requirements.txt b/requirements.txt
index 37dc501..b2392b7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,4 +8,3 @@ pluggy==1.0.0
 py==1.10.0
 pytest==6.2.5
 PyYAML==5.1
-six==1.11.0
diff --git a/setup.py b/setup.py
index dd7542e..6cc3a20 100755
--- a/setup.py
+++ b/setup.py
@@ -25,9 +25,6 @@
       license='Apache',
       platforms='any',
       zip_safe=False,
-      install_requires=[
-          'six>=1.4.0',
-      ],
       tests_require=tests_require,
       packages=[
           'multipart',

From e12bd75497683a23e42710b4918ac11f1ff9f4d7 Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <hugovk@users.noreply.github.com>
Date: Tue, 23 Nov 2021 17:43:15 +0200
Subject: [PATCH 10/12] Tidy Python 3+ imports

---
 docs_requirements.txt             | 1 -
 multipart/multipart.py            | 5 -----
 multipart/tests/compat.py         | 5 -----
 multipart/tests/test_multipart.py | 8 ++------
 requirements.txt                  | 1 -
 tox.ini                           | 1 -
 6 files changed, 2 insertions(+), 19 deletions(-)

diff --git a/docs_requirements.txt b/docs_requirements.txt
index 55b9125..9cebf18 100644
--- a/docs_requirements.txt
+++ b/docs_requirements.txt
@@ -7,7 +7,6 @@ coverage==3.6
 distribute==0.6.34
 docutils==0.10
 invoke==0.2.0
-mock==1.0.1
 pexpect-u==2.5.1
 py==1.10.0
 pytest==6.2.5
diff --git a/multipart/multipart.py b/multipart/multipart.py
index e66c538..df5d374 100644
--- a/multipart/multipart.py
+++ b/multipart/multipart.py
@@ -1,11 +1,6 @@
 from .decoders import *
 from .exceptions import *
 
-try:
-    from urlparse import parse_qs
-except ImportError:
-    from urllib.parse import parse_qs
-
 import os
 import re
 import sys
diff --git a/multipart/tests/compat.py b/multipart/tests/compat.py
index 0fb84c3..897188d 100644
--- a/multipart/tests/compat.py
+++ b/multipart/tests/compat.py
@@ -1,8 +1,3 @@
-try:
-    import unittest2 as unittest
-except ImportError:
-    import unittest
-
 import os
 import re
 import sys
diff --git a/multipart/tests/test_multipart.py b/multipart/tests/test_multipart.py
index 811a640..0f14d95 100644
--- a/multipart/tests/test_multipart.py
+++ b/multipart/tests/test_multipart.py
@@ -5,18 +5,14 @@
 import base64
 import random
 import tempfile
+import unittest
 from .compat import (
     parametrize,
     parametrize_class,
     slow_test,
-    unittest,
 )
 from io import BytesIO
-
-try:
-    from unittest.mock import MagicMock, Mock, patch
-except ImportError:
-    from unittest.mock import MagicMock, Mock, patch
+from unittest.mock import MagicMock, Mock, patch
 
 from ..multipart import *
 
diff --git a/requirements.txt b/requirements.txt
index b2392b7..82ac3e3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,6 @@
 atomicwrites==1.2.1
 attrs==19.2.0
 coverage==4.5.1
-mock==2.0.0
 more-itertools==4.3.0
 pbr==4.3.0
 pluggy==1.0.0
diff --git a/tox.ini b/tox.ini
index c62cdb1..2072d7f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,7 +6,6 @@ deps=
     pytest
     pytest-cov
     pytest-timeout
-    mock
     PyYAML
 commands=
     pytest --cov-report term-missing --cov-config .coveragerc --cov multipart --timeout=30 multipart/tests

From c37ac893fbe66ad3fa3dabc1fdefed35638b1ef4 Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <hugovk@users.noreply.github.com>
Date: Tue, 23 Nov 2021 17:58:52 +0200
Subject: [PATCH 11/12] http to https

---
 LICENSE.txt                                 | 2 +-
 README.rst                                  | 2 +-
 docs/Makefile                               | 2 +-
 docs/make.bat                               | 2 +-
 docs/source/_themes/flask/layout.html       | 2 +-
 docs/source/_themes/flask_small/layout.html | 4 ++--
 docs/source/conf.py                         | 2 +-
 setup.py                                    | 2 +-
 8 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/LICENSE.txt b/LICENSE.txt
index 7ff9d40..303a1bf 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -4,7 +4,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 
-   http://www.apache.org/licenses/LICENSE-2.0
+   https://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
diff --git a/README.rst b/README.rst
index aceb6e2..8f8521b 100644
--- a/README.rst
+++ b/README.rst
@@ -10,7 +10,7 @@ python-multipart is an Apache2 licensed streaming multipart parser for Python.
 Test coverage is currently 100%.
 Documentation is available `here`_.
 
-.. _here: http://andrew-d.github.io/python-multipart/
+.. _here: https://andrew-d.github.io/python-multipart/
 
 Why?
 ----
diff --git a/docs/Makefile b/docs/Makefile
index 9a2fbbd..c045a56 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -9,7 +9,7 @@ BUILDDIR      = build
 
 # User-friendly check for sphinx-build
 ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
-$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://sphinx-doc.org/)
 endif
 
 # Internal variables.
diff --git a/docs/make.bat b/docs/make.bat
index ce7daf8..10fdc25 100644
--- a/docs/make.bat
+++ b/docs/make.bat
@@ -56,7 +56,7 @@ if errorlevel 9009 (
 	echo.may add the Sphinx directory to PATH.
 	echo.
 	echo.If you don't have Sphinx installed, grab it from
-	echo.http://sphinx-doc.org/
+	echo.https://sphinx-doc.org/
 	exit /b 1
 )
 
diff --git a/docs/source/_themes/flask/layout.html b/docs/source/_themes/flask/layout.html
index 5caa4e2..6a80763 100644
--- a/docs/source/_themes/flask/layout.html
+++ b/docs/source/_themes/flask/layout.html
@@ -17,7 +17,7 @@
 {%- block footer %}
   <div class="footer">
     &copy; Copyright {{ copyright }}.
-    Created using <a href="http://sphinx.pocoo.org/">Sphinx</a>.
+    Created using <a href="https://www.sphinx-doc.org/">Sphinx</a>.
   </div>
   {% if pagename == 'index' %}
   </div>
diff --git a/docs/source/_themes/flask_small/layout.html b/docs/source/_themes/flask_small/layout.html
index aa1716a..83e5213 100644
--- a/docs/source/_themes/flask_small/layout.html
+++ b/docs/source/_themes/flask_small/layout.html
@@ -14,8 +14,8 @@
 {% block relbar1 %}{% endblock %}
 {% block relbar2 %}
   {% if theme_github_fork %}
-    <a href="http://github.com/{{ theme_github_fork }}"><img style="position: fixed; top: 0; right: 0; border: 0;"
-    src="http://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub" /></a>
+    <a href="https://github.com/{{ theme_github_fork }}"><img style="position: fixed; top: 0; right: 0; border: 0;"
+    src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub" /></a>
   {% endif %}
 {% endblock %}
 {% block sidebar1 %}{% endblock %}
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 8f1e0ae..d13072e 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -253,4 +253,4 @@
 
 
 # Example configuration for intersphinx: refer to the Python standard library.
-intersphinx_mapping = {'http://docs.python.org/': None}
+intersphinx_mapping = {'https://docs.python.org/': None}
diff --git a/setup.py b/setup.py
index 6cc3a20..fa45e1d 100755
--- a/setup.py
+++ b/setup.py
@@ -21,7 +21,7 @@
       version=version,
       description='A streaming multipart parser for Python',
       author='Andrew Dunham',
-      url='http://github.com/andrew-d/python-multipart',
+      url='https://github.com/andrew-d/python-multipart',
       license='Apache',
       platforms='any',
       zip_safe=False,

From 98c852f926ad592fe88220dde5eff4f8955e9cde Mon Sep 17 00:00:00 2001
From: Tom Christie <tom@tomchristie.com>
Date: Thu, 3 Nov 2022 11:54:55 +0000
Subject: [PATCH 12/12] Update docs_requirements.txt

---
 docs_requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs_requirements.txt b/docs_requirements.txt
index 9cebf18..ee47591 100644
--- a/docs_requirements.txt
+++ b/docs_requirements.txt
@@ -9,7 +9,7 @@ docutils==0.10
 invoke==0.2.0
 pexpect-u==2.5.1
 py==1.10.0
-pytest==6.2.5
+pytest==6.2.6
 pytest-capturelog==0.7
 pytest-cov==1.6
 pytest-timeout==0.3