一个URL路由库。
项目描述
Yrouter
Yrouter是一个不依赖于框架的URL路由包,侧重于简单性和性能。
Router
类
给定一系列route
对象,Router
类从这些对象中构建一个树。
在构建路由时,记住routes -> tree
映射是很重要的(关于这一点稍后还会详细介绍)。
空路由是树的根,其余路由是其子路由。让我们看一个例子
>>> handler = lambda : None
>>> routes = (
route("", handler, name="index"),
route("users", handler, name="users", subroutes = (
route("<int:id>", handler, name="user-details"),
)),
route("articles", subroutes = (
route("<str:category>", handler, name="category"),
route("<slug:title>", handler, name="article"),
)),
)
>>> router = Router(routes)
>>> router.display()
/
users/
<int:id>/
articles/
<str:category>/
<slug:title>/
给定路径匹配处理程序
现在我们可以将URL路径与我们的路由器进行匹配。
>>> router.match("/")
<FullMatch: handler=index, kwargs={}, should_redirect=False>
>>> router.match("users/66/")
<FullMatch: handler=user-details, kwargs={'id': 66}, should_redirect=False>
>>> router.match("articles/tech/")
<FullMatch: handler=category, kwargs={'category': 'tech'}, should_redirect=False>
>>> router.match("articles/hello-world/")
<FullMatch: handler=article, kwargs={'title': 'hello-world'}, should_redirect=False>
我们可以匹配/users/
路径,但不能匹配/articles/
路径,因为后者没有附加处理程序
>>> router.match("users/")
<FullMatch: handler=users, kwargs={}, should_redirect=False>
>>> router.match("articles/")
<NoMatch>
给定处理程序名称查找路径
我们还可以反方向进行:给定处理程序名称和可能的关键字参数查找路径。
>>> router.find("index")
'/'
>>> router.find("user-details", id=66)
'/users/66/'
>>> router.find("category", category="tech")
'/articles/tech/'
如果给定了无效的处理程序名称或缺少/额外的路由搜索关键字参数,find方法将返回None。
>>> router.find("something")
>>> router.find("user-details")
>>> router.find("category", category="tech", foo="bar")
对于具有正则表达式转换器的路由,如果未提供关键字参数,将返回初始路径。但是,如果提供了关键字参数,它们的行为与其他路由类似。
RouteNode
和route
当router
从route
对象中构建树时,它为描述的route
中的每个组件创建一个RouteNode
。
>>> node = route("authors/<int:id>/<str:title>/")
>>> node.display(0)
authors/
<int:id>/
<str:title>/
>>> node
<RouteNode: converter=<ExactConverter: description=authors; identifier=None>; handler=None; children=1>
>>> node = node.children[0]
>>> node
<RouteNode: converter=<IntConverter: description=<int:id>; identifier=id>; handler=None; children=1>
>>> node = node.children[0]
>>> node
<RouteNode: converter=<StringConverter: description=<str:title>; identifier=title>; handler=None; children=0>
>>> node.children
[]
一个 RouteNode
主要通过其 converter
来描述。converter
的任务是判断给定的值是否可以被它所附加的 RouteNode
接受。
以下是我们尝试将 authors/23/some-title
与上述路由进行匹配时发生的情况。
- 首先,路径被这样分割:
["authors", "23", "some-title"]
-
然后,我们尝试将
authors
与第一个RouteNode
转换器ExactConverter
匹配 --> 接受。 -
将
23
与树中下一个节点的转换器匹配 -->IntConverter
--> 接受。 -
将
some-title
与下一个节点的转换器匹配 -->StringConverter
--> 拒绝。 -
最后一个转换器拒绝
some-title
,因为它只匹配字母字符 ==> 返回NoMatch
转换器
默认提供的转换器有:
IntConverter
IntConverter
一个匹配正整数的转换器。
>>> converter = IntConverter(description="<int:id>", identifier="id")
>>> converter.accepts("100")
(True, {'id': 100})
>>> converter.accepts("0")
(True, {'id': 0})
>>> converter.accepts("1.0")
(False, {})
>>> converter.accepts("-1")
(False, {})
>>> converter.accepts("hello-world")
(False, {})
StringConverter
StringConverter
一个只匹配字母字符的转换器。
>>> converter = StringConverter(description="<str:string>", identifier="string")
>>> converter.accepts("hello")
(True, {'string': 'hello'})
>>> converter.accepts("ABC")
(True, {'string': 'ABC'})
>>> converter.accepts("1")
(False, {})
>>> converter.accepts("hello-world")
(False, {})
SlugConverter
SlugConverter
一个匹配缩略名的转换器。
>>> converter = SlugConverter("<slug:slug>", "slug")
>>> converter.accepts("a-slug")
(True, {'slug': 'a-slug'})
>>> converter.accepts("1-random_slug")
(True, {'slug': '1-random_slug'})
>>> converter.accepts("hello")
(True, {'slug': 'hello'})
>>> converter.accepts("hi@hi")
(True, {'slug': 'hi@hi'})
UUIDConverter
UUIDConverter
一个匹配 UUID 的转换器。
>>> converter = UUIDConverter(description="<uuid:uuid>", identifier="uuid_identifier")
>>> converter.accepts("20bfa7b2-50a5-11ec-83dc-479fd603abba")
(True, {'uuid_identifier': '20bfa7b2-50a5-11ec-83dc-479fd603abba'})
>>> converter.accepts("1-2-3-4")
(False, {})
PathConverter
PathConverter
一个匹配任意路径的转换器。
>>> converter = PathConverter(description="<path:path>", identifier="path")
>>> converter.accepts("images/original/hero.jpg")
(True, {'path': 'images/original/hero.jpg'})
>>> converter.accepts("1-2/three/_4")
(True, {'path': '1-2/three/_4'})
RegexConverter
RegexConverter
一个匹配正则表达式的转换器。
>>> converter = RegexConverter("<re:(?P<match>^[a-z]*$)>", "(?P<match>^[a-z]*$)")
>>> converter.accepts("whatever")
(True, {'match': 'whatever'})
>>> converter.accepts("a-b")
(False, {})
由于 yrouter
通过斜杠 (/
) 字符来分隔路由,正则表达式标识符中不允许使用斜杠!
添加转换器
您可以为 yrouter
添加新的转换器。
转换器有一个描述和一个可选的标识符。后者代表匹配路由时的关键字参数。如果 <int:id>
是转换器的描述,那么 id
是标识符。
要注册新的转换器,我们需要创建 AbstractConverter
的子类并实现 accepts
方法。如果它接受给定的值,该方法应返回 (True, matched_kwargs)
,否则返回 REFUSED
。
from yrouter import AbstractConverter, REFUSED
class MyCustomConverter(AbstractConverter, converter_name="custom"):
def accepts(self, value):
return (True, {self.identifier: value}) if value.isidentifier() else REFUSED
此转换器只会接受在 Python 中被视为有效标识符的字符串。
>>> converter = MyCustomConverter(description="<custom:identifier>", identifier="identifier")
>>> converter.accepts("valid_identifier")
(True, {"identifier": "valid_identifier})
>>> converter.accepts("invalid-identifier")
(False, {})
在路由中使用您的转换器
>>> route("<custom:my_identifier>/, handler, name='custom-route'")
您需要确保在尝试在路由中使用转换器之前,读取转换器的代码。
理想情况下,您会将转换器的代码直接写在使用它的路由上方。
尾部斜杠行为
使用 yrouter
,您可以选择所有 URL 是否都有尾部斜杠,或者都不有。
这很重要;您不能有,比如 /users/66
和 users/66/
。
有关此内容的更多详细信息,请参阅:是否添加斜杠。
默认情况下,所有 URL 都会附加斜杠,这意味着如果用户请求资源如 /users/66
,我们上面的路由器将匹配,但会指示用户应该被重定向;在这种情况下到 users/66/
>>> router.match("users/66")
<FullMatch: handler=user-details, kwargs={'id': 66}, should_redirect=True>
因此,如果您不希望将斜杠添加到您的 URL 中,您可以使用 append_slash=False
定义您的路由器。
>>> no_slash_router = Router(routes, append_slash=False)
>>> no_slash_router.match("users/66")
<FullMatch: handler=user-details, kwargs={'id': 66}, should_redirect=False>
>>> no_slash_router.match("users/66/")
<FullMatch: handler=user-details, kwargs={'id': 66}, should_redirect=True>
当我们尝试查找给定处理程序名称的路径时也是如此。
>>> no_slash_router.find("category", category="business")
'/articles/business'
空路由等价于匹配空字符串 ''
和 /
。
>>> router.match("")
<FullMatch: handler=index, kwargs={}, should_redirect=False>
>>> router.match("/")
<FullMatch: handler=index, kwargs={}, should_redirect=False>
>>> no_slash_router.match("")
<FullMatch: handler=index, kwargs={}, should_redirect=False>
>>> no_slash_router.match("/")
<FullMatch: handler=index, kwargs={}, should_redirect=False>
额外考虑
同一级别具有相同前缀的路由
路由的树结构假设每个节点的子节点是唯一描述的。因此,您不能编写如下内容:
>>> routes = (
route("", handler, name="home"),
route("users/", handler, name="users"),
route("users/<int:id>", handler, name="users-details"),
)
>>> router = Router(routes)
Traceback (most recent call last):
...
yrouter.exceptions.RouterConfigurationError: A node matching 'users' already exists at this level of the tree.
改为这样做:
>>> routes = (
route("", handler, name="home"),
route("users/", handler, name="users", subroutes = (
route("<int:id>", handler, name="users-details"),
)),
)
只要 users
路由有一个处理程序附加到它,它就会被匹配。
具有相似名称的路由
不建议具有相似名称的路由。
您可以使用以下技术区分来自不同模块的路由:
>>> routes = (
route("", handler, name="home:index"),
route("users", handler, name="users:index", subroutes = (
route("<int:id>", handler, name="users:details"),
)),
)
>>> router = Router(routes)
>>> router.find("home:index")
'/'
>>> router.find("users:index")
'/users/'
>>> router.find("users:details", id=66)
'/users/66/'
路由器中第一个非空路径的路由
如果第一个路由不是由空字符串''
或'/'
描述的空路由,yrouter
将自动为它创建一个路由。这是必要的,因为它是路由树的根本。空路径不会被匹配。
>>> router = Router([route("home/", handler, name="home")])
>>> router.match("/home/")
<FullMatch: handler=home, kwargs={}, should_redirect=False>
>>> assert not (router.match("") or router.match("/"))
与其他库的集成
yrouter
的构建想法来源于websockets库中的这个功能请求。因此,yrouter-websockets
是基于yrouter
的websockets
库的路由包。
安装
Yrouter需要Python>=3.9
。您可以从Pypi安装它。
pip install yrouter
运行测试
yrouter
包含单元测试和文档测试。您可以使用pytest
运行测试套件。
cd path/to/yrouter
pytest
基准测试
您可以在yrouter-bench
中找到yrouter
与其他一些路由模块的比较。
项目详情
下载文件
下载您平台的文件。如果您不确定要选择哪个,请了解更多关于安装包的信息。
源代码分布
构建分布
yrouter-1.1.0.tar.gz的哈希
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 61cc78fa4590734f0eb3b66a45355b3091736ecf50551a1d534cef5806fed2a7 |
|
MD5 | 876a60effcf9212a7804ed1e0d25e12a |
|
BLAKE2b-256 | 0e9d2ffcab7ce2f15e0cb18fc999924982177437943b8436ebfc2f22b1c62b41 |
yrouter-1.1.0-py3-none-any.whl的哈希
算法 | 哈希摘要 | |
---|---|---|
SHA256 | c79bc9bcf2fb1a946f3c55227df45301410d07585715ffce4dfac8a9e90ac69d |
|
MD5 | 70ad13306d43204296a286d985ea010b |
|
BLAKE2b-256 | 08251dc5cc2502f0b2a6b62a0f0bf4c7014fe538d6b8c1ad42c1f0cd5d701834 |