Pecan 框架使用及源码分析

别人怎样去看待你的价值并不重要,重要是你自己怎样看待自身的价值。即便你是一块货真价实的金子,多说己长也便是短,自知己短便是长。一个人的真正伟大之处,就在于能认识到自己的渺小。

Pecan框架的目标是实现一个采用对象分发方式进行URL路由的轻量级Web框架。它非常专注于自己的目标,它的大部分功能都和URL

由以及请求和响应的处理相关,而不去实现模板、安全以及数据库层,这些东西都可以通过其他的库来实现。关于Pecan的更多信息,可

查看文档

pecan 工程创建

pecan源码目录

在这里插入图片描述

创建 pecan 工程

一般工程

1
pecan create test

创建pecan工程用到了bin下的pecan脚本

1
2
3
4
5
// bin/pecan
#!/usr/bin/env python
if __name__ == '__main__':
from pecan.commands import CommandRunner
CommandRunner.handle_command_line()

调用了pecan/commands/base.py下的CommandRunner类中的handle_command_line()方法

1
2
3
4
5
6
7
8
9
# 具体处理命令
def run(self, args):
ns = self.parser.parse_args(args) # 解析参数
self.commands[ns.command_name]().run(ns) # 执行命令,相当于执行commands下相应模块中的run方法

@classmethod
def handle_command_line(cls):
runner = CommandRunner() # 实例化该对象时,加载commands文件夹下定义的名命令
runner.run(sys.argv[1:])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# pecan/commands/base.py
# 加载command的方法,entry point在setup.py里面注册了
def load_commands(self):
for ep in pkg_resources.iter_entry_points('pecan.command'):
log.debug('%s loading plugin %s', self.__class__.__name__, ep)
if ep.name in self.commands:
warn(
"Duplicate entry points found on `%s` - ignoring %s" % (
ep.name,
ep
),
RuntimeWarning
)
continue
try:
# 该方法是去加载模块中含有run方法的类
cmd = ep.load()
cmd.run # 确保加载的类有run方法
except Exception as e: # pragma: nocover
warn("Unable to load plugin %s: %s" % (ep, e), RuntimeWarning)
continue
self.add({ep.name: cmd}) # 添加到self.commands字典里面,上面的run运行时,从字典中取出类,实例化,然后调用run方法

可以看到,pecan创建工程的过程和django类似(其实基本所有的python web框架都是一样的)

下面来看看创建工程具体做了什么工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#  pecan/commands/create.py

class ScaffoldManager(object):
def __init__(self):
self.scaffolds = {}
self.load_scaffolds()
def load_scaffolds(self):
# 加载在setup.py文件中注册了的类
for ep in pkg_resources.iter_entry_points('pecan.scaffold'):
log.debug('%s loading scaffold %s', self.__class__.__name__, ep)
try:
cmd = ep.load()
cmd.copy_to # 确保有copy_to方法
except Exception as e: # pragma: nocover
warn(
"Unable to load scaffold %s: %s" % (ep, e), RuntimeWarning
)
continue
self.add({ep.name: cmd})
def add(self, cmd):
self.scaffolds.update(cmd)


class CreateCommand(BaseCommand):
# 通过实例化该对象,加载pecan/scaffolds下的BaseScaffold和RestAPIScaffold类,加载方法和加载command方法一样
manager = ScaffoldManager()
arguments = ({
'name': 'project_name',
'help': 'the (package) name of the new project'
}, {
'name': 'template_name',
'metavar': 'template_name',
'help': 'a registered Pecan template',
'nargs': '?',
'default': DEFAULT_SCAFFOLD, # 值为base,会调用BaseScaffold的实例
'choices': manager.scaffolds.keys()
})
# 该方法会首先执行
def run(self, args):
super(CreateCommand, self).run(args)
# self.manager.scaffolds为{'rest-api': <class 'pecan.scaffolds.RestAPIScaffold'>, 'base': <class 'pecan.scaffolds.BaseScaffold'>}
# 具体由RestAPIScaffold和BaseScaffold去创建工程
self.manager.scaffolds[args.template_name]().copy_to(
args.project_name # 项目名:test
)

注:通过上面的代码可以知道,默认创建工程时使用的是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
2
3
4
def run(self, args):
super(ServeCommand, self).run(args)
app = self.load_app() # 创建一个app
self.serve(app, app.config) # 部署app

众所周知,要运行一个python的web服务,需要两个条件:

  • application
  • wsgi server

上面的load_app()方法创建了一个application(会调用到core.py里面的load_app()方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# pecan/core.py
def load_app(config, **kwargs):
from .configuration import _runtime_conf, set_config
# 根据配置文件初始化配置
set_config(config, overwrite=True)
for package_name in getattr(_runtime_conf.app, 'modules', []):
module = __import__(package_name, fromlist=['app'])
# 项目工程创建好后,有个test/test/app.py文件,里面的setup_app(config)具体来创建app
if hasattr(module, 'app') and hasattr(module.app, 'setup_app'):
# 这里开始创建一个app
app = module.app.setup_app(_runtime_conf, **kwargs)
app.config = _runtime_conf
return app
raise RuntimeError(
'No app.setup_app found in any of the configured app.modules'
)

# test/test/app.py
def setup_app(config):
model.init_model() # 创建app时,初始化model,一般在这实现orm映射
app_conf = dict(config.app)
# test/conifg.py文件中有app配置项,定义了app的重要内容
return make_app(
app_conf.pop('root'),
logging=getattr(config, 'logging', {}),
**app_conf
)

# pecan/__init__.py
def make_app(root, **kw):
...
# root是test/config.py中app项里面配置的,作为路由的入口
app = Pecan(root, **kw)
...
return app

app其实就是一个Pecan实例。

对于wsgi server,下面的代码可以看出,pecan使用了python内置的simple_server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# pecan/commands/serve.py
def serve(self, app, conf):
if self.args.reload:
try:
# 使用了watchdog,用于修改文件后自动部署(暂不做讲解)
self.watch_and_spawn(conf)
except ImportError:
print('The `--reload` option requires `watchdog` to be '
'installed.')
print(' $ pip install watchdog')
else:
# 这里就是具体部署app的方法
self._serve(app, conf)

def _serve(self, app, conf):
# 引入python内置simple_server
from wsgiref.simple_server import make_server
host, port = conf.server.host, int(conf.server.port)
srv = make_server(
host,
port,
app, # 创建的app
handler_class=PecanWSGIRequestHandler, # 处理器
)
print('Starting server in PID %s' % os.getpid())
if host == '0.0.0.0':
print(
'serving on 0.0.0.0:%s, view at http://127.0.0.1:%s' %
(port, port)
)
else:
print("serving on http://%s:%s" % (host, port))
try:
# 运行web服务
srv.serve_forever()
except KeyboardInterrupt:
# allow CTRL+C to shutdown
pass

至此,一个python web服务就运行起来了。

pecan 源码解析

新建pecan工程的默认config.py文件(上面讲过,该文件是从scaffolds中的模板copy的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# Server Specific Configurations
# 启动的默认的host和port
server = {
'port': '8080',
'host': '0.0.0.0'
}

# Pecan Application Configurations
# app的配置
app = {
'root': '${package}.controllers.root.RootController', # root项很重要,指定开始路由的处理器
'modules': ['${package}'],
'debug': True
# 还可以配置 static_root、template_path、errors 等。
}

# python格式的日志配置
logging = {
'root': {'level': 'INFO', 'handlers': ['console']},
'loggers': {
'${package}': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False},
'pecan': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False},
'py.warnings': {'handlers': ['console']},
'__force_dict__': True
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'color'
}
},
'formatters': {
'simple': {
'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]'
'[%(threadName)s] %(message)s')
},
'color': {
'()': 'pecan.log.ColorFormatter',
'format': ('%(asctime)s [%(padded_color_levelname)s] [%(name)s]'
'[%(threadName)s] %(message)s'),
'__force_dict__': True
}
}
}

# Custom Configurations must be in Python dictionary format::
#
# foo = {'bar':'baz'}
#
# All configurations are accessible at::
# pecan.conf

路由分析

app是一个Pecan实例,先来看看初始化Pecan实例时做了哪些工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# core.py文件

class Pecan(PecanBase):
def __new__(cls, *args, **kw):
if kw.get('use_context_locals') is False:
self = super(Pecan, cls).__new__(ExplicitPecan, *args, **kw)
self.__init__(*args, **kw)
return self
return super(Pecan, cls).__new__(cls)

def __init__(self, *args, **kw):
self.init_context_local(kw.get('context_local_factory'))
# 调用了PecanBase的初始化方法
super(Pecan, self).__init__(*args, **kw)

......
......
class PecanBase(object):
def __init__(self, root, default_renderer='mako',
template_path='templates', hooks=lambda: [],
custom_renderers={}, extra_template_vars={},
force_canonical=True, guess_content_type_from_ext=True,
context_local_factory=None, request_cls=Request,
response_cls=Response, **kw):
# 这里将配置文件中的root配置项导入为一个python对象
if isinstance(root, six.string_types):
root = self.__translate_root__(root)
self.root = root # url中的路径,比如:/v1/books
self.request_cls = request_cls # webob的Request类,所以pecan以来WebOb
self.response_cls = response_cls # webob的Response类
self.renderers = RendererFactory(custom_renderers, extra_template_vars) # 渲染器
self.default_renderer = default_renderer
# 初始化钩子程序
if six.callable(hooks):
hooks = hooks()
self.hooks = list(sorted(
hooks,
key=operator.attrgetter('priority')
))
self.template_path = template_path
self.force_canonical = force_canonical
self.guess_content_type_from_ext = guess_content_type_from_ext

在core.py中定义了一个全局变量state,它的生命周期和整个请求的生命周期一致,保存了请求过程中各种参数状态值

1
state = None

当一个请求从wsgiserver转发过来,首先处理的是Pecan中的call方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# core.py文件

class Pecan(PecanBase):
def __call__(self, environ, start_response):
try:
state.hooks = []
state.app = self
state.controller = None
state.arguments = None
# 调用了PecanBase的__call__方法
return super(Pecan, self).__call__(environ, start_response)
finally:
del state.hooks # 存储钩子程序的list
del state.request # 请求对象
del state.response # 响应对象
del state.controller # 处理器(程序中被expose装饰的方法)
del state.arguments # 参数
del state.app # Peacn对象

class PecanBase(object):
def __call__(self, environ, start_response):
# WebOb的Request和Response
req = self.request_cls(environ)
resp = self.response_cls()

state = RoutingState(req, resp, self)
environ['pecan.locals'] = {
'request': req,
'response': resp
}
controller = None
internal_redirect = False
try:
req.context = environ.get('pecan.recursive.context', {})
req.pecan = dict(content_type=None)
# 路由方法,对象分发路由机制,传入state,记录整个过程中的状态
controller, args, kwargs = self.find_controller(state)
# 调用处理方法
self.invoke_controller(controller, args, kwargs, state)
except Exception as e:
......
......

self._handle_empty_response_body(state)
# 返回结果
return state.response(environ, start_response)

主要调用了find_controller和invoke_controller方法。find_controller根据对象分发机制找到url的处理方法,如果没找到,则抛出异常,由后面的except代码块处理,找到了就调用invoke_controller执行该处理方法,将处理结果保存到state中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class PecanBase(object):
def find_controller(self, state):
req = state.request
pecan_state = req.pecan
pecan_state['routing_path'] = path = req.path_info
# 处理钩子程序
self.handle_hooks(self.hooks, 'on_route', state)
......
......
# 具体路由方法
controller, remainder = self.route(req, self.root, path)
......
......
# 根据路由结果处理参数,如果没有路由到,则该方法会抛出异常
args, varargs, kwargs = self.get_args(
state,
params.mixed(),
remainder,
cfg['argspec'],
im_self
)
state.arguments = Arguments(args, varargs, kwargs)
# 处理钩子程序
self.handle_hooks(self.determine_hooks(controller), 'before', state)

return controller, args + varargs, kwargs

钩子程序分为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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def route(self, req, node, path):
path = path.split('/')[1:]
# 转化后的路径 path:['v1','boos']
try:
# 调用了routing.py文件中的lookup_controller方法
node, remainder = lookup_controller(node, path, req)
return node, remainder
except NonCanonicalPath as e:
if self.force_canonical and \
not _cfg(e.controller).get('accept_noncanonical', False):
if req.method == 'POST':
raise RuntimeError(
"You have POSTed to a URL '%s' which "
"requires a slash. Most browsers will not maintain "
"POST data when redirected. Please update your code "
"to POST to '%s/' or set force_canonical to False" %
(req.pecan['routing_path'],
req.pecan['routing_path'])
)
redirect(code=302, add_slash=True, request=req)
return e.controller, e.remainder

# routing.py文件
def lookup_controller(obj, remainder, request=None):
......
......
# 存储在obj中未找到处理方法时的_default,_lookup
notfound_handlers = []
while True:
try:
obj, remainder = find_object(obj, remainder, notfound_handlers,
request)
handle_security(obj)
return obj, remainder
except (exc.HTTPNotFound, exc.HTTPMethodNotAllowed,
PecanNotFound) as e:
if isinstance(e, PecanNotFound):
e = exc.HTTPNotFound()
while notfound_handlers:
name, obj, remainder = notfound_handlers.pop()
if name == '_default':
return obj, remainder
else:
result = handle_lookup_traversal(obj, remainder)
if result:
if (
remainder == [''] and
len(obj._pecan['argspec'].args) > 1
):
raise e
obj_, remainder_ = result
return lookup_controller(obj_, remainder_, request)
else:
raise e

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
def find_object(obj, remainder, notfound_handlers, request):
prev_obj = None
while True:
if obj is None:
raise PecanNotFound
# 如果传入的obj直接是一个处理方法(被expsoe装饰),直接返回
if iscontroller(obj):
if getattr(obj, 'custom_route', None) is None:
return obj, remainder
# 处理自定义路由信息
_detect_custom_path_segments(obj)

# 根据自定义路由名找到处理方法
if remainder:
custom_route = __custom_routes__.get((obj.__class__, remainder[0]))
if custom_route:
return getattr(obj, custom_route), remainder[1:]
cross_boundary(prev_obj, obj)
# 如果根据默认和自定义路由都没找到,则找该controller中的index方法
# 如果有路由:/v1/books//best(不标准的路径),那么就只能路由到/v1/books,后面的就没法路由
try:
next_obj, rest = remainder[0], remainder[1:]
if next_obj == '':
index = getattr(obj, 'index', None)
if iscontroller(index):
return index, rest
except IndexError:
index = getattr(obj, 'index', None)
if iscontroller(index):
raise NonCanonicalPath(index, [])
# 存储_default方法到notfound_handlers
default = getattr(obj, '_default', None)
if iscontroller(default):
notfound_handlers.append(('_default', default, remainder))
# 则存储_lookup方法到notfound_handlers
lookup = getattr(obj, '_lookup', None)
if iscontroller(lookup):
notfound_handlers.append(('_lookup', lookup, remainder))
# 根据自定义的_route方法来处理路由(pecan允许开发者在controller中自定义_route方法,让开发者完全掌控路由方式)
route = getattr(obj, '_route', None)
if iscontroller(route):
if len(getargspec(route).args) == 2:
warnings.warn(
(
"The function signature for %s.%s._route is changing "
"in the next version of pecan.\nPlease update to: "
"`def _route(self, args, request)`." % (
obj.__class__.__module__,
obj.__class__.__name__
)
),
DeprecationWarning
)
next_obj, next_remainder = route(remainder)
else:
next_obj, next_remainder = route(remainder, request)
cross_boundary(route, next_obj)
return next_obj, next_remainder

if not remainder:
raise PecanNotFound

prev_remainder = remainder
prev_obj = obj
remainder = rest
# 根据方法名(或者属性名)(默认的路由方式)找到处理方法
try:
obj = getattr(obj, next_obj, None)
except UnicodeEncodeError:
obj = None
if not obj and not notfound_handlers and hasattr(prev_obj, 'index'):
if request.method in _cfg(prev_obj.index).get('generic_handlers',
{}):
return prev_obj.index, prev_remainder

find_object 首先会处理自定义的路由信息,然后存储_default_lookup,最后处理默认路由(个人觉得可以先处理默认路由信息,然后根据是否配置route装饰进行取舍,这样可能处理更高效)

routing.py中的lookup_controller 和 find_object是核心路由方式的实现,从代码中可以看出,最终找到处理方法的方式是根据路径(/v1/books)中每一个segment来查找对应的对象,然后根据当前对象再查找下一个对象,所以pecan的路由机制叫做对象分发

装饰器

装饰器定义在decorators.py中,其中最重要的就是expose,它标识了这个被装饰的方法可以被路由找到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def when_for(controller):
def when(method, **kw):
def decorate(f):
_cfg(f)['generic_handler'] = True
controller._pecan['generic_handlers'][method.upper()] = f
controller._pecan['allowed_methods'].append(method.upper())
expose(**kw)(f)
return f
return decorate
return when

# template:标识了渲染模板,默认moko,
# generic:默认的方法处理所有类型的请求(GET,POST,PUT,DELETE),如果为true,则各种类型的请求分开处理
# route:自定义路由
def expose(template=None,
generic=False,
route=None,
**kw):
content_type = kw.get('content_type', 'text/html')
if template == 'json':
content_type = 'application/json'
def decorate(f):
# flag the method as exposed
f.exposed = True

cfg = _cfg(f)
cfg['explicit_content_type'] = 'content_type' in kw

if route:
# This import is here to avoid a circular import issue
from pecan import routing
if cfg.get('generic_handler'):
raise ValueError(
'Path segments cannot be overridden for generic '
'controllers.'
)
routing.route(route, f)

# set a "pecan" attribute, where we will store details
cfg['content_type'] = content_type
cfg.setdefault('template', []).append(template)
cfg.setdefault('content_types', {})[content_type] = template

# handle generic controllers
if generic:
if f.__name__ in ('_default', '_lookup', '_route'):
raise ValueError(
'The special method %s cannot be used as a generic '
'controller' % f.__name__
)
cfg['generic'] = True
cfg['generic_handlers'] = dict(DEFAULT=f)
cfg['allowed_methods'] = []
# 方法可以被类似@index.when()装饰
f.when = when_for(f)

# store the arguments for this controller method
# 参数
cfg['argspec'] = getargspec(f)

return f
return decorate
  1. cfg = _cfg(f)代码为方法指定了参数_pecan,dict对象,其中存储了该方法很多重要信息

  2. cfg[‘generic_handlers’] = dict(DEFAULT=f)generictrue时,其中存储了具体的处理方法,{‘generic_handlers’:{‘DEFAULT’:function,‘POST’:function,‘PUT’:function}},当请求时POST,PUT,DELETE等方式时,就是从其中获取处理方法

  3. f.when = when_for(f)

1
2
3
4
5
6
# 看似是对index_post方法进行装饰,但是主要还是对index方法进行处理
# 这里把index_post方法添加到index方法的_pecan['generic_handlers']中
# 这个写法很有意思,大家可以借鉴
@index.when(method='POST')
def index_post(self):
pass

根据POST,PUT,DELETE路由

routing.pyfind_object方法会返回找到的subcontroller,它是有@expose装饰的一个方法

1
2
3
4
5
6
7
8
9
10
11
12
def find_object(obj, remainder, notfound_handlers, request):
...
...

# Last-ditch effort: if there's not a matching subcontroller, no
# `_default`, no `_lookup`, and no `_route`, look to see if there's
# an `index` that has a generic method defined for the current request
# method.
if not obj and not notfound_handlers and hasattr(prev_obj, 'index'):
if request.method in _cfg(prev_obj.index).get('generic_handlers',
{}):
return prev_obj.index, prev_remainder

在core.py中根据POST具体找到相应的方法

1
2
3
4
5
6
7
8
9
10
11
def find_controller(self, state):
...
...

if cfg.get('generic'):
im_self = six.get_method_self(controller)
handlers = cfg['generic_handlers']
# 根据POST找到处理方法
controller = handlers.get(req.method, handlers['DEFAULT'])
handle_security(controller, im_self)
cfg = _cfg(controller)

所以最终找到处理方法是在core.py中,其实这里我认为处理的不好,还是应该在routing.py中处理

这里有几个写的不好的地方:

  1. 当请求为/v1//books这种不标准的形式的时候,pecan的路由机制是没法处理的
1
2
3
4
5
6
7
try:
next_obj, rest = remainder[0], remainder[1:]
# 可以将这里改成 if next_obj == '' and not rest 解决该问题
if next_obj == '':
index = getattr(obj, 'index', None)
if iscontroller(index):
return index, rest
1
2
3
def route(self, req, node, path):
# 可以将list中最后的空字符串删除,这个需和第一点配合
path = path.split('/')[1:]
  1. 对于 POST /v1/books/safgrgfwsfrsg (最后的一个segment没有定义,并且没有定义_lookup和_default),这是依然能够找到路由方法,但是会在参数处理的时候报错,这个地方不合理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def find_object(obj, remainder, notfound_handlers, request):
...
...
try:
obj = getattr(obj, next_obj, None)
except UnicodeEncodeError:
obj = None

# 添加这段代码规避该问题
###########
if not obj and not notfound_handlers and remainder:
abort(404)
##########

# Last-ditch effort: if there's not a matching subcontroller, no
# `_default`, no `_lookup`, and no `_route`, look to see if there's
# an `index` that has a generic method defined for the current request
# method.
if not obj and not notfound_handlers and hasattr(prev_obj, 'index'):
if request.method in _cfg(prev_obj.index).get('generic_handlers',
{}):
return prev_obj.index, prev_remainder

上面3点对源码的改动可以完成我们自定义的一些需求,并且是pecan的代码结构更加合理。

pecan 控制器和路由系统

对于 pecan 的路由分发机制还是有必要再分析一下

Pecan路由采用的是对象分发机制,将HTTP请求分发到控制器,然后到控制器里定义的方法。

对象分发机制将请求路径进行切割,根据请求路径从root控制器开始,按次序寻找路径对应的控制器及方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from pecan import expose

class BooksController(object):
@expose()
def index(self):
return "Welcome to book section."

@expose()
def bestsellers(self):
return "We have 5 books in the top 10."

class CatalogController(object):
@expose()
def index(self):
return "Welcome to the catalog."

books = BooksController()

class RootController(object):
@expose()
def index(self):
return "Welcome to store.example.com!"

@expose()
def hours(self):
return "Open 24/7 on the web."

catalog = CatalogController()

对于上述代码,如果此时有这样一个请求:/catalog/books/bestsellers,则pecan首先将这个请求分割成:catalog, books, bestsellers。接下来,pecan将会从root控制器中寻找catalog,找到catalog对象后,pecan会继续在catalog控制器中寻找books,以此类推一直找到bestsellers。如果URL以’/‘结束,那么pecan将会查找最后一个控制器的index方法。

进一步讲,下面的这些请求路径:

1
2
3
4
5
└── /
├── /hours
└── /catalog
└── /catalog/books
└── /catalog/books/bestsellers

将会路由给这些控制器方法:

1
2
3
4
5
└── RootController.index
├── RootController.hours
└── CatalogController.index
└── BooksController.index
└── BooksController.bestsellers

路由算法

有时,标准的对象分发路由方式不足以将某个URL路由到一个控制器上。pecan提供了几种方法去使对象分发方式的路由发生短路,以便用更多的控制来处理URL,以下这些特殊的方法用来实现这个目标:_lookup(),_default(),_route()。在你的控制器上定义这些方法可以让你更加灵活的处理一个URL的全部内容或部分内容。

Controller 增加方法处理路由

我们需要不同的路由返回不同的内容。这里我们介绍一种Pecan注册路由的方法。RootController加一个方法叫做diff。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# /usr/bin/env python
# coding=utf-8

from utils import happy_expose

class RootController(object):

@happy_expose
def index(self):
return "Hello World"

@happy_expose
def _default(self, *remainder):
return 'Hello World from root default'

@happy_expose
def diff(self):
return 'You find different worlds'

增加了diff方法,装饰器不要忘了,怎么访问这个不同的路径呢,很简单:

1
2
3
http://127.0.0.1:5000/diff

页面显示 You find different worlds

我们可以通过添加不同的方法名,来处理不同的路由,返回不同的结果。Pecan会根据路由查看,你这个控制器有没有对应的属性,有的话就交给这个属性方法处理。

Controller 增加属性处理路由

我们已经知道了一种注册路由的方法了,现在介绍第二种。

更改代码,这次我们原有的文件结构不变,给root.py增加点东西,增加后变成下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# /usr/bin/env python
# coding=utf-8

from utils import happy_expose


class BookController(object):

@happy_expose
def index(self):
return "Welcome to the Sea of Books"


class RootController(object):

book = BookController()

@happy_expose
def index(self):
return "Hello World"

@happy_expose
def _default(self, *remainder):
return 'Hello World from root default'

@happy_expose
def diff(self):
return 'You find different worlds'

两点改变:

  • 增加BookController这个类的定义
  • RootController增加一个属性 book = BookController()
1
2
3
4
5
浏览器输入http://127.0.0.1:5000/book/

页面显示 Welcome to the Sea of Books

我们如愿看到,“Welcome to the Sea of Books”

这次的原理,其实是个上一个添加路由的方法是一样的,Pecan会根据路由查看,你这个控制器有没有对应的属性,有的话就交给这个属性方法处理。上一个是添加了一个成员方法,这次是一个成员属性而已。

如果我们访问:

1
2
3
4
5
http://127.0.0.1:5000/book/sea 会是什么样呢?

页面显示 Hello World from root default

咦,怎么进了RootController的_default了呢?!

其实原理是这么回事的:

Pecan会把路由分成[“book”, “sea”]

  1. 从RootController去发现有没有能处理book的Controller。发现RooController有一个book属性值为BookController的对象。

  2. 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
2
3
4
5
6
7
8
9
class BookController(object):

@happy_expose
def index(self):
return "Welcome to the Sea of Books"

@happy_expose
def _default(self, *remainder):
return "This is Book default"
1
2
3
再访问http://127.0.0.1:5000/book/sea 会是什么样呢?

页面显示 This is Book default

为啥是这个结果呢?

  1. 第一步的时候,会把RooController()._default,放入一个待用队列,源码里叫做notfound_handlers=[RooController()._default]

  2. 第二步的时候,虽然没有发现能处理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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def get_student_by_primary_key(num):
a = ["xiao_ming", "xiao_li"]
num = int(num) if type(num) == int and len(a) > int(num) else 0
return a[num]


class Addr(object):
@expose()
def index(self):
return "addr"


class StudentController(object):
def __init__(self, student):
self.student = student

@expose()
def name(self):
return self.student

addr = Addr()


class RootController(object):

@expose()
def _lookup(self, primary_key, *remainder):
student = get_student_by_primary_key(primary_key)
if student:
return StudentController(student), remainder
else:
return "404"

例如”/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
2
3
4
5
6
7
8
9
10
11
12
13
from functools import wraps

def happy_expose(f=None, **kw):
if f is None:
def inner_expose(func):
return happy_expose(func, **kw)
return inner_expose
else:
@wraps(f)
@expose(**kw)
def _expose(*args, **kwargs):
return f(*args, **kwargs)
return _expose
1
2
3
4
5
6
7
8
9
class RootController(object):

@expose()
def hours(self):
return "Open 24/7 on the web."

@happy_expose
def _default(self, *remainder):
return 'Hello World from root default'

_default方法是最后被调用的。

_route()方法

_route()方法允许一个控制器完全覆盖pecan的路由机制。pecan它本身也使用_route()方法去实现它的RestController。如果你想在pecan之上定义一套替代的路由机制,那么定义一个包含_route()方法的基控制器将会使你完全掌控请求的路由。

expose 暴露控制器方法

expose告诉Pecan类中的哪些方法是公开可见的 。如果一个方法没有用修饰expose(),Pecan永远不会将请求路由到它。

pecan默认采用expose进行路由绑定,需要路由控制器类的方法都要经过expose装饰器的装饰,pecan就可以使HTTP请求找到对应的方法。

不同的使用方法会有不同的效果,如下:

@expose()

1
2
3
4
5
6
from pecan import expose

class RootController(object):
@expose()
def hello(self):
return 'Hello World’

被装饰的方法需要返回一个字符串,表示HTML响应的body。

@expose(html_template_name)

1
2
3
4
5
6
from pecan import expose

class RootController(object):
@expose('html_template.mako')
def hello(self):
return {'msg': 'Hello!’}
1
2
3
4
<!-- html_template.mako -->
<html>
<body>${msg}</body>
</html>

被装饰的方法返回一个字典,字典的key可以在html模板中使用${key}的方式引用。

@expose(route=’some-path’)

例如有这样一个请求:/some-path,由于python语法限制,pecan并不能将该请求的处理方法声明为some-path。使用@expose(route=’some-path’),被装饰方法将响应/some-path请求。

1
2
3
4
5
class RootController(object):

@expose(route='some-path')
def some_path(self):
return dict()

注意:尽量不使用dict(),使用不当,HTTP状态码将是204,及服务器没有返回任何内容错误。

另一种方式:pecan.route()

1
2
3
4
5
6
7
class RootController(object):

@expose()
def some_path(self):
return dict()

pecan.route('some-path', RootController.some_path)

延伸:利用route()方法来将请求路由给下一级控制器

1
2
3
4
5
6
7
8
9
10
class ChildController(object):

@expose()
def child(self):
return dict()

class RootController(object):
pass

pecan.route(RootController, 'child-path', ChildController())

在这个例子中,pecan应用将会给请求/child-path/child/返回HTTP 200响应。

@expose(generic=True)

expose()方法中的generic参数可以根据请求方法对URL进行重载,即一个url路径可以被两个不同的方法处理。

1
2
3
4
5
6
7
8
9
10
11
12
class RootController(object):

# HTTP GET /
@expose(generic=True, template='json')
def index(self):
return dict()

# HTTP POST /
@index.when(method='POST', template='json')
def index_POST(self, **kw):
uuid = create_something()
return dict(uuid=uuid)

对于”/“的GET请求,由index()方法处理;对于”/“的POST请求,由index_POST方法处理。

根据请求方法来路由,其实还有一种方式,继承pecan.rest.RestController来实现

但是这里有个坑,就是不能实现index方法,不然会被覆盖,具体用法看下:

With RestController

default method with RestController

Pecan学习:官方文档解读

Writing RESTful Web Services with Generic Controllers

@expose()叠加用法

1
2
3
4
5
6
class RootController(object):
@expose('json')
@expose('text_template.mako', content_type='text/plain')
@expose('html_template.mako')
def hello(self):
return {'msg': 'Hello!'}

叠加使用后一个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
2
3
4
5
6
7
8
9
10
[composite:hello]
use = egg:Paste#urlmap
/ = hello

[pipeline:hello]
pipeline = main

[app:main]
paste.app_factory = main:app_factory
root = main.Controller

main.py简单文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import wsgiref.simple_server as wss
from paste import deploy
from pecan import expose, Response
import pecan

class Controller(object):
@expose()
def hello(self):
return Response('Hello, World!', 202)

def build_wsgi_app():
import os
abspath = os.path.dirname(os.path.abspath(__file__))
conf_path = os.path.join(abspath, "hello.ini")
app = deploy.loadapp("config:{0}".format(conf_path), name="main")
return app

def app_factory(global_config, **local_conf):
return pecan.make_app(root=local_conf.get('root'))

if __name__ == "__main__":
server = wss.make_server('', 8000, build_wsgi_app())
server.serve_forever()

配置文件中,构建一个叫hello的composite,使用一个叫hello的pipeline。pipeline使用Controller作为控制器。

build_wsgi_app 加载配置文件。

运行server

1
python main.py

img



参考:

pecan的路由机制

pecan源码阅读

Pecan控制器和路由系统

python 学习记录:pecan 框架原理分析和示例

-------------本文结束 感谢您的阅读-------------
作者Magiceses
有问题请 留言 或者私信我的 微博
满分是10分的话,这篇文章你给几分