分模块测试

application.py

对 application.py 的测试,调用命令:

python test/application.py

test_UppercaseMethods(self)

这个函数源代码是:

def testUppercaseMethods(self):
    urls = ("/", "hello")
    app = web.application(urls, locals())
    class hello:
        def GET(self): return "hello"
        def internal(self): return "secret"

    response = app.request('/', method='internal')
    self.assertEquals(response.status, '405 Method Not Allowed')

这个函数似乎是在测试 request 请求的方法不正确时,application 的反应。

我们打印一下 response 的值,发现内容是这样的:

<Storage {'status': '405 Method Not Allowed', 
'headers': {'Content-Type': 'text/html', 
'Allow': 'GET, HEAD, POST, PUT, DELETE'}, 
'header_items': [('Content-Type', 'text/html'), 
('Allow', 'GET, HEAD, POST, PUT, DELETE')], 'data': 'None'}>

从这里可以看出允许的方法:

'Allow': 'GET, HEAD, POST, PUT, DELETE'

response.status 最终值为:

'status': '405 Method Not Allowed',

我们再一次深入 web.application.request, 看一看具体实现。

    def request(self, localpart='/', method='GET', data=None,
                host="0.0.0.0:8080", headers=None, https=False, **kw):

        ...

        env = dict(
            env, 
            HTTP_HOST=host, 
            REQUEST_METHOD=method, 
            PATH_INFO=path, 
            QUERY_STRING=query, 
            HTTPS=str(https)
            )

        ...

        response = web.storage()
        def start_response(status, headers):
            response.status = status
            response.headers = dict(headers)
            response.header_items = headers

        response.data = "".join(self.wsgifunc()(env, start_response))

        return response

我们只保留了关键代码,可以看出,method 首先被设置在了 env 中,然后, 通过

    self.wsgifunc()(env, start_response))

start_responsewsgifunc() 返回的函数调用,response 的值被设置。 如果阅读了前面推荐的几篇关于 WSGI 的文章,对这里的 self.wsgifunc()start_response应该不会感觉到陌生。

好,我们继续深入 web.application.wsgifunc()

    def wsgifunc(self, *middleware):

        ...

        def wsgi(env, start_resp):
            # clear threadlocal to avoid inteference of previous requests
            self._cleanup()

            self.load(env)
            try:
                # allow uppercase methods only
                if web.ctx.method.upper() != web.ctx.method:
                    raise web.nomethod()

                result = self.handle_with_processors()
                if is_generator(result):
                    result = peep(result)
                else:
                    result = [result]
            except web.HTTPError, e:
                result = [e.data]


            result = web.safestr(iter(result))

            status, headers = web.ctx.status, web.ctx.headers
            start_resp(status, headers)

            def cleanup():
                self._cleanup()
                yield '' # force this function to be a generator

            return itertools.chain(result, cleanup())

        for m in middleware: 
            wsgi = m(wsgi)

        return wsgi

从上述代码可以看出,返回的函数就是 wsgi, wsgi 在返回前,可能 会使用 middleware 进行一层一层的包装。不过这里,我们只看 wsgi 的代码就可以了。

关键的两行在:

status, headers = web.ctx.status, web.ctx.headers
start_resp(status, headers)

这里,web.application.requests.start_response 函数 被调用,并设置好 statusheaders,二者的值从 web.ctx 中取得。所以,下一部,我们 关心 web.ctx 值的来源。因为 method 的信息在 env 中,所以 web.ctx 的值一定会受 env 的影响,所以在 wsgi 中要找到 env是如何 被使用的。

self.load(env)
try:
    # allow uppercase methods only
    if web.ctx.method.upper() != web.ctx.method:
        raise web.nomethod()

我们猜想 self.load(env)web.ctx 进行了设置,然后后面的 tryweb.ctx 检查,这里对 upper的检查,让我们想到了,原来一开始的 testUppercaseMethods 是在测试 method 名字是不是全是大写。。。

于是在 raise web.nomethod() 前加一个 print ,测试一下是不是执行到 了这里,发现还真是。那么我们再深入 load ,看看如何根据 env 设置 web.ctx

def load(self, env):
    """Initializes ctx using env."""
    ctx = web.ctx
    ctx.clear()
    ctx.status = '200 OK'

    ...

    ctx.method = env.get('REQUEST_METHOD')

    ...

load 函数会根据 env 设置 web.ctx ,但是只是简单赋值,没有对 method 方法的检查。于是,我明白了,原来是在 raise web.nomethod()中对 status 进行了设置。

raise 之前打印 web.ctx.status 的值,果然是 200 OK。那我们再进入 web.nomethod 看看吧。

这个函数在 webapi.py 中,进入文件一看,果然是对许多状态的定义。 找到 nomethod ,发现指向 NoMethod,下面看 NoMethod:

class NoMethod(HTTPError):
    """A `405 Method Not Allowed` error."""
    def __init__(self, cls=None):
        status = '405 Method Not Allowed'
        headers = {}
        headers['Content-Type'] = 'text/html'

        methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE']
        if cls:
            methods = [method for method in methods if hasattr(cls, method)]

        headers['Allow'] = ', '.join(methods)
        data = None
        HTTPError.__init__(self, status, headers, data)

发现还真是对错误号 405 的处理。 设置好status, 最终的处理 发给了 HTTPError,所以我们再进入 HTTPError

class HTTPError(Exception):
    def __init__(self, status, headers={}, data=""):
        ctx.status = status
        for k, v in headers.items():
            header(k, v)
        self.data = data
        Exception.__init__(self, status)

这里终于发现了对 ctx.status 的设置。这就是在这里我们知道了下面这两 行关键代码的最终来源。

status, headers = web.ctx.status, web.ctx.headers
start_resp(status, headers)

前者设置 statusheaders,后者调用 requests 函数中的 start_responseresponse 的状态进行了设置。最后返回给最一开始 testUppercaseMethods 中的 response

comments powered by Disqus