别人怎样去看待你的价值并不重要,重要是你自己怎样看待自身的价值。即便你是一块货真价实的金子,多说己长也便是短,自知己短便是长。一个人的真正伟大之处,就在于能认识到自己的渺小。
Pecan
框架的目标是实现一个采用对象分发方式进行URL
路由的轻量级Web
框架。它非常专注于自己的目标,它的大部分功能都和URL
路
由以及请求和响应的处理相关,而不去实现模板、安全以及数据库层,这些东西都可以通过其他的库来实现。关于Pecan
的更多信息,可
以 查看文档
pecan 工程创建
pecan源码目录
创建 pecan 工程
一般工程
1 | pecan create test |
创建pecan工程用到了bin下的pecan脚本
1 | // bin/pecan |
调用了pecan/commands/base.py下的CommandRunner类中的handle_command_line()方法
1 | # 具体处理命令 |
1 | # pecan/commands/base.py |
可以看到,pecan创建工程的过程和django类似(其实基本所有的python web框架都是一样的)
下面来看看创建工程具体做了什么工作
1 | # pecan/commands/create.py |
注:通过上面的代码可以知道,默认创建工程时使用的是BaseScaffold类,创建的是一般的pecan工程,而要创建restful的pecan工程,则需要使用下面的方式创建
Restful工程
1 | pecan create test rest-api |
BaseScaffold和RestAPIScaffold调用copy_to(project_name)方法去创建工程,主要工作就是根据传入的项目名创建目录,拷贝文件等,pecan/scaffolds模块中放置了相关的模板文件
至此,一个pecan工程就创建完毕。
运行 pecan 工程
运行方式:
1 | pecan serve config.py |
和创建工程一样,运行时调用了pecan/commands/create.py模块的方法。
1 | def run(self, args): |
众所周知,要运行一个python的web服务,需要两个条件:
- application
- wsgi server
上面的load_app()方法创建了一个application(会调用到core.py里面的load_app()方法)
1 | # pecan/core.py |
app其实就是一个Pecan实例。
对于wsgi server,下面的代码可以看出,pecan使用了python内置的simple_server
1 | # pecan/commands/serve.py |
至此,一个python web服务就运行起来了。
pecan 源码解析
新建pecan工程的默认config.py文件(上面讲过,该文件是从scaffolds中的模板copy的)
1 | # Server Specific Configurations |
路由分析
app是一个Pecan实例,先来看看初始化Pecan实例时做了哪些工作
1 | # core.py文件 |
在core.py中定义了一个全局变量state,它的生命周期和整个请求的生命周期一致,保存了请求过程中各种参数状态值
1 | state = None |
当一个请求从wsgiserver转发过来,首先处理的是Pecan中的call方法
1 | # core.py文件 |
主要调用了find_controller和invoke_controller方法。find_controller根据对象分发机制找到url的处理方法,如果没找到,则抛出异常,由后面的except代码块处理,找到了就调用invoke_controller执行该处理方法,将处理结果保存到state中。
1 | class PecanBase(object): |
钩子程序分为4种,路由前(on_route),路由后处理前(before),处理后(after),发生错误(on_error),钩子程序可在app.py中自定义,需要继承PecanHook类(在hooks.py中定义)
route(req, self.root, path):
req:WebOb的Request对象,存储了请求的信息
self.root:是第一个处理对象(config.py中定义的root对象)
path:路径信息,如:/v1/books
1 | def route(self, req, node, path): |
lookup_controller针对每一个controller对象,在其中查找对应的处理方法,如果没找到,则会继续找_default,如果没定义_default,则找_lookup,然后继续循环调用lookup_controller,直到找到对应的方法,或notfound_handlers 为空抛出异常
obj, remainder = find_object(obj, remainder, notfound_handlers, request)
obj:当前的controller对象
remainder:路由信息,如[‘v1’, ‘books’]
notfound_handlers:该controller中没找到时,存储_default
或者_lookup
request:请求信息
1 | def find_object(obj, remainder, notfound_handlers, request): |
find_object 首先会处理自定义的路由信息,然后存储_default
和_lookup
,最后处理默认路由(个人觉得可以先处理默认路由信息,然后根据是否配置route装饰进行取舍,这样可能处理更高效)
routing.py中的lookup_controller 和 find_object是核心路由方式的实现,从代码中可以看出,最终找到处理方法的方式是根据路径(/v1/books)中每一个segment来查找对应的对象,然后根据当前对象再查找下一个对象,所以pecan的路由机制叫做对象分发
装饰器
装饰器定义在decorators.py中,其中最重要的就是expose,它标识了这个被装饰的方法可以被路由找到
1 | def when_for(controller): |
cfg = _cfg(f)
代码为方法指定了参数_pecan,dict
对象,其中存储了该方法很多重要信息cfg[‘generic_handlers’] = dict(DEFAULT=f)
当generic
为true
时,其中存储了具体的处理方法,{‘generic_handlers’:{‘DEFAULT’:function,‘POST’:function,‘PUT’:function}}
,当请求时POST,PUT,DELETE
等方式时,就是从其中获取处理方法f.when = when_for(f)
1 | # 看似是对index_post方法进行装饰,但是主要还是对index方法进行处理 |
根据POST,PUT,DELETE路由
在routing.py
中find_object
方法会返回找到的subcontroller
,它是有@expose装饰的一个方法
1 | def find_object(obj, remainder, notfound_handlers, request): |
在core.py中根据POST具体找到相应的方法
1 | def find_controller(self, state): |
所以最终找到处理方法是在core.py中,其实这里我认为处理的不好,还是应该在routing.py中处理
这里有几个写的不好的地方:
- 当请求为/v1//books这种不标准的形式的时候,pecan的路由机制是没法处理的
1 | try: |
1 | def route(self, req, node, path): |
- 对于 POST /v1/books/safgrgfwsfrsg (最后的一个segment没有定义,并且没有定义_lookup和_default),这是依然能够找到路由方法,但是会在参数处理的时候报错,这个地方不合理
1 | def find_object(obj, remainder, notfound_handlers, request): |
上面3点对源码的改动可以完成我们自定义的一些需求,并且是pecan的代码结构更加合理。
pecan 控制器和路由系统
对于 pecan 的路由分发机制还是有必要再分析一下
Pecan路由采用的是对象分发机制,将HTTP请求分发到控制器,然后到控制器里定义的方法。
对象分发机制将请求路径进行切割,根据请求路径从root控制器开始,按次序寻找路径对应的控制器及方法。
1 | from pecan import expose |
对于上述代码,如果此时有这样一个请求:/catalog/books/bestsellers,则pecan首先将这个请求分割成:catalog, books, bestsellers。接下来,pecan将会从root控制器中寻找catalog,找到catalog对象后,pecan会继续在catalog控制器中寻找books,以此类推一直找到bestsellers。如果URL以’/‘结束,那么pecan将会查找最后一个控制器的index方法。
进一步讲,下面的这些请求路径:
1 | └── / |
将会路由给这些控制器方法:
1 | └── RootController.index |
路由算法
有时,标准的对象分发路由方式不足以将某个URL路由到一个控制器上。pecan提供了几种方法去使对象分发方式的路由发生短路,以便用更多的控制来处理URL,以下这些特殊的方法用来实现这个目标:_lookup(),_default(),_route()
。在你的控制器上定义这些方法可以让你更加灵活的处理一个URL的全部内容或部分内容。
Controller 增加方法处理路由
我们需要不同的路由返回不同的内容。这里我们介绍一种Pecan注册路由的方法。RootController加一个方法叫做diff。
1 | # /usr/bin/env python |
增加了diff方法,装饰器不要忘了,怎么访问这个不同的路径呢,很简单:
1 | http://127.0.0.1:5000/diff |
我们可以通过添加不同的方法名,来处理不同的路由,返回不同的结果。Pecan会根据路由查看,你这个控制器有没有对应的属性,有的话就交给这个属性方法处理。
Controller 增加属性处理路由
我们已经知道了一种注册路由的方法了,现在介绍第二种。
更改代码,这次我们原有的文件结构不变,给root.py增加点东西,增加后变成下面这样
1 | # /usr/bin/env python |
两点改变:
- 增加BookController这个类的定义
- RootController增加一个属性 book = BookController()
1 | 浏览器输入http://127.0.0.1:5000/book/ |
这次的原理,其实是个上一个添加路由的方法是一样的,Pecan会根据路由查看,你这个控制器有没有对应的属性,有的话就交给这个属性方法处理。上一个是添加了一个成员方法,这次是一个成员属性而已。
如果我们访问:
1 | http://127.0.0.1:5000/book/sea 会是什么样呢? |
其实原理是这么回事的:
Pecan会把路由分成[“book”, “sea”]
从RootController去发现有没有能处理book的Controller。发现RooController有一个book属性值为BookController的对象。
Pecan从BookController的对象去发现有没有能处理sea的Controller。找了一圈发现没有,这就尴尬了处理不了!但是为啥没有报404呢?
因为在第一步的时候,Pecan不仅会去发现有没有能处理book的Controller,同时还会去检查RootController有没有一些应对没找到Controller时处理的方法。这些方法可以是_defalut
(这个我们已经用过了),_lookup
(这个后面在讲)。发现有_default
,会将_default
放入一个列表,待用。第二步以后发现,没有能处理的Controller,所以调用了RootController对象的_default
的方法,我们就看到了“Hello World from root default”
如果在 BookController 放个 _default 呢?
1 | class BookController(object): |
1 | 再访问http://127.0.0.1:5000/book/sea 会是什么样呢? |
为啥是这个结果呢?
第一步的时候,会把
RooController()._default
,放入一个待用队列,源码里叫做notfound_handlers=[RooController()._default]
第二步的时候,虽然没有发现能处理sea的控制器,也会去查找一个有没有
_default,_look_up
。发现有_default
,也会放入队列。变成notfound_handlers=[RooController()._default, BookController()._default]
。
当最终没有能处理[‘book’, ‘sea’]时,开始notfound_handlers
上场。他会倒着拿里面的值,就是notfound_handlers.pop()
。所以会BookController()._default
处理。
_lookup()
_lookup()
提供一种方式处理一个URL的部分内容,并返回一个新的控制器用于处理URL的剩余部分。一个_lookup()
方法可以提供一个或多个参数,以及URL的分片。同时_lookup()方法应该用可变的位置表示URL的剩余部分,并且在它的返回值里包含未处理的剩余URL部分。在下面的例子中,对象分发路由算法将会把remainder列表传递给该方法返回的控制器。
_lookup()
除了被用来动态创建控制器以外,当没有其他任何控制器方法能够匹配一个URL且没有定义_default()
方法时,_lookup()
方法作为最后一个方法被调用。
此方法返回一个新的控制器用于控制url的剩余部分,如下代码:
1 | def get_student_by_primary_key(num): |
例如”/1/name”路径,将走_lookup()方法,返回 StudentController(student), remainder;
此时,StudentController(student) 是一个新的控制器,而remainder是url的剩余部分,即 name;
StudentController() 控制器将找到def name(self) 进而响应请求。
同理,对于”/100/addr”,也走_lookup()方法,返回 StudentController(student), remainder;
此时,StudentController(student) 是一个新的控制器,而remainder是url的剩余部分,即 addr;
找到addr = Addr(),进而得到响应。
_default()
对于标准的对象路由分发机制,当没有任何控制器可以处理url是,_default将要作为最后一个方法被调度。
1 | from functools import wraps |
1 | class RootController(object): |
_default方法是最后被调用的。
_route()方法
_route()
方法允许一个控制器完全覆盖pecan的路由机制。pecan它本身也使用_route()
方法去实现它的RestController。如果你想在pecan之上定义一套替代的路由机制,那么定义一个包含_route()
方法的基控制器将会使你完全掌控请求的路由。
expose 暴露控制器方法
expose告诉Pecan类中的哪些方法是公开可见的 。如果一个方法没有用修饰expose(),Pecan永远不会将请求路由到它。
pecan默认采用expose进行路由绑定,需要路由控制器类的方法都要经过expose装饰器的装饰,pecan就可以使HTTP请求找到对应的方法。
不同的使用方法会有不同的效果,如下:
@expose()
1 | from pecan import expose |
被装饰的方法需要返回一个字符串,表示HTML响应的body。
@expose(html_template_name)
1 | from pecan import expose |
1 | <!-- html_template.mako --> |
被装饰的方法返回一个字典,字典的key可以在html模板中使用${key}
的方式引用。
@expose(route=’some-path’)
例如有这样一个请求:/some-path,由于python语法限制,pecan并不能将该请求的处理方法声明为some-path。使用@expose(route=’some-path’),被装饰方法将响应/some-path
请求。
1 | class RootController(object): |
注意:尽量不使用dict(),使用不当,HTTP状态码将是204,及服务器没有返回任何内容错误。
另一种方式:pecan.route()
1 | class RootController(object): |
延伸:利用route()方法来将请求路由给下一级控制器
1 | class ChildController(object): |
在这个例子中,pecan应用将会给请求/child-path/child/返回HTTP 200响应。
@expose(generic=True)
expose()方法中的generic参数可以根据请求方法对URL进行重载,即一个url路径可以被两个不同的方法处理。
1 | class RootController(object): |
对于”/“的GET请求,由index()方法处理;对于”/“的POST请求,由index_POST方法处理。
根据请求方法来路由,其实还有一种方式,继承pecan.rest.RestController
来实现
但是这里有个坑,就是不能实现index
方法,不然会被覆盖,具体用法看下:
Writing RESTful Web Services with Generic Controllers
@expose()叠加用法
1 | class RootController(object): |
叠加使用后一个hello
方法可以响应三种格式的请求(application/json, text/plain, text/html)。
- 当客户端请求/hello.json或者http header中包含“Accept: application/json”时,将hello()方法响应的名字空间渲染进json文本,及@expose(‘json’)用法;
- 当客户端请求/hello.txt或者http header中包含“Accept: text/plain”时,使用text_template.mako模板文件响应,即@expose(‘text_template.mako’, content_type=’text/plain’)用法;
- 当客户端请求/hello.html时,使用html_template.mako模板文件。如果客户端请求/hello,并且没有显式指明内容格式,则pecan默认使用text/html的内容格式进行响应,假设客户端想要HTML。
pecan 使用示例
配置文件test.ini
1 | [composite:hello] |
main.py简单文件
1 | import wsgiref.simple_server as wss |
配置文件中,构建一个叫hello的composite,使用一个叫hello的pipeline。pipeline使用Controller作为控制器。
build_wsgi_app 加载配置文件。
运行server
1 | python main.py |
参考: