summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHelmut Grohne <helmut@subdivi.de>2023-06-17 19:35:21 +0200
committerHelmut Grohne <helmut@subdivi.de>2023-06-18 22:44:01 +0200
commit4d52eaa4801df3f3169df8e58758bcccf22dc4de (patch)
treeb8740a88e380a750d9d2607bb39cbc759a8d7175
parent682ce67b73453809237532a7ce2feee07f2900d5 (diff)
downloadwsgitools-4d52eaa4801df3f3169df8e58758bcccf22dc4de.tar.gz
drop support for Python 2.x
-rw-r--r--README3
-rw-r--r--pyproject.toml2
-rwxr-xr-xsetup.py2
-rwxr-xr-xtest.py21
-rw-r--r--wsgitools/applications.py19
-rw-r--r--wsgitools/authentication.py2
-rw-r--r--wsgitools/digest.py9
-rw-r--r--wsgitools/filters.py11
-rw-r--r--wsgitools/internal.py30
-rw-r--r--wsgitools/middlewares.py28
-rw-r--r--wsgitools/scgi/__init__.py4
-rw-r--r--wsgitools/scgi/asynchronous.py9
-rw-r--r--wsgitools/scgi/forkpool.py22
13 files changed, 58 insertions, 104 deletions
diff --git a/README b/README
index 2d70e77..1ad2a89 100644
--- a/README
+++ b/README
@@ -2,7 +2,8 @@ The software should be usable by reading the docstrings. If you think that
certain features are missing or you found a bug, don't hesitate to ask me
via mail!
-Supported Python versions currently are 2.7 and >= 3.5.
+Supported Python versions currently are >= 3.5 and <= 3.11. 3.12 will be
+degraded, due to use of deprecated modules.
Installation should be easy using setup.py. I recommend running the test suite
by invoking "python test.py" from the source tree to spot problems early.
diff --git a/pyproject.toml b/pyproject.toml
index 18abc10..3ee9835 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -25,7 +25,7 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware",
"Topic :: Internet :: WWW/HTTP :: WSGI :: Server",
]
-requires-python = ">=2.7"
+requires-python = ">=3.5"
version="0.3.1"
[tool.pylint]
diff --git a/setup.py b/setup.py
index 698f8b1..e07de91 100755
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
from setuptools import setup
diff --git a/test.py b/test.py
index c9d97f1..9690baf 100755
--- a/test.py
+++ b/test.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
import base64
import unittest
@@ -11,7 +11,7 @@ import sys
from wsgitools.internal import bytes2str, str2bytes
-class Request(object):
+class Request:
def __init__(self, case):
"""
@type case: unittest.TestCase
@@ -77,7 +77,7 @@ class Request(object):
iterator.close()
return res
-class Result(object):
+class Result:
def __init__(self, case):
"""
@type case: unittest.TestCase
@@ -202,7 +202,7 @@ class AuthDigestMiddlewareTest(unittest.TestCase):
nonce = nonce.split('"')[1]
req = self.req.copy()
token = md5(str2bytes("bar:foo:%s" % password)).hexdigest()
- other = md5(str2bytes("GET:")).hexdigest()
+ other = md5(b"GET:").hexdigest()
resp = md5(str2bytes("%s:%s:%s" % (token, nonce, other))).hexdigest()
req.setheader('Authorization', 'Digest algorithm=md5,nonce="%s",' \
'uri=,username=bar,response="%s"' % (nonce, resp))
@@ -221,8 +221,8 @@ class AuthDigestMiddlewareTest(unittest.TestCase):
res.getheader("WWW-Authenticate").split())))
nonce = nonce.split('"')[1]
req = self.req.copy()
- token = md5(str2bytes("bar:foo:baz")).hexdigest()
- other = md5(str2bytes("GET:")).hexdigest()
+ token = md5(b"bar:foo:baz").hexdigest()
+ other = md5(b"GET:").hexdigest()
resp = "%s:%s:1:qux:auth:%s" % (token, nonce, other)
resp = md5(str2bytes(resp)).hexdigest()
req.setheader('Authorization', 'Digest algorithm=md5,nonce="%s",' \
@@ -259,7 +259,7 @@ class NoWriteCallableMiddlewareTest(unittest.TestCase):
self.assertEqual(res.writtendata, [])
self.assertEqual(b"".join(res.returneddata), b"firstsecond")
-class StupidIO(object):
+class StupidIO:
"""file-like without tell method, so StaticFile is not able to
determine the content-length."""
def __init__(self, content):
@@ -317,7 +317,7 @@ class CachingMiddlewareTest(unittest.TestCase):
if "maxage0" in environ["SCRIPT_NAME"]:
headers.append(("Cache-Control", "max-age=0"))
start_response("200 Found", headers)
- return [str2bytes("%d" % count)]
+ return [b"%d" % count]
def testCache(self):
res = Request(self)(self.cached)
@@ -385,10 +385,7 @@ class RequestLogWSGIFilterTest(unittest.TestCase):
def testSimple(self):
app = applications.StaticContent("200 Found",
[("Content-Type", "text/plain")], b"nothing")
- if isinstance("x", bytes):
- log = io.BytesIO()
- else:
- log = io.StringIO()
+ log = io.StringIO()
logfilter = filters.RequestLogWSGIFilter.creator(log)
app = filters.WSGIFilterMiddleware(app, logfilter)
req = Request(self)
diff --git a/wsgitools/applications.py b/wsgitools/applications.py
index df304db..9894cf8 100644
--- a/wsgitools/applications.py
+++ b/wsgitools/applications.py
@@ -2,13 +2,8 @@ import os.path
__all__ = []
-try:
- basestring
-except NameError:
- basestring = str
-
__all__.append("StaticContent")
-class StaticContent(object):
+class StaticContent:
"""
This wsgi application provides static content on whatever request it
receives with method GET or HEAD (content stripped). If not present, a
@@ -60,7 +55,7 @@ class StaticContent(object):
return self.content
__all__.append("StaticFile")
-class StaticFile(object):
+class StaticFile:
"""
This wsgi application provides the content of a static file on whatever
request it receives with method GET or HEAD (content stripped). If not
@@ -94,7 +89,7 @@ class StaticFile(object):
if not data:
break
yield data
- if isinstance(self.filelike, basestring):
+ if isinstance(self.filelike, str):
stream.close()
def __call__(self, environ, start_response):
@@ -110,7 +105,7 @@ class StaticFile(object):
stream = None
size = -1
try:
- if isinstance(self.filelike, basestring):
+ if isinstance(self.filelike, str):
# raises IOError
stream = open(self.filelike, "rb")
size = os.path.getsize(self.filelike)
@@ -133,16 +128,16 @@ class StaticFile(object):
start_response(self.status, headers)
if environ["REQUEST_METHOD"].upper() == "HEAD":
- if isinstance(self.filelike, basestring):
+ if isinstance(self.filelike, str):
stream.close()
return []
- if isinstance(self.filelike, basestring) and 'wsgi.file_wrapper' in environ:
+ if isinstance(self.filelike, str) and 'wsgi.file_wrapper' in environ:
return environ['wsgi.file_wrapper'](stream, self.blocksize)
if 0 <= size <= self.blocksize:
data = stream.read(size)
- if isinstance(self.filelike, basestring):
+ if isinstance(self.filelike, str):
stream.close()
return [data]
return self._serve_in_chunks(stream)
diff --git a/wsgitools/authentication.py b/wsgitools/authentication.py
index 59747e0..c076d7f 100644
--- a/wsgitools/authentication.py
+++ b/wsgitools/authentication.py
@@ -9,7 +9,7 @@ class AuthenticationRequired(Exception):
class ProtocolViolation(AuthenticationRequired):
pass
-class AuthenticationMiddleware(object):
+class AuthenticationMiddleware:
"""Base class for HTTP authorization schemes.
@cvar authorization_method: the implemented Authorization method. It will
diff --git a/wsgitools/digest.py b/wsgitools/digest.py
index 5b101e5..6eb4cb3 100644
--- a/wsgitools/digest.py
+++ b/wsgitools/digest.py
@@ -47,8 +47,7 @@ def gen_rand_str(bytesentropy=33):
True
"""
randnum = randbits(bytesentropy*8)
- randstr = ("%%0%dX" % (2*bytesentropy)) % randnum
- randbytes = str2bytes(randstr)
+ randbytes = (b"%%0%dX" % (2*bytesentropy)) % randnum
randbytes = base64.b16decode(randbytes)
randbytes = base64.b64encode(randbytes)
randstr = bytes2str(randbytes)
@@ -146,7 +145,7 @@ class StaleNonce(AuthenticationRequired):
pass
__all__.append("AbstractTokenGenerator")
-class AbstractTokenGenerator(object):
+class AbstractTokenGenerator:
"""Interface class for generating authentication tokens for
L{AuthDigestMiddleware}.
@@ -300,7 +299,7 @@ class UpdatingHtdigestTokenGenerator(HtdigestTokenGenerator):
return HtdigestTokenGenerator.__call__(self, user, algo)
__all__.append("NonceStoreBase")
-class NonceStoreBase(object):
+class NonceStoreBase:
"""Nonce storage interface."""
def __init__(self):
pass
@@ -516,7 +515,7 @@ class MemoryNonceStore(NonceStoreBase):
return True
__all__.append("LazyDBAPI2Opener")
-class LazyDBAPI2Opener(object):
+class LazyDBAPI2Opener:
"""
Connects to database on first request. Otherwise it behaves like a dbapi2
connection. This may be usefull in combination with L{scgi.forkpool},
diff --git a/wsgitools/filters.py b/wsgitools/filters.py
index 2a97066..7f8543d 100644
--- a/wsgitools/filters.py
+++ b/wsgitools/filters.py
@@ -15,7 +15,7 @@ import io
from wsgitools.internal import str2bytes
__all__.append("CloseableIterator")
-class CloseableIterator(object):
+class CloseableIterator:
"""Concatenating iterator with close attribute."""
def __init__(self, close_function, *iterators):
"""If close_function is not C{None}, it will be the C{close} attribute
@@ -40,8 +40,7 @@ class CloseableIterator(object):
except StopIteration:
self.iterators.pop(0)
return next(self)
- def next(self):
- return self.__next__()
+
__all__.append("CloseableList")
class CloseableList(list):
@@ -61,7 +60,7 @@ class CloseableList(list):
list.__iter__(self))
__all__.append("BaseWSGIFilter")
-class BaseWSGIFilter(object):
+class BaseWSGIFilter:
"""Generic WSGI filter class to be used with L{WSGIFilterMiddleware}.
For each request a filter object gets created.
@@ -138,7 +137,7 @@ class BaseWSGIFilter(object):
"""This method is invoked after the request has finished."""
__all__.append("WSGIFilterMiddleware")
-class WSGIFilterMiddleware(object):
+class WSGIFilterMiddleware:
"""This wsgi middleware can be used with specialized L{BaseWSGIFilter}s to
modify wsgi requests and/or reponses."""
def __init__(self, app, filterclass):
@@ -322,7 +321,7 @@ class TimerWSGIFilter(BaseWSGIFilter):
@rtype: bytes
"""
if data == self.pattern:
- return str2bytes("%8.3g" % (time.time() - self.start))
+ return b"%8.3g" % (time.time() - self.start)
return data
__all__.append("EncodeWSGIFilter")
diff --git a/wsgitools/internal.py b/wsgitools/internal.py
index c4f1da1..9bf7ded 100644
--- a/wsgitools/internal.py
+++ b/wsgitools/internal.py
@@ -1,19 +1,11 @@
-if bytes is str:
- def bytes2str(bstr):
- assert isinstance(bstr, bytes)
- return bstr
- def str2bytes(sstr):
- assert isinstance(sstr, str)
- return sstr
- def textopen(filename, mode):
- return open(filename, mode)
-else:
- def bytes2str(bstr):
- assert isinstance(bstr, bytes)
- return bstr.decode("iso-8859-1") # always successful
- def str2bytes(sstr):
- assert isinstance(sstr, str)
- return sstr.encode("iso-8859-1") # might fail, but spec says it doesn't
- def textopen(filename, mode):
- # We use the same encoding as for all wsgi strings here.
- return open(filename, mode, encoding="iso-8859-1")
+def bytes2str(bstr):
+ assert isinstance(bstr, bytes)
+ return bstr.decode("iso-8859-1") # always successful
+
+def str2bytes(sstr):
+ assert isinstance(sstr, str)
+ return sstr.encode("iso-8859-1") # might fail, but spec says it doesn't
+
+def textopen(filename, mode):
+ # We use the same encoding as for all wsgi strings here.
+ return open(filename, mode, encoding="iso-8859-1")
diff --git a/wsgitools/middlewares.py b/wsgitools/middlewares.py
index 32ecb59..ef9fe84 100644
--- a/wsgitools/middlewares.py
+++ b/wsgitools/middlewares.py
@@ -8,20 +8,12 @@ import collections
import io
from wsgitools.internal import bytes2str, str2bytes
-
-if sys.version_info[0] >= 3:
- def exc_info_for_raise(exc_info):
- return exc_info[1].with_traceback(exc_info[2])
-else:
- def exc_info_for_raise(exc_info):
- return exc_info[0], exc_info[1], exc_info[2]
-
from wsgitools.filters import CloseableList, CloseableIterator
from wsgitools.authentication import AuthenticationRequired, \
ProtocolViolation, AuthenticationMiddleware
__all__.append("SubdirMiddleware")
-class SubdirMiddleware(object):
+class SubdirMiddleware:
"""Middleware choosing wsgi applications based on a dict."""
def __init__(self, default, mapping={}):
"""
@@ -54,7 +46,7 @@ class SubdirMiddleware(object):
return app(environ, start_response)
__all__.append("NoWriteCallableMiddleware")
-class NoWriteCallableMiddleware(object):
+class NoWriteCallableMiddleware:
"""This middleware wraps a wsgi application that needs the return value of
C{start_response} function to a wsgi application that doesn't need one by
writing the data to a C{BytesIO} and then making it be the first result
@@ -78,7 +70,7 @@ class NoWriteCallableMiddleware(object):
try:
if sio.tell() > 0 or gotiterdata:
assert exc_info is not None
- raise exc_info_for_raise(exc_info)
+ raise exc_info[1].with_traceback(exc_info[2])
finally:
exc_info = None
assert isinstance(status, str)
@@ -121,7 +113,7 @@ class NoWriteCallableMiddleware(object):
(data,), ret)
__all__.append("ContentLengthMiddleware")
-class ContentLengthMiddleware(object):
+class ContentLengthMiddleware:
"""Guesses the content length header if possible.
@note: The application used must not use the C{write} callable returned by
C{start_response}."""
@@ -149,7 +141,7 @@ class ContentLengthMiddleware(object):
try:
if gotdata:
assert exc_info is not None
- raise exc_info_for_raise(exc_info)
+ raise exc_info[1].with_traceback(exc_info[2])
finally:
exc_info = None
assert isinstance(status, str)
@@ -216,11 +208,11 @@ def cacheable(environ):
return True
__all__.append("CachingMiddleware")
-class CachingMiddleware(object):
+class CachingMiddleware:
"""Caches reponses to requests based on C{SCRIPT_NAME}, C{PATH_INFO} and
C{QUERY_STRING}."""
- class CachedRequest(object):
+ class CachedRequest:
def __init__(self, timestamp):
self.timestamp = timestamp
self.status = ""
@@ -291,7 +283,7 @@ class CachingMiddleware(object):
try:
if cache_object.body:
assert exc_info is not None
- raise exc_info_for_raise(exc_info)
+ raise exc_info[1].with_traceback(exc_info[2])
finally:
exc_info = None
assert isinstance(status, str)
@@ -319,7 +311,7 @@ class CachingMiddleware(object):
return CloseableIterator(getattr(ret, "close", None), pass_through())
__all__.append("DictAuthChecker")
-class DictAuthChecker(object):
+class DictAuthChecker:
"""Verifies usernames and passwords by looking them up in a dict."""
def __init__(self, users):
"""
@@ -390,7 +382,7 @@ class BasicAuthMiddleware(AuthenticationMiddleware):
self, environ, start_response, exception)
__all__.append("TracebackMiddleware")
-class TracebackMiddleware(object):
+class TracebackMiddleware:
"""In case the application throws an exception this middleware will show an
html-formatted traceback using C{cgitb}."""
def __init__(self, app):
diff --git a/wsgitools/scgi/__init__.py b/wsgitools/scgi/__init__.py
index f651264..e2a68c2 100644
--- a/wsgitools/scgi/__init__.py
+++ b/wsgitools/scgi/__init__.py
@@ -7,7 +7,7 @@ except ImportError:
else:
have_sendfile = True
-class FileWrapper(object):
+class FileWrapper:
"""
@ivar offset: Initially 0. Becomes -1 when reading using next and
becomes positive when reading using next. In the latter case it
@@ -52,8 +52,6 @@ class FileWrapper(object):
if data:
return data
raise StopIteration
- def next(self):
- return self.__next__()
def _convert_environ(environ, multithread=False, multiprocess=False,
run_once=False):
diff --git a/wsgitools/scgi/asynchronous.py b/wsgitools/scgi/asynchronous.py
index 0b014bf..61bbc6b 100644
--- a/wsgitools/scgi/asynchronous.py
+++ b/wsgitools/scgi/asynchronous.py
@@ -9,13 +9,6 @@ import errno
from wsgitools.internal import bytes2str, str2bytes
from wsgitools.scgi import _convert_environ, FileWrapper
-if sys.version_info[0] >= 3:
- def exc_info_for_raise(exc_info):
- return exc_info[1].with_traceback(exc_info[2])
-else:
- def exc_info_for_raise(exc_info):
- return exc_info[0], exc_info[1], exc_info[2]
-
class SCGIConnection(asyncore.dispatcher):
"""SCGI connection class used by L{SCGIServer}."""
# connection states
@@ -147,7 +140,7 @@ class SCGIConnection(asyncore.dispatcher):
if exc_info:
if self.outheaders == True:
try:
- raise exc_info_for_raise(exc_info)
+ raise exc_info[1].with_traceback(exc_info[2])
finally:
exc_info = None
assert self.outheaders != True # unsent
diff --git a/wsgitools/scgi/forkpool.py b/wsgitools/scgi/forkpool.py
index 752f0e7..df8a92f 100644
--- a/wsgitools/scgi/forkpool.py
+++ b/wsgitools/scgi/forkpool.py
@@ -5,10 +5,7 @@ It works with multiple processes that are periodically cleaned up to prevent
memory leaks having an impact to the system.
"""
-try:
- import resource
-except ImportError:
- resource = None
+import resource
import socket
import os
import select
@@ -19,16 +16,9 @@ import signal
from wsgitools.internal import bytes2str, str2bytes
from wsgitools.scgi import _convert_environ, FileWrapper
-if sys.version_info[0] >= 3:
- def exc_info_for_raise(exc_info):
- return exc_info[1].with_traceback(exc_info[2])
-else:
- def exc_info_for_raise(exc_info):
- return exc_info[0], exc_info[1], exc_info[2]
-
__all__ = []
-class SocketFileWrapper(object):
+class SocketFileWrapper:
"""Wraps a socket to a wsgi-compliant file-like object."""
def __init__(self, sock, toread):
"""@param sock: is a C{socket.socket()}"""
@@ -149,8 +139,6 @@ class SocketFileWrapper(object):
if not data:
raise StopIteration
return data
- def next(self):
- return self.__next__()
def flush(self):
"""see pep333"""
def write(self, data):
@@ -167,10 +155,10 @@ class SocketFileWrapper(object):
self.write(line)
__all__.append("SCGIServer")
-class SCGIServer(object):
+class SCGIServer:
"""Usage: create an L{SCGIServer} object and invoke the run method which
will then turn this process into an scgi server."""
- class WorkerState(object):
+ class WorkerState:
"""state: 0 means idle and 1 means working.
These values are also sent as strings '0' and '1' over the socket."""
def __init__(self, pid, sock, state):
@@ -472,7 +460,7 @@ class SCGIServer(object):
def start_response(status, headers, exc_info=None):
if exc_info and response_head[0]:
try:
- raise exc_info_for_raise(exc_info)
+ raise exc_info[1].with_traceback(exc_info[2])
finally:
exc_info = None
assert isinstance(status, str)