Ticket #7993: wsgi-python3-7993-6.1.patch

File wsgi-python3-7993-6.1.patch, 16.2 KB (added by Gavin Panella, 4 years ago)

Incremental patch to the wsgi-py3-7993-6 branch which relaxes some type checks in Python 2.

  • twisted/web/test/test_wsgi.py

    diff --git a/twisted/web/test/test_wsgi.py b/twisted/web/test/test_wsgi.py
    index ae14467..6705572 100644
    a b __metaclass__ = type 
    1010from sys import exc_info
    1111import tempfile
    1212import traceback
     13import warnings
    1314
    1415from zope.interface.verify import verifyObject
    1516
    class WSGITestsMixin: 
    244245                    content = application(environ, startResponse)
    245246            except:
    246247                result.errback()
    247                 startResponse('500 Error', [], exc_info())
     248                startResponse('500 Error', [])
    248249                return iter(())
    249250            else:
    250251                result.callback((environ, startResponse))
    class EnvironTests(WSGITestsMixin, TestCase): 
    800801    def test_wsgiErrorsAcceptsOnlyNativeStrings(self):
    801802        """
    802803        The C{'wsgi.errors'} file-like object from the C{environ} C{dict} will
    803         permit writes of only native strings.
     804        permit writes of only native strings in Python 3, and will warn
     805        against the use of non-native strings in Python 2.
    804806        """
    805807        request, result = self.prepareRequest()
    806808        request.requestReceived()
    class EnvironTests(WSGITestsMixin, TestCase): 
    808810        errors = environ["wsgi.errors"]
    809811
    810812        if _PY3:
    811             self.assertRaises(TypeError, errors.write, b"fred")
     813            # In Python 3, TypeError is raised.
     814            error = self.assertRaises(TypeError, errors.write, b"fred")
     815            self.assertEqual(
     816                "write() argument must be str, not b'fred' (bytes)",
     817                str(error))
    812818        else:
    813             self.assertRaises(TypeError, errors.write, u"fred")
     819            # In Python 2, only a warning is issued; existing WSGI
     820            # applications may rely on this non-compliant behaviour.
     821            with warnings.catch_warnings(record=True) as caught:
     822                errors.write(u"fred")
     823            self.assertEqual(1, len(caught))
     824            self.assertEqual(UnicodeWarning, caught[0].category)
     825            self.assertEqual(
     826                "write() argument should be str, not u'fred' (unicode)",
     827                str(caught[0].message))
    814828
    815829
    816830
    class StartResponseTests(WSGITestsMixin, TestCase): 
    12031217    def test_statusMustBeNativeString(self):
    12041218        """
    12051219        The response status passed to the I{start_response} callable MUST be a
    1206         native string.
     1220        native string in Python 2 and Python 3.
    12071221        """
    12081222        status = b"200 OK" if _PY3 else u"200 OK"
    12091223
    class StartResponseTests(WSGITestsMixin, TestCase): 
    12751289            [b'Baz: quux', b'Foo: bar'])
    12761290
    12771291
    1278     def test_headersMustBePlainList(self):
     1292    def test_headersMustBeSequence(self):
     1293        """
     1294        The headers passed to the I{start_response} callable MUST be a
     1295        sequence.
     1296        """
     1297        headers = [("key", "value")]
     1298
     1299        def application(environ, startResponse):
     1300            startResponse("200 OK", iter(headers))
     1301            return iter(())
     1302
     1303        request, result = self.prepareRequest(application)
     1304        request.requestReceived()
     1305
     1306        def checkMessage(error):
     1307            self.assertRegexpMatches(
     1308                str(error), "headers must be a list, not "
     1309                "<list_?iterator .+> [(]list_?iterator[)]")
     1310
     1311        return self.assertFailure(result, TypeError).addCallback(checkMessage)
     1312
     1313
     1314    @inlineCallbacks
     1315    def test_headersShouldBePlainList(self):
    12791316        """
    1280         The headers passed to the I{start_response} callable MUST be in a
    1281         plain list.
     1317        The headers passed to the I{start_response} callable SHOULD be a plain
     1318        list.
    12821319        """
    12831320        def application(environ, startResponse):
    12841321            startResponse("200 OK", (("not", "list"),))
    12851322            return iter(())
    12861323
    12871324        request, result = self.prepareRequest(application)
     1325
     1326        # In both Python 2 and Python 3, only a warning is issued; existing
     1327        # WSGI applications may rely on this non-compliant behaviour, and we
     1328        # can actually work with any sequence type.
     1329        with warnings.catch_warnings(record=True) as caught:
     1330            request.requestReceived()
     1331            yield result
     1332        self.assertEqual(1, len(caught))
     1333        self.assertEqual(RuntimeWarning, caught[0].category)
     1334        self.assertEqual(
     1335            "headers should be a list, not (('not', 'list'),) (tuple)",
     1336            str(caught[0].message))
     1337
     1338
     1339    def test_headersMustEachBeSequence(self):
     1340        """
     1341        Each header passed to the I{start_response} callable MUST be a
     1342        sequence.
     1343        """
     1344        header = ("key", "value")
     1345
     1346        def application(environ, startResponse):
     1347            startResponse("200 OK", [iter(header)])
     1348            return iter(())
     1349
     1350        request, result = self.prepareRequest(application)
    12881351        request.requestReceived()
    12891352
    12901353        def checkMessage(error):
    1291             self.assertEqual(
    1292                 "headers must be a list, not (('not', 'list'),) (tuple)",
    1293                 str(error))
     1354            self.assertRegexpMatches(
     1355                str(error), "header must be a [(]str, str[)] tuple, not "
     1356                "<tuple_?iterator .+> [(]tuple_?iterator[)]")
    12941357
    12951358        return self.assertFailure(result, TypeError).addCallback(checkMessage)
    12961359
    12971360
    1298     def test_headersMustEachBeTuple(self):
     1361    @inlineCallbacks
     1362    def test_headersShouldEachBeTuple(self):
    12991363        """
    1300         Each header passed to the I{start_response} callable MUST be in a
     1364        Each header passed to the I{start_response} callable SHOULD be a
    13011365        tuple.
    13021366        """
    13031367        def application(environ, startResponse):
    class StartResponseTests(WSGITestsMixin, TestCase): 
    13051369            return iter(())
    13061370
    13071371        request, result = self.prepareRequest(application)
     1372
     1373        # In both Python 2 and Python 3, only a warning is issued; existing
     1374        # WSGI applications may rely on this non-compliant behaviour, and we
     1375        # can actually work with any sequence type.
     1376        with warnings.catch_warnings(record=True) as caught:
     1377            request.requestReceived()
     1378            yield result
     1379        self.assertEqual(1, len(caught))
     1380        self.assertEqual(RuntimeWarning, caught[0].category)
     1381        self.assertEqual(
     1382            "header should be a (str, str) tuple, not ['not', 'tuple'] (list)",
     1383            str(caught[0].message))
     1384
     1385
     1386    def test_headersShouldEachHaveKeyAndValue(self):
     1387        """
     1388        Each header passed to the I{start_response} callable MUST hold a key
     1389        and a value, and ONLY a key and a value.
     1390        """
     1391        def application(environ, startResponse):
     1392            startResponse("200 OK", [("too", "many", "cooks")])
     1393            return iter(())
     1394
     1395        request, result = self.prepareRequest(application)
    13081396        request.requestReceived()
    13091397
    13101398        def checkMessage(error):
    13111399            self.assertEqual(
    1312                 "header must be (str, str) tuple, not ['not', 'tuple'] (list)",
    1313                 str(error))
     1400                "header must be a (str, str) tuple, not "
     1401                "('too', 'many', 'cooks')", str(error))
    13141402
    13151403        return self.assertFailure(result, TypeError).addCallback(checkMessage)
    13161404
    class StartResponseTests(WSGITestsMixin, TestCase): 
    13181406    def test_headerKeyMustBeNativeString(self):
    13191407        """
    13201408        Each header key passed to the I{start_response} callable MUST be at
    1321         native string.
     1409        native string in Python 2 and Python 3.
    13221410        """
    13231411        key = b"key" if _PY3 else u"key"
    13241412
    class StartResponseTests(WSGITestsMixin, TestCase): 
    13401428    def test_headerValueMustBeNativeString(self):
    13411429        """
    13421430        Each header value passed to the I{start_response} callable MUST be at
    1343         native string.
     1431        native string in Python 2 and Python 3.
    13441432        """
    13451433        value = b"value" if _PY3 else u"value"
    13461434
    class StartResponseTests(WSGITestsMixin, TestCase): 
    16521740        def checkMessage(error):
    16531741            if _PY3:
    16541742                self.assertEqual(
    1655                     "write() argument must be bytes, not 'bogus' (str)",
     1743                    "Can only write bytes to a transport, not 'bogus'",
    16561744                    str(error))
    16571745            else:
    16581746                self.assertEqual(
    1659                     "write() argument must be bytes, not u'bogus' (unicode)",
     1747                    "Can only write bytes to a transport, not u'bogus'",
    16601748                    str(error))
    16611749
    16621750        return self.assertFailure(result, TypeError).addCallback(checkMessage)
  • twisted/web/wsgi.py

    diff --git a/twisted/web/wsgi.py b/twisted/web/wsgi.py
    index 62fc330..3f30658 100644
    a b U{Python Web Server Gateway Interface v1.0.1<http://www.python.org/dev/peps/pep- 
    88
    99__metaclass__ = type
    1010
     11from collections import Sequence
    1112from sys import exc_info
     13from warnings import warn
    1214
    1315from zope.interface import implementer
    1416
     17from twisted.internet.threads import blockingCallFromThread
    1518from twisted.python.compat import reraise
    1619from twisted.python.log import msg, err
    1720from twisted.python.failure import Failure
    else: 
    7275        round-trip it to bytes and back using ISO-8859-1 as the encoding.
    7376
    7477        @type string: C{str} or C{bytes}
    75         @rtype: str
     78        @rtype: C{str}
    7679
    7780        @raise UnicodeEncodeError: If C{string} contains non-ISO-8859-1 chars.
    7881        """
    else: 
    8790        ISO-8859-1 byte string.
    8891
    8992        @type string: C{str}
    90         @rtype: bytes
     93        @rtype: C{bytes}
    9194
    9295        @raise UnicodeEncodeError: If C{string} contains non-ISO-8859-1 chars.
    93         @raise TypeError: If C{string} is not a byte string.
    9496        """
    9597        return string.encode("iso-8859-1")
    9698
    class _ErrorStream: 
    117119
    118120        @type data: str
    119121
    120         @raise TypeError: If C{data} is not a native string.
     122        @raise TypeError: On Python 3, if C{data} is not a native string. On
     123            Python 2 a warning will be issued.
    121124        """
    122125        if not isinstance(data, str):
    123             raise TypeError(
    124                 "write() argument must be str, not %r (%s)"
    125                 % (data, type(data).__name__))
     126            if str is bytes:
     127                warn("write() argument should be str, not %r (%s)" % (
     128                    data, type(data).__name__), category=UnicodeWarning)
     129            else:
     130                raise TypeError(
     131                    "write() argument must be str, not %r (%s)"
     132                    % (data, type(data).__name__))
     133
    126134        msg(data, system='wsgi', isError=True)
    127135
    128136
    class _ErrorStream: 
    136144        @param iovec: A C{list} of C{'\\n'}-terminated C{str} which will be
    137145            logged.
    138146
    139         @raise TypeError: If C{iovec} contains any non-native strings.
     147        @raise TypeError: On Python 3, if C{iovec} contains any non-native
     148            strings. On Python 2 a warning will be issued.
    140149        """
    141150        self.write(''.join(iovec))
    142151
    class _WSGIResponse: 
    347356        if self.started and excInfo is not None:
    348357            reraise(excInfo[1], excInfo[2])
    349358
    350         # PEP-3333 mandates that status should be a native string.
     359        # PEP-3333 mandates that status should be a native string. In practice
     360        # this is mandated by Twisted's HTTP implementation too, so we enforce
     361        # on both Python 2 and Python 3.
    351362        if not isinstance(status, str):
    352363            raise TypeError(
    353364                "status must be str, not %r (%s)"
    354365                % (status, type(status).__name__))
    355366
    356         # PEP-3333 mandates a plain list.
    357         if not isinstance(headers, list):
     367        # PEP-3333 mandates that headers should be a plain list, but in
     368        # practice we work with any sequence type and only warn when it's not
     369        # a plain list.
     370        if isinstance(headers, list):
     371            pass  # This is okay.
     372        elif isinstance(headers, Sequence):
     373            warn("headers should be a list, not %r (%s)" % (
     374                headers, type(headers).__name__), category=RuntimeWarning)
     375        else:
    358376            raise TypeError(
    359377                "headers must be a list, not %r (%s)"
    360378                % (headers, type(headers).__name__))
    361379
    362         # PEP-3333 mandates that each header should be a (str, str) tuple.
     380        # PEP-3333 mandates that each header should be a (str, str) tuple, but
     381        # in practice we work with any sequence type and only warn when it's
     382        # not a plain list.
    363383        for header in headers:
    364384            if isinstance(header, tuple):
    365                 is_okay = (
    366                     len(header) == 2 and
    367                     isinstance(header[0], str) and
    368                     isinstance(header[1], str)
    369                 )
    370                 if not is_okay:
    371                     raise TypeError(
    372                         "header must be (str, str) tuple, not %r" % (header, ))
     385                pass  # This is okay.
     386            elif isinstance(header, Sequence):
     387                warn("header should be a (str, str) tuple, not %r (%s)" % (
     388                    header, type(header).__name__), category=RuntimeWarning)
    373389            else:
    374390                raise TypeError(
    375                     "header must be (str, str) tuple, not %r (%s)"
     391                    "header must be a (str, str) tuple, not %r (%s)"
    376392                    % (header, type(header).__name__))
    377393
     394            # However, the sequence MUST contain only 2 elements.
     395            if len(header) != 2:
     396                raise TypeError(
     397                    "header must be a (str, str) tuple, not %r"
     398                    % (header, ))
     399
     400            # Both elements MUST be native strings. Non-native strings will be
     401            # rejected by the underlying HTTP machinery in any case, but we
     402            # reject them here in order to provide a more informative error.
     403            for elem in header:
     404                if not isinstance(elem, str):
     405                    raise TypeError(
     406                        "header must be (str, str) tuple, not %r"
     407                        % (header, ))
     408
    378409        self.status = status
    379410        self.headers = headers
    380411        return self.write
    class _WSGIResponse: 
    387418        the status and headers first.
    388419
    389420        This will be called in a non-I/O thread.
    390 
    391         @raise TypeError: If C{data} is not a byte string.
    392421        """
    393         # Check that `data` is bytes now because we will not get any feedback
    394         # from callFromThread() later on.
    395         if not isinstance(data, bytes):
    396             raise TypeError(
    397                 "write() argument must be bytes, not %r (%s)"
    398                 % (data, type(data).__name__))
    399 
    400         def wsgiWrite(started):
    401             if not started:
    402                 self._sendResponseHeaders()
    403             self.request.write(data)
    404 
    405422        # PEP-3333 states:
    406423        #
    407424        #   The server or gateway must transmit the yielded bytestrings to the
    class _WSGIResponse: 
    409426        #   each bytestring before requesting another one.
    410427        #
    411428        # This write() method is used for the imperative and (indirectly) for
    412         # the more familiar iterable-of-bytestrings WSGI mechanism, but offers
    413         # no back-pressure, and so violates this part of PEP-3333.
     429        # the more familiar iterable-of-bytestrings WSGI mechanism. It uses
     430        # C{blockingCallFromThread} to schedule writes. This allows exceptions
     431        # to propagate up from the underlying HTTP implementation. However,
     432        # that underlying implementation does not, as yet, provide any way to
     433        # know if the written data has been transmitted, so this method
     434        # violates the above part of PEP-3333.
    414435        #
    415436        # PEP-3333 also says that a server may:
    416437        #
    class _WSGIResponse: 
    422443        #
    423444        # However, providing some back-pressure may nevertheless be a Good
    424445        # Thing at some point in the future.
    425         self.reactor.callFromThread(wsgiWrite, self.started)
    426         self.started = True
     446
     447        def wsgiWrite(started):
     448            if not started:
     449                self._sendResponseHeaders()
     450            self.request.write(data)
     451
     452        try:
     453            return blockingCallFromThread(
     454                self.reactor, wsgiWrite, self.started)
     455        finally:
     456            self.started = True
    427457
    428458
    429459    def _sendResponseHeaders(self):