Contents
深思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了解的介绍下:
- WSGI的app之间如何传递消息?在environ中添加吗?
- 多个app之间通过wsgi方式来工作,一般是怎么被调用的?它们的调用如何实现配置化?
- 对于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
- 然后通过paster来管理这些app的执行顺序。这样每个app还可以有自已的配置参数。这样就可以理解为什么tg和pylons要使用paster了。不过对我来说的确是理解复杂。甚至象pkg_resources的使用也是最近才一点点理解并开始使用的。
- Qiangning Hong
WSGI的处理模式为 WSGI Server -> (WSGI Middleware)* -> WSGI Application
- 一次请求只有一个app。middleware和app之间通过environ交换消息。
- YoungKing(Zopen.cn)
- 每个wsgi app是一个callable对象,传递两个参数,一个是environment ,一个是start_response 函数,
- start_response这个函数用来response,
- 你在这里设置status和header,最后返回一个iterator(通常是字符串list)
- 潘俊勇 <[email protected]
- filter/app之间使用environment的消息机制了交换数据的,这种基于消息,而非API的通讯方式,更加底层,可以减少依赖。
多个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
- 可以看到,这里不仅支持wsgi组件关系的部署,还支持各个组件的参数配置
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]
关于dispatch, 在zope中叫做对象漫游traversal,bfg已经将这个转换成了一个可替换的中间件,在这里有说明和比较:
WSGI中间件是如何复用的?
- 潘俊勇 <[email protected]
- 关于profile的middleware,repoze社区有了repoze.profile,这里有个可用的中间件清单:
- 其中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
- Pylons推进Routes成为一个WSGI middleware是一个创举,这样使得框架中的dispatcher也变成可替换的了。
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分成三个部分,
- 一个就叫Paste,是一个组件库,包含几个WSGI server和很多WSGI middleware。
- Paste Script提供一个命令行界面,便于创建项目、启动服务等等。
- Paste Deploy提供一个配置方法,便于系统配置和部署。你如果用不到这些东西,自然不需要。
比如我自己为了在GAE上做东西玩,自己做了一个framework玩,由于只是自己用,不需要考虑配置、互换性等问题,就完全没有使用pastescript/deploy,只用了他一个middleware,代码很简单,一共142行,但基本的功能(URL Dispatch、Template、Authentication)已经可以用了。
- 但uliweb如果要做成一个通用的框架满足更多人的需要的话,可能还是看一下paste比较好。
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的问题集
Limodou:到是可以考虑把uliweb下的expose改造一下,一方面进行url的注册,另一方面将一个view函数封装成一个wsgi的application的东西。
- 这样的话,哪些东西做成wsgi方式的会有意义呢?因为一个功能,依赖越少可能越好。而象view这样的东西,多数与你的后台,逻辑相关,这些东西比较困难。直接把框架的部分功能做成wsgi方式的有难度,因为框架最具特色的可能就是组件的搭配,配置管理,组件管理和开发方式,思想,而这些东西好象也很难wsgi化。而且有些根本与wsgi的处理无关的,象组件的管理,配置管理之类的。现在想不出来uliweb有哪些可能是独持的可以做成wsgi的东西,因为uliweb能实现的功能,别人基本上都有。而且涉及到wsgi的别人的比uliweb的好象还要更多。
黄毅:我想框架对wsgi友好的含义,一是把自己暴露为一个符合标准wsgi app,二是尽可能抽象出并重用对框架app透明的wsgi middleware,就够了。框架肯定会有很多自己的东西,wsgi并不是一个框架。
- Qiangning Hong
- 可重用的部分做成wsgi middleware的方式就是有意义的。把框架内部的各组件都做成WSGI化的,有助于从中提炼出来可重用的部分。
对于Pylons而言,这个框架的终极思想就是没有框架,只作为一个WSGI组件的粘合剂存在,每个人用的Pylons都是不一样的。所以他尽可能的把各个组件都WSGI化,用户根据自己的偏好去搭建。 TurboGears 2.0挑选了自己喜欢的一些组件(比如Genshi之类),作为默认组件,就可以在Pylons上建立起一个有特点的框架。
<<< Limodou ~ 其实没有框架就意味着按照自已的方式来组织框架,就是我说的定制化。只不过只有wsgi还无法完成web的处理,我们还需要象template,orm之类的通用模块,而这些与wsgi没什么关系也是一个问题。所以全部wsgi首先是不可能,完全利用wsgi搭建的也只是处理这一块,还有许多的工作要处理。因此我想定制化不一定要在wsgi之上,可以是在一个初步定制化的基础之上的二次定制。
>>> Qiangning Hong~ template的WSGI化: http://pypi.python.org/pypi/wsgiview/
<<< Limodou ~ 这个是把模板的处理进行了包装,可以返回一个wsgi的application。虽然是一种办法,但是我并不认为到处都是wsgi就可以简化web开发,还不如自动对应模板来得简单。应该把这种处理封装到框架内部,如果让一般的web用户来使用,太麻烦,我感觉不好。
>>> Qiangning Hong~ 我也觉得这个没什么大用,因为基本上到模板的时候,app把应该干的事情都已经干完了,渲染模板仅仅是一个函数调用,所以看不出来把渲染这件事情做成wsgi app有什么实质的好处。贴出来只是说一下有人在做这样的事情。
ORM的WSGI化: http://pypi.python.org/pypi/Alchemyware2.0
<<< Limodou ~ 好象会共享出一个session对象,起到了一个全局化的作用。这也算ORM的wsgi?好象与使用全局模块不是一样吗?
Qiangning Hong~ 这个的作用很大,可以打破不同的middleware对SQLAlchemy全局变量调用可能存在的冲突。如果一个middleware对SQLAlchemy的使用有特殊性,可以放在自己的session里,不和其他的发生冲突。
<<< 黄毅 <[email protected]> ~我觉得上面这两个路子不对,还是认为不是什么东西都可以通过扩展wsgi来做,template已经turbogears已经有一个叫做 buffet 的标准化接口了吧,我觉得那个已经够了
Uliweb如果也要走"不可知"的道路,那最后也是这样的模型。所以我提议uliweb多看看pylons,如果能在pylons之上构架框架,会少走很多弯路,产生出来的结果也会更有意义(正如TurboGears项目贡献出来很多WSGI组件一样)。
<<<Limodou ~ uliweb的不可知只是核心部分,它可以被其他人用来构建定制化的东西。
- 所以一般用户使用的应该是可知的。并且我希望uliweb在工作时,会有一个缺省选择,用户可以更换,但不配置时就是缺省的。
- 不过要完全做到不可知很难,因为你的调用接口总要的统一的,但这些可能只是一个局部的标准,别人不一定能遵守或做到,所以要么采用和别人一样的接口,但是仍然可能有不满足的情况。要么自已来定义接口,由用户来针对不同的组件进行适配。
>>>Qiangning Hong~ 这个就是TurboGears的路线。TurboGears原来就是完全自己做,选择自己喜欢的第三方组件搭建框架,和RoR一样。等发展到一定程度了,用的人多了,就开始有替换组件的需求。弄来弄去,最后发现直接在Pylons上搭建,选择一套默认组件,由Pylons去负责组件的可替换性,TurboGears只需要专注在用户的易用性和功能增强上面就行了,这样的方案对整个社区都有利。所以才有了TurboGears
- 比如我很看好的uliweb的可重用app模型,如果能够独立成一个wsgi app,就可以直接在Pylons里重用了。这样uliweb就成为一个可重用的wsgi的app的集合,而非一个巨型的大wsgi app。
<<<Limodou ~ 这个想法到是很有意思。不过uliweb的app其实就是一个python的包,它包含了静态信息,配置信息,翻译信息等。uliweb在启动时可以根据配置或动态发现它们,然后合并它们的配置信息,提取url等,而这些处理都是由Dispatcher来完成的。所以这些app并不是一个wsgi的应用。不太明白你希望的重用是什么样子,是Dispatcher还是那些个app?
>>> Qiangning Hong ~ 如果uliweb能变成一个wsgi middleware,把它往其他的wsgi app上一套,那些app就能像在uliweb里一样变得可重用了,这个世界就很美妙了
不过我对uliweb理解不深,不知道能不能实现。
<<<Limodou ~ UliWeb的app是一个包,不是一个真正的对象啊,怎么套啊。不是一样的东西
>>> Qiangning Hong ~ 给这个包加上 call(start_response, environ) 试试?
Controller本身是wsgi app有一个好处:
- 即在框架内部也可以直接使用wsgi middlewares来进行定制。
- 比如不同的controller使用不同的authentication机制,我就可以用不同的auth middleware去decorate相应的controller。
- 如果只在整个框架外部才暴露一个wsgi app接口,那内部的这些东西都只能用私有的模式实现,重用度就大大降低了
<<< 黄毅 <[email protected]> ~ 这样就有一个问题,authentication 组件做成 wsgi middleware 的话需要自己去解析请求,但作为一个 app 内部的组件的话,可以由app对请求解析一次,authentication 直接取解析后的数据即可。 除非说要把 request、response对象也标准化,那按照这条路走下去的话,就是要搞一个标准统一所有框架,变成一个大一统的框架而已。
<<<Limodou~ 的确是这样,对于django的auth middleware我很理解,并且它在处理后会将用户对象赋组request上。而authkit是一个wsgi middleware,它如何关心后台的用户机制是数据库,还是ldap,还是什么的,又如何与request相关联的呢?因为用户认证的情况在后面的应用处理也要使用的。不知道它怎么做的?
>>> Qiangning Hong~ 如果不希望在authentication组件里面自己解析request的话,可以在authentication middleware前面套一个解析request的middleware,比如webob。
- 这样这个authentication middleware只依赖于这个request parser middleware,而不是依赖于整个框架。
- 所有其他使用那个request parser middleware,也就是webob的wsgi框架都可以使用这些authentication middleware,可重用性显然要好很多。
BTW,我是希望看到request对象标准化的,webob很有这方面的潜质。
所以我说Pylons社区推进routing标准化是一个创举。
- 可以想象,如果request对象也标准化了,比如都使用和webob相同的接口,那各个框架之间的鸿沟又可以再大大缩小一步,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中只要:
- 就可以了。按你的做法,say_hello其实是需要知道前面使用了selector的。这种我认为是一种假定的方式,可能会有问题。主要还是显得麻烦,对于开发者来说需要了解的东西太多。做为框架完全可以再简化。
- 有时追求复用与代码简化是冲突的,而我是希望用户用起来简单,在一定程度上的复用。而且与业务相关的处理复用本来难度就很大,倒不如考虑将与业务无关的东西复杂,并且减少对外部环境的假定或依赖更好一些。当然,具体还要看到底是什么东西是可以复用,应该如何被复用,也不是件容易的事。
- 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()
完全还可以更简单。比如我前面贴的meilanweb.py里Controller.__call__里面那一堆就是为了让写应用的时候变简单的,写代码时只要操作webob的对象就可以了,不用涉及WSGI的细节。
反馈
创建 by -- ZoomQuiet [2008-12-25 04:10:48]