Repository: twisted/twisted

Issue:
drop support for eol python 3.7
**Is your feature request related to a problem? Please describe.**
3.7 is eol we should no longer support it

**Describe the solution you'd like**
drop support for 3.7



Discussion:

Resolving patch:
diff --git a/src/twisted/internet/defer.py b/src/twisted/internet/defer.py
--- a/src/twisted/internet/defer.py
+++ b/src/twisted/internet/defer.py
@@ -14,6 +14,7 @@
 import warnings
 from abc import ABC, abstractmethod
 from asyncio import AbstractEventLoop, Future, iscoroutine
+from contextvars import Context as _Context, copy_context as _copy_context
 from enum import Enum
 from functools import wraps
 from sys import exc_info
@@ -42,7 +43,7 @@
 
 import attr
 from incremental import Version
-from typing_extensions import Concatenate, Literal, ParamSpec, Protocol
+from typing_extensions import Concatenate, Literal, ParamSpec
 
 from twisted.internet.interfaces import IDelayedCall, IReactorTime
 from twisted.logger import Logger
@@ -51,32 +52,6 @@
 from twisted.python.deprecate import deprecated, warnAboutFunction
 from twisted.python.failure import Failure, _extraneous
 
-
-class _Context(Protocol):
-    def run(self, f: Callable[..., object], *args: object, **kwargs: object) -> object:
-        ...
-
-
-try:
-    from contextvars import copy_context as __copy_context
-
-    _contextvarsSupport = True
-
-except ImportError:
-    _contextvarsSupport = False
-
-    class _NoContext:
-        @staticmethod
-        def run(f: Callable[..., object], *args: object, **kwargs: object) -> object:
-            return f(*args, **kwargs)
-
-    def _copy_context() -> Type[_NoContext]:
-        return _NoContext
-
-
-else:
-    _copy_context = __copy_context  # type: ignore[assignment]
-
 log = Logger()
 
 
diff --git a/src/twisted/python/compat.py b/src/twisted/python/compat.py
--- a/src/twisted/python/compat.py
+++ b/src/twisted/python/compat.py
@@ -26,7 +26,6 @@
 import os
 import platform
 import socket
-import sys
 import urllib.parse as urllib_parse
 import warnings
 from collections.abc import Sequence
@@ -43,11 +42,6 @@
 
 from twisted.python.deprecate import deprecated, deprecatedModuleAttribute
 
-if sys.version_info >= (3, 7, 0):
-    _PY37PLUS = True
-else:
-    _PY37PLUS = False
-
 if platform.python_implementation() == "PyPy":
     _PYPY = True
 else:
diff --git a/src/twisted/web/http.py b/src/twisted/web/http.py
--- a/src/twisted/web/http.py
+++ b/src/twisted/web/http.py
@@ -126,7 +126,7 @@
 from twisted.logger import Logger
 from twisted.protocols import basic, policies
 from twisted.python import log
-from twisted.python.compat import _PY37PLUS, nativeString, networkString
+from twisted.python.compat import nativeString, networkString
 from twisted.python.components import proxyForInterface
 from twisted.python.deprecate import deprecated
 from twisted.python.failure import Failure
@@ -983,41 +983,29 @@ def requestReceived(self, command, path, version):
                 args.update(parse_qs(self.content.read(), 1))
             elif key == mfd:
                 try:
-                    if _PY37PLUS:
-                        cgiArgs = cgi.parse_multipart(
-                            self.content,
-                            pdict,
-                            encoding="utf8",
-                            errors="surrogateescape",
-                        )
-                    else:
-                        cgiArgs = cgi.parse_multipart(self.content, pdict)
-
-                    if _PY37PLUS:
-                        # The parse_multipart function on Python 3.7+
-                        # decodes the header bytes as iso-8859-1 and
-                        # decodes the body bytes as utf8 with
-                        # surrogateescape -- we want bytes
-                        self.args.update(
-                            {
-                                x.encode("iso-8859-1"): [
-                                    z.encode("utf8", "surrogateescape")
-                                    if isinstance(z, str)
-                                    else z
-                                    for z in y
-                                ]
-                                for x, y in cgiArgs.items()
-                                if isinstance(x, str)
-                            }
-                        )
-                    else:
-                        # The parse_multipart function on Python 3
-                        # decodes the header bytes as iso-8859-1 and
-                        # returns a str key -- we want bytes so encode
-                        # it back
-                        self.args.update(
-                            {x.encode("iso-8859-1"): y for x, y in cgiArgs.items()}
-                        )
+                    cgiArgs = cgi.parse_multipart(
+                        self.content,
+                        pdict,
+                        encoding="utf8",
+                        errors="surrogateescape",
+                    )
+
+                    # The parse_multipart function on Python 3.7+
+                    # decodes the header bytes as iso-8859-1 and
+                    # decodes the body bytes as utf8 with
+                    # surrogateescape -- we want bytes
+                    self.args.update(
+                        {
+                            x.encode("iso-8859-1"): [
+                                z.encode("utf8", "surrogateescape")
+                                if isinstance(z, str)
+                                else z
+                                for z in y
+                            ]
+                            for x, y in cgiArgs.items()
+                            if isinstance(x, str)
+                        }
+                    )
                 except Exception as e:
                     # It was a bad request, or we got a signal.
                     self.channel._respondToBadRequestAndDisconnect()


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -103,7 +103,7 @@ jobs:
           # end to end functional test usage for non-distributed trial runs.
           # When updating the minimum Python version here, also update the
           # `python_requires` from `setup.cfg`.
-          - python-version: '3.7.1'
+          - python-version: '3.8.0'
             tox-env: 'nodeps-withcov-posix'
             job-name: 'nodeps-test'
 
@@ -113,7 +113,7 @@ jobs:
           # We go with latest micro release for the smallest minor release
           # that we support.
           # This is also used to run tests with minimum dependency versions.
-          - python-version: '3.7'
+          - python-version: '3.8'
             noipv6: '-noipv6'
             tox-env: 'mindeps-withcov-posix'
             job-name: 'minimum-deps-noipv6'
@@ -137,7 +137,7 @@ jobs:
             tox-env: 'macos-withcov-alldeps'
 
           # Windows, minimum Python version with select reactor.
-          - python-version: '3.7'
+          - python-version: '3.8'
             runs-on: 'windows-2022'
             tox-env: 'alldeps-withcov-windows'
             job-name: 'win-default-tests-select'
@@ -172,7 +172,7 @@ jobs:
           # We run the full test suite with the GI reactor against an X virtual
           # framebuffer server.  This covers our integration with the version
           # of Gtk packaged within our selected version of Linux above.
-          - python-version: '3.7'
+          - python-version: '3.8'
             tox-env: 'alldeps-gtk-withcov-posix'
             platform-deps: 'gtk-platform'
             tox-wrapper: 'xvfb-run -a'
diff --git a/src/twisted/conch/test/test_filetransfer.py b/src/twisted/conch/test/test_filetransfer.py
--- a/src/twisted/conch/test/test_filetransfer.py
+++ b/src/twisted/conch/test/test_filetransfer.py
@@ -19,7 +19,6 @@
 from twisted.internet.testing import StringTransport
 from twisted.protocols import loopback
 from twisted.python import components
-from twisted.python.compat import _PY37PLUS
 from twisted.python.filepath import FilePath
 from twisted.trial.unittest import TestCase
 
@@ -532,48 +531,6 @@ def _cbTestExtendedRequest(self, ignored):
         self._emptyBuffers()
         return self.assertFailure(d, NotImplementedError)
 
-    @defer.inlineCallbacks
-    @skipIf(_PY37PLUS, "Broken by PEP 479 and deprecated.")
-    def test_openDirectoryIterator(self):
-        """
-        Check that the object returned by
-        L{filetransfer.FileTransferClient.openDirectory} can be used
-        as an iterator.
-        """
-
-        # This function is a little more complicated than it would be
-        # normally, since we need to call _emptyBuffers() after
-        # creating any SSH-related Deferreds, but before waiting on
-        # them via yield.
-
-        d = self.client.openDirectory(b"")
-        self._emptyBuffers()
-        openDir = yield d
-
-        filenames = set()
-        try:
-            for f in openDir:
-                self._emptyBuffers()
-                (filename, _, fileattrs) = yield f
-                filenames.add(filename)
-        finally:
-            d = openDir.close()
-            self._emptyBuffers()
-            yield d
-
-        self._emptyBuffers()
-
-        self.assertEqual(
-            filenames,
-            {
-                b".testHiddenFile",
-                b"testDirectory",
-                b"testRemoveFile",
-                b"testRenameFile",
-                b"testfile1",
-            },
-        )
-
     @defer.inlineCallbacks
     def test_openDirectoryIteratorDeprecated(self):
         """
diff --git a/src/twisted/test/test_defer.py b/src/twisted/test/test_defer.py
--- a/src/twisted/test/test_defer.py
+++ b/src/twisted/test/test_defer.py
@@ -7,6 +7,7 @@
 
 from __future__ import annotations
 
+import contextvars
 import functools
 import gc
 import re
@@ -22,7 +23,6 @@
     new_event_loop as _new_event_loop,
 )
 from typing import (
-    TYPE_CHECKING,
     Any,
     Callable,
     Coroutine,
@@ -64,14 +64,6 @@
 from twisted.python.failure import Failure
 from twisted.trial import unittest
 
-if TYPE_CHECKING:
-    import contextvars
-else:
-    try:
-        import contextvars
-    except ImportError:
-        contextvars = None
-
 
 def ensuringDeferred(
     f: Callable[..., Coroutine[Deferred[_T], Any, _T]]
@@ -3718,9 +3710,6 @@ def test_fromFutureDeferredCancelled(self) -> None:
 
 
 class CoroutineContextVarsTests(unittest.TestCase):
-    if contextvars is None:
-        skip = "contextvars is not available"  # type: ignore[unreachable]
-
     def test_withInlineCallbacks(self) -> None:
         """
         When an inlineCallbacks function is called, the context is taken from