routing.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  1. # Copyright 2015 The Tornado Authors
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  4. # not use this file except in compliance with the License. You may obtain
  5. # a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  11. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  12. # License for the specific language governing permissions and limitations
  13. # under the License.
  14. """Flexible routing implementation.
  15. Tornado routes HTTP requests to appropriate handlers using `Router`
  16. class implementations. The `tornado.web.Application` class is a
  17. `Router` implementation and may be used directly, or the classes in
  18. this module may be used for additional flexibility. The `RuleRouter`
  19. class can match on more criteria than `.Application`, or the `Router`
  20. interface can be subclassed for maximum customization.
  21. `Router` interface extends `~.httputil.HTTPServerConnectionDelegate`
  22. to provide additional routing capabilities. This also means that any
  23. `Router` implementation can be used directly as a ``request_callback``
  24. for `~.httpserver.HTTPServer` constructor.
  25. `Router` subclass must implement a ``find_handler`` method to provide
  26. a suitable `~.httputil.HTTPMessageDelegate` instance to handle the
  27. request:
  28. .. code-block:: python
  29. class CustomRouter(Router):
  30. def find_handler(self, request, **kwargs):
  31. # some routing logic providing a suitable HTTPMessageDelegate instance
  32. return MessageDelegate(request.connection)
  33. class MessageDelegate(HTTPMessageDelegate):
  34. def __init__(self, connection):
  35. self.connection = connection
  36. def finish(self):
  37. self.connection.write_headers(
  38. ResponseStartLine("HTTP/1.1", 200, "OK"),
  39. HTTPHeaders({"Content-Length": "2"}),
  40. b"OK")
  41. self.connection.finish()
  42. router = CustomRouter()
  43. server = HTTPServer(router)
  44. The main responsibility of `Router` implementation is to provide a
  45. mapping from a request to `~.httputil.HTTPMessageDelegate` instance
  46. that will handle this request. In the example above we can see that
  47. routing is possible even without instantiating an `~.web.Application`.
  48. For routing to `~.web.RequestHandler` implementations we need an
  49. `~.web.Application` instance. `~.web.Application.get_handler_delegate`
  50. provides a convenient way to create `~.httputil.HTTPMessageDelegate`
  51. for a given request and `~.web.RequestHandler`.
  52. Here is a simple example of how we can we route to
  53. `~.web.RequestHandler` subclasses by HTTP method:
  54. .. code-block:: python
  55. resources = {}
  56. class GetResource(RequestHandler):
  57. def get(self, path):
  58. if path not in resources:
  59. raise HTTPError(404)
  60. self.finish(resources[path])
  61. class PostResource(RequestHandler):
  62. def post(self, path):
  63. resources[path] = self.request.body
  64. class HTTPMethodRouter(Router):
  65. def __init__(self, app):
  66. self.app = app
  67. def find_handler(self, request, **kwargs):
  68. handler = GetResource if request.method == "GET" else PostResource
  69. return self.app.get_handler_delegate(request, handler, path_args=[request.path])
  70. router = HTTPMethodRouter(Application())
  71. server = HTTPServer(router)
  72. `ReversibleRouter` interface adds the ability to distinguish between
  73. the routes and reverse them to the original urls using route's name
  74. and additional arguments. `~.web.Application` is itself an
  75. implementation of `ReversibleRouter` class.
  76. `RuleRouter` and `ReversibleRuleRouter` are implementations of
  77. `Router` and `ReversibleRouter` interfaces and can be used for
  78. creating rule-based routing configurations.
  79. Rules are instances of `Rule` class. They contain a `Matcher`, which
  80. provides the logic for determining whether the rule is a match for a
  81. particular request and a target, which can be one of the following.
  82. 1) An instance of `~.httputil.HTTPServerConnectionDelegate`:
  83. .. code-block:: python
  84. router = RuleRouter([
  85. Rule(PathMatches("/handler"), ConnectionDelegate()),
  86. # ... more rules
  87. ])
  88. class ConnectionDelegate(HTTPServerConnectionDelegate):
  89. def start_request(self, server_conn, request_conn):
  90. return MessageDelegate(request_conn)
  91. 2) A callable accepting a single argument of `~.httputil.HTTPServerRequest` type:
  92. .. code-block:: python
  93. router = RuleRouter([
  94. Rule(PathMatches("/callable"), request_callable)
  95. ])
  96. def request_callable(request):
  97. request.write(b"HTTP/1.1 200 OK\\r\\nContent-Length: 2\\r\\n\\r\\nOK")
  98. request.finish()
  99. 3) Another `Router` instance:
  100. .. code-block:: python
  101. router = RuleRouter([
  102. Rule(PathMatches("/router.*"), CustomRouter())
  103. ])
  104. Of course a nested `RuleRouter` or a `~.web.Application` is allowed:
  105. .. code-block:: python
  106. router = RuleRouter([
  107. Rule(HostMatches("example.com"), RuleRouter([
  108. Rule(PathMatches("/app1/.*"), Application([(r"/app1/handler", Handler)])),
  109. ]))
  110. ])
  111. server = HTTPServer(router)
  112. In the example below `RuleRouter` is used to route between applications:
  113. .. code-block:: python
  114. app1 = Application([
  115. (r"/app1/handler", Handler1),
  116. # other handlers ...
  117. ])
  118. app2 = Application([
  119. (r"/app2/handler", Handler2),
  120. # other handlers ...
  121. ])
  122. router = RuleRouter([
  123. Rule(PathMatches("/app1.*"), app1),
  124. Rule(PathMatches("/app2.*"), app2)
  125. ])
  126. server = HTTPServer(router)
  127. For more information on application-level routing see docs for `~.web.Application`.
  128. .. versionadded:: 4.5
  129. """
  130. import re
  131. from functools import partial
  132. from tornado import httputil
  133. from tornado.httpserver import _CallableAdapter
  134. from tornado.escape import url_escape, url_unescape, utf8
  135. from tornado.log import app_log
  136. from tornado.util import basestring_type, import_object, re_unescape, unicode_type
  137. from typing import (
  138. Any,
  139. Union,
  140. Optional,
  141. Awaitable,
  142. List,
  143. Dict,
  144. Pattern,
  145. Tuple,
  146. overload,
  147. Sequence,
  148. )
  149. class Router(httputil.HTTPServerConnectionDelegate):
  150. """Abstract router interface."""
  151. def find_handler(
  152. self, request: httputil.HTTPServerRequest, **kwargs: Any
  153. ) -> Optional[httputil.HTTPMessageDelegate]:
  154. """Must be implemented to return an appropriate instance of `~.httputil.HTTPMessageDelegate`
  155. that can serve the request.
  156. Routing implementations may pass additional kwargs to extend the routing logic.
  157. :arg httputil.HTTPServerRequest request: current HTTP request.
  158. :arg kwargs: additional keyword arguments passed by routing implementation.
  159. :returns: an instance of `~.httputil.HTTPMessageDelegate` that will be used to
  160. process the request.
  161. """
  162. raise NotImplementedError()
  163. def start_request(
  164. self, server_conn: object, request_conn: httputil.HTTPConnection
  165. ) -> httputil.HTTPMessageDelegate:
  166. return _RoutingDelegate(self, server_conn, request_conn)
  167. class ReversibleRouter(Router):
  168. """Abstract router interface for routers that can handle named routes
  169. and support reversing them to original urls.
  170. """
  171. def reverse_url(self, name: str, *args: Any) -> Optional[str]:
  172. """Returns url string for a given route name and arguments
  173. or ``None`` if no match is found.
  174. :arg str name: route name.
  175. :arg args: url parameters.
  176. :returns: parametrized url string for a given route name (or ``None``).
  177. """
  178. raise NotImplementedError()
  179. class _RoutingDelegate(httputil.HTTPMessageDelegate):
  180. def __init__(
  181. self, router: Router, server_conn: object, request_conn: httputil.HTTPConnection
  182. ) -> None:
  183. self.server_conn = server_conn
  184. self.request_conn = request_conn
  185. self.delegate = None # type: Optional[httputil.HTTPMessageDelegate]
  186. self.router = router # type: Router
  187. def headers_received(
  188. self,
  189. start_line: Union[httputil.RequestStartLine, httputil.ResponseStartLine],
  190. headers: httputil.HTTPHeaders,
  191. ) -> Optional[Awaitable[None]]:
  192. assert isinstance(start_line, httputil.RequestStartLine)
  193. request = httputil.HTTPServerRequest(
  194. connection=self.request_conn,
  195. server_connection=self.server_conn,
  196. start_line=start_line,
  197. headers=headers,
  198. )
  199. self.delegate = self.router.find_handler(request)
  200. if self.delegate is None:
  201. app_log.debug(
  202. "Delegate for %s %s request not found",
  203. start_line.method,
  204. start_line.path,
  205. )
  206. self.delegate = _DefaultMessageDelegate(self.request_conn)
  207. return self.delegate.headers_received(start_line, headers)
  208. def data_received(self, chunk: bytes) -> Optional[Awaitable[None]]:
  209. assert self.delegate is not None
  210. return self.delegate.data_received(chunk)
  211. def finish(self) -> None:
  212. assert self.delegate is not None
  213. self.delegate.finish()
  214. def on_connection_close(self) -> None:
  215. if self.delegate is not None:
  216. self.delegate.on_connection_close()
  217. class _DefaultMessageDelegate(httputil.HTTPMessageDelegate):
  218. def __init__(self, connection: httputil.HTTPConnection) -> None:
  219. self.connection = connection
  220. def finish(self) -> None:
  221. self.connection.write_headers(
  222. httputil.ResponseStartLine("HTTP/1.1", 404, "Not Found"),
  223. httputil.HTTPHeaders(),
  224. )
  225. self.connection.finish()
  226. # _RuleList can either contain pre-constructed Rules or a sequence of
  227. # arguments to be passed to the Rule constructor.
  228. _RuleList = Sequence[
  229. Union[
  230. "Rule",
  231. List[Any], # Can't do detailed typechecking of lists.
  232. Tuple[Union[str, "Matcher"], Any],
  233. Tuple[Union[str, "Matcher"], Any, Dict[str, Any]],
  234. Tuple[Union[str, "Matcher"], Any, Dict[str, Any], str],
  235. ]
  236. ]
  237. class RuleRouter(Router):
  238. """Rule-based router implementation."""
  239. def __init__(self, rules: Optional[_RuleList] = None) -> None:
  240. """Constructs a router from an ordered list of rules::
  241. RuleRouter([
  242. Rule(PathMatches("/handler"), Target),
  243. # ... more rules
  244. ])
  245. You can also omit explicit `Rule` constructor and use tuples of arguments::
  246. RuleRouter([
  247. (PathMatches("/handler"), Target),
  248. ])
  249. `PathMatches` is a default matcher, so the example above can be simplified::
  250. RuleRouter([
  251. ("/handler", Target),
  252. ])
  253. In the examples above, ``Target`` can be a nested `Router` instance, an instance of
  254. `~.httputil.HTTPServerConnectionDelegate` or an old-style callable,
  255. accepting a request argument.
  256. :arg rules: a list of `Rule` instances or tuples of `Rule`
  257. constructor arguments.
  258. """
  259. self.rules = [] # type: List[Rule]
  260. if rules:
  261. self.add_rules(rules)
  262. def add_rules(self, rules: _RuleList) -> None:
  263. """Appends new rules to the router.
  264. :arg rules: a list of Rule instances (or tuples of arguments, which are
  265. passed to Rule constructor).
  266. """
  267. for rule in rules:
  268. if isinstance(rule, (tuple, list)):
  269. assert len(rule) in (2, 3, 4)
  270. if isinstance(rule[0], basestring_type):
  271. rule = Rule(PathMatches(rule[0]), *rule[1:])
  272. else:
  273. rule = Rule(*rule)
  274. self.rules.append(self.process_rule(rule))
  275. def process_rule(self, rule: "Rule") -> "Rule":
  276. """Override this method for additional preprocessing of each rule.
  277. :arg Rule rule: a rule to be processed.
  278. :returns: the same or modified Rule instance.
  279. """
  280. return rule
  281. def find_handler(
  282. self, request: httputil.HTTPServerRequest, **kwargs: Any
  283. ) -> Optional[httputil.HTTPMessageDelegate]:
  284. for rule in self.rules:
  285. target_params = rule.matcher.match(request)
  286. if target_params is not None:
  287. if rule.target_kwargs:
  288. target_params["target_kwargs"] = rule.target_kwargs
  289. delegate = self.get_target_delegate(
  290. rule.target, request, **target_params
  291. )
  292. if delegate is not None:
  293. return delegate
  294. return None
  295. def get_target_delegate(
  296. self, target: Any, request: httputil.HTTPServerRequest, **target_params: Any
  297. ) -> Optional[httputil.HTTPMessageDelegate]:
  298. """Returns an instance of `~.httputil.HTTPMessageDelegate` for a
  299. Rule's target. This method is called by `~.find_handler` and can be
  300. extended to provide additional target types.
  301. :arg target: a Rule's target.
  302. :arg httputil.HTTPServerRequest request: current request.
  303. :arg target_params: additional parameters that can be useful
  304. for `~.httputil.HTTPMessageDelegate` creation.
  305. """
  306. if isinstance(target, Router):
  307. return target.find_handler(request, **target_params)
  308. elif isinstance(target, httputil.HTTPServerConnectionDelegate):
  309. assert request.connection is not None
  310. return target.start_request(request.server_connection, request.connection)
  311. elif callable(target):
  312. assert request.connection is not None
  313. return _CallableAdapter(
  314. partial(target, **target_params), request.connection
  315. )
  316. return None
  317. class ReversibleRuleRouter(ReversibleRouter, RuleRouter):
  318. """A rule-based router that implements ``reverse_url`` method.
  319. Each rule added to this router may have a ``name`` attribute that can be
  320. used to reconstruct an original uri. The actual reconstruction takes place
  321. in a rule's matcher (see `Matcher.reverse`).
  322. """
  323. def __init__(self, rules: Optional[_RuleList] = None) -> None:
  324. self.named_rules = {} # type: Dict[str, Any]
  325. super().__init__(rules)
  326. def process_rule(self, rule: "Rule") -> "Rule":
  327. rule = super().process_rule(rule)
  328. if rule.name:
  329. if rule.name in self.named_rules:
  330. app_log.warning(
  331. "Multiple handlers named %s; replacing previous value", rule.name
  332. )
  333. self.named_rules[rule.name] = rule
  334. return rule
  335. def reverse_url(self, name: str, *args: Any) -> Optional[str]:
  336. if name in self.named_rules:
  337. return self.named_rules[name].matcher.reverse(*args)
  338. for rule in self.rules:
  339. if isinstance(rule.target, ReversibleRouter):
  340. reversed_url = rule.target.reverse_url(name, *args)
  341. if reversed_url is not None:
  342. return reversed_url
  343. return None
  344. class Rule:
  345. """A routing rule."""
  346. def __init__(
  347. self,
  348. matcher: "Matcher",
  349. target: Any,
  350. target_kwargs: Optional[Dict[str, Any]] = None,
  351. name: Optional[str] = None,
  352. ) -> None:
  353. """Constructs a Rule instance.
  354. :arg Matcher matcher: a `Matcher` instance used for determining
  355. whether the rule should be considered a match for a specific
  356. request.
  357. :arg target: a Rule's target (typically a ``RequestHandler`` or
  358. `~.httputil.HTTPServerConnectionDelegate` subclass or even a nested `Router`,
  359. depending on routing implementation).
  360. :arg dict target_kwargs: a dict of parameters that can be useful
  361. at the moment of target instantiation (for example, ``status_code``
  362. for a ``RequestHandler`` subclass). They end up in
  363. ``target_params['target_kwargs']`` of `RuleRouter.get_target_delegate`
  364. method.
  365. :arg str name: the name of the rule that can be used to find it
  366. in `ReversibleRouter.reverse_url` implementation.
  367. """
  368. if isinstance(target, str):
  369. # import the Module and instantiate the class
  370. # Must be a fully qualified name (module.ClassName)
  371. target = import_object(target)
  372. self.matcher = matcher # type: Matcher
  373. self.target = target
  374. self.target_kwargs = target_kwargs if target_kwargs else {}
  375. self.name = name
  376. def reverse(self, *args: Any) -> Optional[str]:
  377. return self.matcher.reverse(*args)
  378. def __repr__(self) -> str:
  379. return "{}({!r}, {}, kwargs={!r}, name={!r})".format(
  380. self.__class__.__name__,
  381. self.matcher,
  382. self.target,
  383. self.target_kwargs,
  384. self.name,
  385. )
  386. class Matcher:
  387. """Represents a matcher for request features."""
  388. def match(self, request: httputil.HTTPServerRequest) -> Optional[Dict[str, Any]]:
  389. """Matches current instance against the request.
  390. :arg httputil.HTTPServerRequest request: current HTTP request
  391. :returns: a dict of parameters to be passed to the target handler
  392. (for example, ``handler_kwargs``, ``path_args``, ``path_kwargs``
  393. can be passed for proper `~.web.RequestHandler` instantiation).
  394. An empty dict is a valid (and common) return value to indicate a match
  395. when the argument-passing features are not used.
  396. ``None`` must be returned to indicate that there is no match."""
  397. raise NotImplementedError()
  398. def reverse(self, *args: Any) -> Optional[str]:
  399. """Reconstructs full url from matcher instance and additional arguments."""
  400. return None
  401. class AnyMatches(Matcher):
  402. """Matches any request."""
  403. def match(self, request: httputil.HTTPServerRequest) -> Optional[Dict[str, Any]]:
  404. return {}
  405. class HostMatches(Matcher):
  406. """Matches requests from hosts specified by ``host_pattern`` regex."""
  407. def __init__(self, host_pattern: Union[str, Pattern]) -> None:
  408. if isinstance(host_pattern, basestring_type):
  409. if not host_pattern.endswith("$"):
  410. host_pattern += "$"
  411. self.host_pattern = re.compile(host_pattern)
  412. else:
  413. self.host_pattern = host_pattern
  414. def match(self, request: httputil.HTTPServerRequest) -> Optional[Dict[str, Any]]:
  415. if self.host_pattern.match(request.host_name):
  416. return {}
  417. return None
  418. class DefaultHostMatches(Matcher):
  419. """Matches requests from host that is equal to application's default_host.
  420. Always returns no match if ``X-Real-Ip`` header is present.
  421. """
  422. def __init__(self, application: Any, host_pattern: Pattern) -> None:
  423. self.application = application
  424. self.host_pattern = host_pattern
  425. def match(self, request: httputil.HTTPServerRequest) -> Optional[Dict[str, Any]]:
  426. # Look for default host if not behind load balancer (for debugging)
  427. if "X-Real-Ip" not in request.headers:
  428. if self.host_pattern.match(self.application.default_host):
  429. return {}
  430. return None
  431. class PathMatches(Matcher):
  432. """Matches requests with paths specified by ``path_pattern`` regex."""
  433. def __init__(self, path_pattern: Union[str, Pattern]) -> None:
  434. if isinstance(path_pattern, basestring_type):
  435. if not path_pattern.endswith("$"):
  436. path_pattern += "$"
  437. self.regex = re.compile(path_pattern)
  438. else:
  439. self.regex = path_pattern
  440. assert len(self.regex.groupindex) in (0, self.regex.groups), (
  441. "groups in url regexes must either be all named or all "
  442. "positional: %r" % self.regex.pattern
  443. )
  444. self._path, self._group_count = self._find_groups()
  445. def match(self, request: httputil.HTTPServerRequest) -> Optional[Dict[str, Any]]:
  446. match = self.regex.match(request.path)
  447. if match is None:
  448. return None
  449. if not self.regex.groups:
  450. return {}
  451. path_args = [] # type: List[bytes]
  452. path_kwargs = {} # type: Dict[str, bytes]
  453. # Pass matched groups to the handler. Since
  454. # match.groups() includes both named and
  455. # unnamed groups, we want to use either groups
  456. # or groupdict but not both.
  457. if self.regex.groupindex:
  458. path_kwargs = {
  459. str(k): _unquote_or_none(v) for (k, v) in match.groupdict().items()
  460. }
  461. else:
  462. path_args = [_unquote_or_none(s) for s in match.groups()]
  463. return dict(path_args=path_args, path_kwargs=path_kwargs)
  464. def reverse(self, *args: Any) -> Optional[str]:
  465. if self._path is None:
  466. raise ValueError("Cannot reverse url regex " + self.regex.pattern)
  467. assert len(args) == self._group_count, (
  468. "required number of arguments " "not found"
  469. )
  470. if not len(args):
  471. return self._path
  472. converted_args = []
  473. for a in args:
  474. if not isinstance(a, (unicode_type, bytes)):
  475. a = str(a)
  476. converted_args.append(url_escape(utf8(a), plus=False))
  477. return self._path % tuple(converted_args)
  478. def _find_groups(self) -> Tuple[Optional[str], Optional[int]]:
  479. """Returns a tuple (reverse string, group count) for a url.
  480. For example: Given the url pattern /([0-9]{4})/([a-z-]+)/, this method
  481. would return ('/%s/%s/', 2).
  482. """
  483. pattern = self.regex.pattern
  484. if pattern.startswith("^"):
  485. pattern = pattern[1:]
  486. if pattern.endswith("$"):
  487. pattern = pattern[:-1]
  488. if self.regex.groups != pattern.count("("):
  489. # The pattern is too complicated for our simplistic matching,
  490. # so we can't support reversing it.
  491. return None, None
  492. pieces = []
  493. for fragment in pattern.split("("):
  494. if ")" in fragment:
  495. paren_loc = fragment.index(")")
  496. if paren_loc >= 0:
  497. try:
  498. unescaped_fragment = re_unescape(fragment[paren_loc + 1 :])
  499. except ValueError:
  500. # If we can't unescape part of it, we can't
  501. # reverse this url.
  502. return (None, None)
  503. pieces.append("%s" + unescaped_fragment)
  504. else:
  505. try:
  506. unescaped_fragment = re_unescape(fragment)
  507. except ValueError:
  508. # If we can't unescape part of it, we can't
  509. # reverse this url.
  510. return (None, None)
  511. pieces.append(unescaped_fragment)
  512. return "".join(pieces), self.regex.groups
  513. class URLSpec(Rule):
  514. """Specifies mappings between URLs and handlers.
  515. .. versionchanged: 4.5
  516. `URLSpec` is now a subclass of a `Rule` with `PathMatches` matcher and is preserved for
  517. backwards compatibility.
  518. """
  519. def __init__(
  520. self,
  521. pattern: Union[str, Pattern],
  522. handler: Any,
  523. kwargs: Optional[Dict[str, Any]] = None,
  524. name: Optional[str] = None,
  525. ) -> None:
  526. """Parameters:
  527. * ``pattern``: Regular expression to be matched. Any capturing
  528. groups in the regex will be passed in to the handler's
  529. get/post/etc methods as arguments (by keyword if named, by
  530. position if unnamed. Named and unnamed capturing groups
  531. may not be mixed in the same rule).
  532. * ``handler``: `~.web.RequestHandler` subclass to be invoked.
  533. * ``kwargs`` (optional): A dictionary of additional arguments
  534. to be passed to the handler's constructor.
  535. * ``name`` (optional): A name for this handler. Used by
  536. `~.web.Application.reverse_url`.
  537. """
  538. matcher = PathMatches(pattern)
  539. super().__init__(matcher, handler, kwargs, name)
  540. self.regex = matcher.regex
  541. self.handler_class = self.target
  542. self.kwargs = kwargs
  543. def __repr__(self) -> str:
  544. return "{}({!r}, {}, kwargs={!r}, name={!r})".format(
  545. self.__class__.__name__,
  546. self.regex.pattern,
  547. self.handler_class,
  548. self.kwargs,
  549. self.name,
  550. )
  551. @overload
  552. def _unquote_or_none(s: str) -> bytes:
  553. pass
  554. @overload # noqa: F811
  555. def _unquote_or_none(s: None) -> None:
  556. pass
  557. def _unquote_or_none(s: Optional[str]) -> Optional[bytes]: # noqa: F811
  558. """None-safe wrapper around url_unescape to handle unmatched optional
  559. groups correctly.
  560. Note that args are passed as bytes so the handler can decide what
  561. encoding to use.
  562. """
  563. if s is None:
  564. return s
  565. return url_unescape(s, encoding=None, plus=False)