From 4d52eaa4801df3f3169df8e58758bcccf22dc4de Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Sat, 17 Jun 2023 19:35:21 +0200 Subject: drop support for Python 2.x --- README | 3 ++- pyproject.toml | 2 +- setup.py | 2 +- test.py | 21 +++++++++------------ wsgitools/applications.py | 19 +++++++------------ wsgitools/authentication.py | 2 +- wsgitools/digest.py | 9 ++++----- wsgitools/filters.py | 11 +++++------ wsgitools/internal.py | 30 +++++++++++------------------- wsgitools/middlewares.py | 28 ++++++++++------------------ wsgitools/scgi/__init__.py | 4 +--- wsgitools/scgi/asynchronous.py | 9 +-------- wsgitools/scgi/forkpool.py | 22 +++++----------------- 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) -- cgit v1.2.3