WSGI/AskWSGI

深思WSGI

SEE: WSGI的问题集 - python-cnCPyUG华蟒用户组

Limodou 问题

limodou <[email protected]>
reply-to        [email protected]
to      "Python.cn@google" <[email protected]>
date    Thu, Dec 25, 2008 at 10:59
subject [CPyUG:74762] WSGI的问题集

希望对wsgi了解的介绍下:

  1. WSGI的app之间如何传递消息?在environ中添加吗?
  2. 多个app之间通过wsgi方式来工作,一般是怎么被调用的?它们的调用如何实现配置化?
  3. 对于request, response这样的东西如何在wsgi中处理,如何传递?是每次通过environ直接生成吗?如:

request = Request(environ)

这样做,会不会有效率问题。另外如何处理request中动态添加的属性,丢失吗?

讨论集锦

WSGI的app之间如何传递消息

Limodou
def configure(app):
   return ErrorHandlerMiddleware(
           SessionMiddleware(
            IdentificationMiddleware(
             AuthenticationMiddleware(
              UrlParserMiddleware(app))))))

因此出现了paster,在每个app程序中添加:

def app_factory(global_config, **local_config):
   return application

多个app之间的调用

Qiangning Hong
  • 同上,一个request最终落到一个app上。
  • 不过每一个middleware对于调用者而言,接口和app是一样的。
  • 一般在前端有一个appdispatcher通过URL选择执行哪一个app,可以是直接在WSGI Server里的代码指定,也可以这个dispatcher本身就是一个middleware。
  • Pylons使用的Routes这两种方式都支持。WSGI有一个route标准的草案。
潘俊勇 <[email protected]
  • paste支持ini文件方式的部署配置,如:

[app:main]
use = egg:wo
filter-with = fs

[filter:fs]
paste.filter_factory = zopen.frs4wsgi.filter:filter_factory
directory = var/uploadtemp

[server:main]
use = egg:Paste#http
host = 0.0.0.0
port = 8092

request/response的WSGI 处置

Qiangning Hong
  • 是的,webob其实就是怎么干的。
  • webob好像是用的lazy方式,初始化一个对象很轻量。如果要考虑效率问题,可以把生成的request对象也放在environ里面,之后的middleware和app就都可以直接使用了。Pylons是这样做的。
  • Limodou: 对于第三个问题,可能就与具体的框架相关了。目前不管是django也好,还是uliweb也好,其本上只有一个主wsgi应用在运行。不过因为uliweb的主要处理就是一个wsgi的app,所以是可以放在wsgi中进行使用的。

    • Pylons是每一个Controller就是一个WSGI app。通过Routes来dispatch到某一个特定的controller运行
YoungKing(Zopen.cn)
  • 在wsgi中,environment对应request,start_response对应response.envrionment中可以传任意python对象,这个比request方便多了.你可以修改envionment,把属性放到这里来
潘俊勇 <[email protected]

WSGI中间件是如何复用的?

潘俊勇 <[email protected]
  • 关于profile的middleware,repoze社区有了repoze.profile,这里有个可用的中间件清单:
  • http://repoze.org/repoze_components.html#middleware

  • 其中repoze.what作为认证的中间件,功能很强。
  • 好像paste社区也有一个wsgi middleware清单的,刚刚没找到。
  • 目前paste社区和repoze社区关系非常紧密的,一些中间件开发力量和项目也合并起来的
Qiangning Hong
  • Pylons推进Routes成为一个WSGI middleware是一个创举,这样使得框架中的dispatcher也变成可替换的了。
    • 以前Pylons和TurboGears的最大的区别就在于一个是基于Routes一个是基于CherryPy,不可调和。

    • 现在有了WSGI route标准,连dispatcher也可以替换了。
    • WSGI middleware的好处是可重用性好,所有的WSGI-aware框架都可以使用。
    • PASTE本身是一些WSGI组件的集合和工具,用不着的话当然完全可以不使用它。

  • Pylons是从RoR抄过来的,所以有Controller/View的概念。喜欢用函数的话,完全也可以写成一个函数就是一个WSGI

application的。比如写成这样:

def say_hello(environ, start_response):
   start_response("200 OK", [('Content-type', 'text/plain')])
   return ["Hello %s!" %environ['selector.vars']['name']]

from selector import Selector

s = Selector()
s.add('/myapp/hello/{name}', GET=say_hello)

from flup.server.scgi import WSGIServer
WSGIServer(s).run()

selector是一个WSGI URL Dispatcher Middleware,根据URL选择一个WSGI app运行,

用Routes也可以实现类似效果。

顺便说一句,paste.debug.profile.ProfileMiddleware就是一个用来做 profile 的 WSGI middleware

meilanweb

paste分成三个部分

比如我自己为了在GAE上做东西玩,自己做了一个framework玩,由于只是自己用,不需要考虑配置、互换性等问题,就完全没有使用pastescript/deploy,只用了他一个middleware,代码很简单,一共142行,但基本的功能(URL Dispatch、Template、Authentication)已经可以用了。

   1 #meilanweb.py
   2 import os
   3 import sys
   4 import wsgiref.simple_server
   5 import wsgiref.handlers
   6 from types import TypeType, ClassType
   7 import threading
   8 
   9 import webob
  10 import webob.exc
  11 from routes import Mapper as _Mapper
  12 from routes.middleware import RoutesMiddleware
  13 from paste.exceptions.errormiddleware import ErrorMiddleware
  14 import webhelpers as h
  15 from mako.lookup import TemplateLookup
  16 from google.appengine.api import users
  17 
  18 c = threading.local()
  19 
  20 def Mapper(*a, **kw):
  21    prefix = kw.pop('prefix', None)
  22    m = _Mapper(*a, **kw)
  23    if prefix:
  24        m.prefix = prefix
  25    return m
  26 
  27 def make_app(mapper, controllers, template_dir=None, debug=False):
  28    app = Application(controllers, template_dir=template_dir)
  29    app = AuthenticateMiddleware(app)
  30    app = RoutesMiddleware(app, mapper)
  31    app = ErrorDocumentMiddleware(app)
  32    app = ErrorMiddleware(app, debug=debug)
  33    return app
  34 
  35 def run(app, host='', port=8080):
  36    server = wsgiref.simple_server.make_server(host, port, app)
  37    server.serve_forever()
  38 
  39 def run_cgi(app):
  40    wsgiref.handlers.CGIHandler().run(app)
  41 
  42 class Application(object):
  43    def __init__(self, controllers, template_dir=None):
  44        self.controllers = controllers
  45        if template_dir:
  46            self.lookup = TemplateLookup(template_dir, input_encoding='utf8',
  47                                         output_encoding='utf8')
  48 
  49    def __call__(self, environ, start_response):
  50        controller = self.resolve(environ)
  51        environ['meilanweb.render'] = self.render
  52        s = controller(environ, start_response)
  53        return s
  54 
  55    def resolve(self, environ):
  56        """Get the controller app from the environ."""
  57        match = environ['wsgiorg.routing_args'][1]
  58        try:
  59            controller_name = match['controller']
  60        except KeyError:
  61            raise webob.exc.HTTPNotFound()
  62        controller_class_name = \
  63                class_name_from_controller_name(controller_name)
  64        controller = self.controllers[controller_class_name]
  65        if isinstance(controller, (TypeType, ClassType)):
  66            controller = controller()
  67        return controller
  68 
  69    def render(self, template, **kwargs):
  70        return self.lookup.get_template(template).render(**kwargs)
  71 
  72 
  73 def class_name_from_controller_name(controller_name):
  74    # the code is stolen from Pylons
  75    """
  76    Excample::
  77 
  78        >>> class_name_from_controller_name('with-dashes')
  79        'WithDashes'
  80        >>> class_name_from_controller_name('with_underscores')
  81        'WithUnderscores'
  82        >>> class_name_from_controller_name('oneword')
  83        'Oneword'
  84    """
  85    words = controller_name.replace('-', '_').split('_')
  86    return ''.join(w.title() for w in words)
  87 
  88 
  89 class Controller(object):
  90    def __call__(self, environ, start_response):
  91        c.__dict__.clear()
  92        self.environ = environ
  93        match = environ['wsgiorg.routing_args'][1]
  94        action_name = match['action'].replace('-', '_')
  95        action = getattr(self, action_name)
  96        kwargs = match.copy()
  97        del kwargs['controller']
  98        del kwargs['action']
  99        self.request = webob.Request(environ)
 100        self.response = webob.Response(request=self.request)
 101        retval = action(**kwargs)
 102        if retval:
 103            self.response.write(retval)
 104        return self.response(environ, start_response)
 105 
 106    def render(self, template, **kwargs):
 107        return self.environ['meilanweb.render'](template, c=c, h=h, **kwargs)
 108 
 109 
 110 def default_template_dir(filepath):
 111    here = os.path.dirname(os.path.abspath(filepath))
 112    return os.path.join(here, 'templates')
 113 
 114 
 115 class ErrorDocumentMiddleware(object):
 116    def __init__(self, app):
 117        self.app = app
 118 
 119    def __call__(self, environ, start_response):
 120        try:
 121            return self.app(environ, start_response)
 122        except webob.exc.WSGIHTTPException, e:
 123            e.environ = environ
 124            return e(environ, start_response)
 125 
 126 
 127 class AuthenticateMiddleware(object):
 128    def __init__(self, app):
 129        self.app = app
 130 
 131    def __call__(self, environ, start_response):
 132        user = users.get_current_user()
 133        if user:
 134            environ['REMOTE_USER'] = user
 135        try:
 136            return self.app(environ, start_response)
 137        except webob.exc.HTTPUnauthorized, e:
 138            req = webob.Request(environ)
 139            url = users.create_login_url(req.url)
 140            raise webob.exc.HTTPTemporaryRedirect(location=url)
 141 
 142 h.create_logout_url = users.create_logout_url

整体

Gu Yingbo <[email protected]
  • 我认为wsgi的中间件倾向于做一些简单独立事情,每个中间件app 的依赖性比较低,
  • 像werkzeug的DebuggedApplication这种风格就很好,一条语句就可以为一个wsgi应用添加一个调试界面。

  • 我自己也就用中间件做一个请求的计时,sql语句调试开关这样简单的事情。
  • 我觉得wsgi的最大好处是:

    • 分离了服务器和应用的实现,
    • 写一个wsgi应用就有一大把的部署方式,自己可以根据需要选择合适的。
黄毅 <[email protected]>
再啰嗦几句,web框架要统一就要标准化组件之间的接口,但这些已经不是wsgi关注的领域了。
  • environ字典对 app 来说只代表最原始的 web请求,它不应该是框架内部组件之间交换数据的场所。
  • 框架应该在入口处统一解析一次 environ,并构造一个 request 对象,其后的事情都应该建立在这个request对象的基础之上,而不用再去直接处理environ了,
  • url dispatcher解析URL的结果也应该存放在这个 request 对象中,而不是 environ 中。
    • 像 url dispatcher、auth 这些都应该是框架内的组件,比如说可以有一个框架内的middleware协议,就像 django 的middleware那样,不需要wsgi那么复杂,它们之间就只需要传递 request和response 对象即可,environ、start_response对它们来说都是太底层的东西。
  • 而 wsgi middleware 一定要对所有wsgi application透明,这是我理解的 wsgi。
  • 标准化web框架应该是在 web框架内部做的事情,而不应该一味地去扩展wsgi,试图把 wsgi 变成一个框架。
  • 标准化web框架也不准确,顶多是标准化一个MVC的web框架。
  • uliweb没怎么看过,不过我认为limudou完全不必把controller变成wsgi application,应该把框架暴露成一个wsgi app,并且制定一些框架内部组件之间的标准化接口,比如框架内的middleware之间的接口,存取配置的接口,调用模板的接口,使用ORM的接口等。

>>> Limodou:好的建议。一个框架中有许多的处理,有些是与wsgi相关的,有些不是。所以只有wsgi统一化也只解决了一部分问题。从看到的一些wsgimiddleware的互访来说,它们其实也约定了许多标准的组件,比如sqlalchemy,webob等。而这些只是组件的选择上的一致性,还没有达到接口的规范统一。不过这是很难的,而且也不知道是否真有意义。

  • 现在uliweb基本上就是你说的样子。middleware的标准基本上和django是一样的,不过我增加了一个__init__的定义,这样是可以进行初始化的,做一些特殊的事情。

WSGI不是框架

黄毅 <[email protected]>
reply-to        [email protected]
to      [email protected]
date    Thu, Dec 25, 2008 at 14:29
subject [CPyUG:74822] Re: WSGI的问题集

for UliWeb

Limodou
  • 谢谢,这下就清楚许多了。
  • 按上面的回答,uliweb中的是Dispatcher(类名都是这么起的),所以它本身是wsgi的。
  • 在uliweb的前段时间,还可以通过config.py来添加在执行Dispatcher前的wsgi序列,不过现在去了,因为修改了许多地方,加回来也很容易。
  • 因此我想uliweb会同时支持wsgi middleware和django-like middleware。
  • 因为django-like middleware更多是在针对request, response的处理,目标比较统一。
  • 对于wsgi的互用,我提第4个问题:
    • 不同的wsgi middleware在不同的项目中是如何互用的?

      • 是不是目前都是需要基于paster才可以?有共它的方式,还是根本不存在问题。
      • 因为我看到paster是可以在创建wsgi middleware时引入一些local_conf的,这个倒是可以继续用,自已写一个简单的装入器。其它的有没有类似要依赖于paster的东西呢?

复用vs易用

UliWeb 对策
  • 现在想一想wsgi就是提供了一个标准的接口,可以在不同的组件之间进行互相调用。不过把view函数写成app还是感觉麻烦,一个hello在uliweb中只要:

   1 @expose('/myapp/hello/<name>')
   2 def hello(name):
   3    return 'hello, %s!' % name
Qiangning Hong
  • 那个仅仅是selector的一个演示而已,一般情况下view里面是不需要使用selector.vars的。
  • 框架要干的事情就是把问题简化,比如用selector的作者写的另一个工具yaro的话,上面的例子就变成这样:

from yaro import Yaro

@Yaro
def hello_world(req):
   return "Hello World!"

from selector import Selector
s = Selector()
s.add('/myapp/hello/{name}', GET=say_hello)
from flup.server.scgi import WSGIServer
WSGIServer(s).run()


反馈

创建 by -- ZoomQuiet [2008-12-25 04:10:48]

WSGI/AskWSGI (last edited 2009-12-25 07:15:39 by localhost)