Skip to content

Commit

Permalink
Fixes Python 3.12 compatilibity #817
Browse files Browse the repository at this point in the history
* Update unittest asserts to support Python 3.12+

Related to python/cpython#89325

* Update green.http.client for changes in Python 3.12

Related to python/cpython@f0b234e

Co-Authored-By: Victor Stinner <vstinner@python.org>

* Set green.thred.daemon_threads_allowed for Python 3.12+ support

Related to python/cpython@4702552

* Python 3.12+ only: Adjust for removal of ssl.wrap_socket()

Related to python/cpython@00464bb

* Python 3.12 fixes by hroncok

This PR attempts to add compatibility for older Python versions.

We draw the line at Python versions that predate ssl.SSLContext, though. The
remaining Python 2.7 documentation doesn't even mention the version at which
that was introduced.

* Allow access to global __ssl within class definition.

* Remember whether original ssl module has wrap_socket() function.

* Replace a few assertTrue() calls with assert statements.

* In GreenSSLSocket.__new__(), use cls, not self.

* Add 3.12

* Add newer versions

* Warnings don't mean the test should fail

* Fix syntax error

* Style fixes

* Simplify given we only support 3.8+

* Document 3.12 support

---------

Co-authored-by: Miro Hrončok <miro@hroncok.cz>
Co-authored-by: Victor Stinner <vstinner@python.org>
Co-authored-by: Itamar Turner-Trauring <itamar@itamarst.org>
Co-authored-by: Itamar Turner-Trauring <itamar@pythonspeed.com>
  • Loading branch information
5 people committed Dec 15, 2023
1 parent d78f8f6 commit e51f72a
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 153 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test.yaml
Expand Up @@ -42,6 +42,7 @@ jobs:
- { py: "3.10", toxenv: py310-selects, ignore-error: false, os: ubuntu-latest }
- { py: "3.10", toxenv: ipv6, ignore-error: false, os: ubuntu-latest }
- { py: "3.11", toxenv: py311-epolls, ignore-error: false, os: ubuntu-latest }
- { py: "3.12", toxenv: py312-epolls, ignore-error: false, os: ubuntu-latest }
- { py: pypy3.9, toxenv: pypy3-epolls, ignore-error: true, os: ubuntu-20.04 }

steps:
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Expand Up @@ -65,7 +65,7 @@ Apologies for any inconvenience.
Supported Python versions
=========================

Python 3.8-3.11 are currently supported.
Python 3.8-3.12 are currently supported.

Flair
=====
Expand Down
29 changes: 20 additions & 9 deletions eventlet/green/http/client.py
Expand Up @@ -74,13 +74,13 @@
| ( putheader() )* endheaders()
v
Request-sent
|\_____________________________
|\\_____________________________
| | getresponse() raises
| response = getresponse() | ConnectionError
v v
Unread-response Idle
[Response-headers-read]
|\____________________
|\\____________________
| |
| response.read() | putrequest()
v v
Expand Down Expand Up @@ -1450,6 +1450,22 @@ def getresponse(self):
except ImportError:
pass
else:
def _create_https_context(http_version):
# Function also used by urllib.request to be able to set the check_hostname
# attribute on a context object.
context = ssl._create_default_https_context()
# send ALPN extension to indicate HTTP/1.1 protocol
if http_version == 11:
context.set_alpn_protocols(['http/1.1'])
# enable PHA for TLS 1.3 connections if available
if context.post_handshake_auth is not None:
context.post_handshake_auth = True
return context

def _populate_https_context(context, check_hostname):
if check_hostname is not None:
context.check_hostname = check_hostname

class HTTPSConnection(HTTPConnection):
"This class allows communication via SSL."

Expand All @@ -1466,13 +1482,8 @@ def __init__(self, host, port=None, key_file=None, cert_file=None,
self.key_file = key_file
self.cert_file = cert_file
if context is None:
context = ssl._create_default_https_context()
will_verify = context.verify_mode != ssl.CERT_NONE
if check_hostname is None:
check_hostname = context.check_hostname
if check_hostname and not will_verify:
raise ValueError("check_hostname needs a SSL context with "
"either CERT_OPTIONAL or CERT_REQUIRED")
context = _create_https_context(self._http_vsn)
_populate_https_context(context, check_hostname)
if key_file or cert_file:
context.load_cert_chain(cert_file, key_file)
self._context = context
Expand Down
139 changes: 80 additions & 59 deletions eventlet/green/ssl.py
Expand Up @@ -6,7 +6,7 @@
import sys
from eventlet import greenio, hubs
from eventlet.greenio import (
set_nonblocking, GreenSocket, CONNECT_ERR, CONNECT_SUCCESS,
GreenSocket, CONNECT_ERR, CONNECT_SUCCESS,
)
from eventlet.hubs import trampoline, IOClosed
from eventlet.support import get_errno, PY33
Expand All @@ -22,9 +22,9 @@
'create_default_context', '_create_default_https_context']

_original_sslsocket = __ssl.SSLSocket
_original_wrap_socket = __ssl.wrap_socket
_original_sslcontext = getattr(__ssl, 'SSLContext', None)
_original_sslcontext = __ssl.SSLContext
_is_under_py_3_7 = sys.version_info < (3, 7)
_original_wrap_socket = __ssl.SSLContext.wrap_socket


@contextmanager
Expand Down Expand Up @@ -76,7 +76,7 @@ def __new__(cls, sock=None, keyfile=None, certfile=None,
session=kw.get('session'),
)
else:
ret = _original_wrap_socket(
ret = cls._wrap_socket(
sock=sock.fd,
keyfile=keyfile,
certfile=certfile,
Expand All @@ -95,6 +95,26 @@ def __new__(cls, sock=None, keyfile=None, certfile=None,
ret.__class__ = GreenSSLSocket
return ret

@staticmethod
def _wrap_socket(sock, keyfile, certfile, server_side, cert_reqs,
ssl_version, ca_certs, do_handshake_on_connect, ciphers):
context = _original_sslcontext(protocol=ssl_version)
context.options |= cert_reqs
if certfile or keyfile:
context.load_cert_chain(
certfile=certfile,
keyfile=keyfile,
)
if ca_certs:
context.load_verify_locations(ca_certs)
if ciphers:
context.set_ciphers(ciphers)
return context.wrap_socket(
sock=sock,
server_side=server_side,
do_handshake_on_connect=do_handshake_on_connect,
)

# we are inheriting from SSLSocket because its constructor calls
# do_handshake whose behavior we wish to override
def __init__(self, sock, keyfile=None, certfile=None,
Expand Down Expand Up @@ -436,58 +456,59 @@ def sslwrap_simple(sock, keyfile=None, certfile=None):
return ssl_sock


if hasattr(__ssl, 'SSLContext'):
_original_sslcontext = __ssl.SSLContext

class GreenSSLContext(_original_sslcontext):
__slots__ = ()

def wrap_socket(self, sock, *a, **kw):
return GreenSSLSocket(sock, *a, _context=self, **kw)

# https://github.com/eventlet/eventlet/issues/371
# Thanks to Gevent developers for sharing patch to this problem.
if hasattr(_original_sslcontext.options, 'setter'):
# In 3.6, these became properties. They want to access the
# property __set__ method in the superclass, and they do so by using
# super(SSLContext, SSLContext). But we rebind SSLContext when we monkey
# patch, which causes infinite recursion.
# https://github.com/python/cpython/commit/328067c468f82e4ec1b5c510a4e84509e010f296
@_original_sslcontext.options.setter
def options(self, value):
super(_original_sslcontext, _original_sslcontext).options.__set__(self, value)

@_original_sslcontext.verify_flags.setter
def verify_flags(self, value):
super(_original_sslcontext, _original_sslcontext).verify_flags.__set__(self, value)

@_original_sslcontext.verify_mode.setter
def verify_mode(self, value):
super(_original_sslcontext, _original_sslcontext).verify_mode.__set__(self, value)

if hasattr(_original_sslcontext, "maximum_version"):
@_original_sslcontext.maximum_version.setter
def maximum_version(self, value):
super(_original_sslcontext, _original_sslcontext).maximum_version.__set__(self, value)

if hasattr(_original_sslcontext, "minimum_version"):
@_original_sslcontext.minimum_version.setter
def minimum_version(self, value):
super(_original_sslcontext, _original_sslcontext).minimum_version.__set__(self, value)

SSLContext = GreenSSLContext

if hasattr(__ssl, 'create_default_context'):
_original_create_default_context = __ssl.create_default_context

def green_create_default_context(*a, **kw):
# We can't just monkey-patch on the green version of `wrap_socket`
# on to SSLContext instances, but SSLContext.create_default_context
# does a bunch of work. Rather than re-implementing it all, just
# switch out the __class__ to get our `wrap_socket` implementation
context = _original_create_default_context(*a, **kw)
context.__class__ = GreenSSLContext
return context

create_default_context = green_create_default_context
_create_default_https_context = green_create_default_context
class GreenSSLContext(_original_sslcontext):
__slots__ = ()

def wrap_socket(self, sock, *a, **kw):
return GreenSSLSocket(sock, *a, _context=self, **kw)

# https://github.com/eventlet/eventlet/issues/371
# Thanks to Gevent developers for sharing patch to this problem.
if hasattr(_original_sslcontext.options, 'setter'):
# In 3.6, these became properties. They want to access the
# property __set__ method in the superclass, and they do so by using
# super(SSLContext, SSLContext). But we rebind SSLContext when we monkey
# patch, which causes infinite recursion.
# https://github.com/python/cpython/commit/328067c468f82e4ec1b5c510a4e84509e010f296
@_original_sslcontext.options.setter
def options(self, value):
super(_original_sslcontext, _original_sslcontext).options.__set__(self, value)

@_original_sslcontext.verify_flags.setter
def verify_flags(self, value):
super(_original_sslcontext, _original_sslcontext).verify_flags.__set__(self, value)

@_original_sslcontext.verify_mode.setter
def verify_mode(self, value):
super(_original_sslcontext, _original_sslcontext).verify_mode.__set__(self, value)

if hasattr(_original_sslcontext, "maximum_version"):
@_original_sslcontext.maximum_version.setter
def maximum_version(self, value):
super(_original_sslcontext, _original_sslcontext).maximum_version.__set__(self, value)

if hasattr(_original_sslcontext, "minimum_version"):
@_original_sslcontext.minimum_version.setter
def minimum_version(self, value):
super(_original_sslcontext, _original_sslcontext).minimum_version.__set__(self, value)


SSLContext = GreenSSLContext


# TODO: ssl.create_default_context() was added in 2.7.9.
# Not clear we're still trying to support Python versions even older than that.
if hasattr(__ssl, 'create_default_context'):
_original_create_default_context = __ssl.create_default_context

def green_create_default_context(*a, **kw):
# We can't just monkey-patch on the green version of `wrap_socket`
# on to SSLContext instances, but SSLContext.create_default_context
# does a bunch of work. Rather than re-implementing it all, just
# switch out the __class__ to get our `wrap_socket` implementation
context = _original_create_default_context(*a, **kw)
context.__class__ = GreenSSLContext
return context

create_default_context = green_create_default_context
_create_default_https_context = green_create_default_context
3 changes: 3 additions & 0 deletions eventlet/green/thread.py
Expand Up @@ -113,3 +113,6 @@ def stack_size(size=None):
# this thread will suffer

from eventlet.corolocal import local as _local

if hasattr(__thread, 'daemon_threads_allowed'):
daemon_threads_allowed = __thread.daemon_threads_allowed
1 change: 1 addition & 0 deletions setup.py
Expand Up @@ -43,6 +43,7 @@
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python",
"Topic :: Internet",
"Topic :: Software Development :: Libraries :: Python Modules",
Expand Down
5 changes: 3 additions & 2 deletions tests/__init__.py
Expand Up @@ -357,8 +357,9 @@ def run_python(path, env=None, args=None, timeout=None, pythonpath_extend=None,
if len(parts) > 1:
skip_args.append(parts[1])
raise SkipTest(*skip_args)
ok = output.rstrip() == b'pass'
if not ok:
lines = output.splitlines()
ok = lines[-1].rstrip() == b'pass'
if not ok or len(lines) > 1:
sys.stderr.write('Program {0} output:\n---\n{1}\n---\n'.format(path, output.decode()))
assert ok, 'Expected single line "pass" in stdout'

Expand Down

0 comments on commit e51f72a

Please sign in to comment.