从 py2 迁移到 py3 后,将 cherrypy 的响应读取为 bytes/string

Reading cherrypy's response as bytes/string after migrating from py2 to py3

我刚刚将一个 cherrypy 应用程序从 2.7 迁移到 python(运行ning 在 3.6 上)。我之前有一堆基于 this recipe 的测试设置。配方的重点是模拟网络并在各个端点上执行测试单元。

现在我的服务器似乎 运行 没问题。但是,如果我 运行 测试单元,它们在 py3 中主要是 return 错误(全部通过 py2),这似乎与响应以字节(在 py3 中)而不是字符串(在 py2).

A test response
    ======================================================================
    FAIL: test_index (__main__.EndpointTests)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/home/anonymous/PycharmProjects/server-py3/tests/test_DServer.py", line 67, in test_index

        self.assertEqual(response.body, ['Hello World!'])
    AssertionError: Lists differ: [b'Hello World!'] != ['Hello World!']

    First differing element 0:
    b'Hello World!'
    'Hello World!'

    - [b'Hello World!']
    ?  -

    + ['Hello World!']

同样:

======================================================================
FAIL: test_valid_login (__main__.EndpointTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/anonymous/PycharmProjects/server-py3/tests/test_DServer.py", line 73, in test_valid_login
    self.assertEqual(response.output_status, '200 OK')
AssertionError: b'200 OK' != '200 OK'

我知道关于字节的行为有些不同(如 here 所解释的)。

实际上有 2 个问题:

在我的测试中处理这个问题的最佳方法是什么?我是否需要在服务器响应中断言的每个字符串前添加 b?

服务器上的快速测试似乎表明它可以工作。但是,我可能会以其他方式被这个问题困扰吗?关于 cherrypy 和迁移到 py3 的其他陷阱,有什么智慧的话吗?

之后我不再支持py2,我可以做一个干净的迁移。

找到了。

需要在 cptestcase 中进行编辑。问题的核心是这个配方在某种程度上依赖于 Cherrypy 的内部工作,2to3(我用来完成迁移的繁重工作的工具)无法很好地管理细节以达到 cp 的喜好。

总结是,您需要切换到 io.BytesIO,而不是 io.StringIO(这是 2to3 默认提供的)。因此,之前对 StringIO(data) 的调用应该是 BytesIO(data)。关键是 cp 现在内部期望那些 strings/bytes (因为 py2 并没有真正在 2 之间产生任何区别)是实际字节(因为 py3 实际上确实区分)。是的,在进行断言的实际测试中,您必须将 response.output_status & response.body 从字节转换为字符串或将它们与字节进行比较,如下所示:

self.assertEqual(response.output_status, b'200 OK')

但是 query_string(对于 GET)必须仍然是一个字符串。

这是对我有用的代码的完整编辑:

from io import BytesIO
import unittest
import urllib.request, urllib.parse, urllib.error

import cherrypy

cherrypy.config.update({'environment': "test_suite"})
cherrypy.server.unsubscribe()

local = cherrypy.lib.httputil.Host('127.0.0.1', 50000, "")
remote = cherrypy.lib.httputil.Host('127.0.0.1', 50001, "")

__all__ = ['BaseCherryPyTestCase']

class BaseCherryPyTestCase(unittest.TestCase):
    def request(self, path='/', method='GET', app_path='', scheme='http',
                proto='HTTP/1.1', data=None, headers=None, **kwargs):

        h = {'Host': '127.0.0.1'}

        if headers is not None:
            h.update(headers)

        if method in ('POST', 'PUT') and not data:
            data = urllib.parse.urlencode(kwargs).encode('utf-8')
            kwargs = None
            h['content-type'] = 'application/x-www-form-urlencoded'

        qs = None
        if kwargs:
            qs = urllib.parse.urlencode(kwargs)

        fd = None
        if data is not None:
            h['content-length'] = '%d' % len(data.decode('utf-8'))
            fd = BytesIO(data)

        app = cherrypy.tree.apps.get(app_path)
        if not app:
            raise AssertionError("No application mounted at '%s'" % app_path)

        app.release_serving()

        request, response = app.get_serving(local, remote, scheme, proto)
        try:
            h = [(k, v) for k, v in h.items()]
            response = request.run(method, path, qs, proto, h, fd)
        finally:
            if fd:
                fd.close()
                fd = None

        if response.output_status.startswith(b'500'):
            print(response.body)
            raise AssertionError("Unexpected error")

        response.collapse_body()
        return response