Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/tornado/routing.py: 29%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

221 statements  

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 

15"""Flexible routing implementation. 

16 

17Tornado routes HTTP requests to appropriate handlers using `Router` 

18class implementations. The `tornado.web.Application` class is a 

19`Router` implementation and may be used directly, or the classes in 

20this module may be used for additional flexibility. The `RuleRouter` 

21class can match on more criteria than `.Application`, or the `Router` 

22interface can be subclassed for maximum customization. 

23 

24`Router` interface extends `~.httputil.HTTPServerConnectionDelegate` 

25to provide additional routing capabilities. This also means that any 

26`Router` implementation can be used directly as a ``request_callback`` 

27for `~.httpserver.HTTPServer` constructor. 

28 

29`Router` subclass must implement a ``find_handler`` method to provide 

30a suitable `~.httputil.HTTPMessageDelegate` instance to handle the 

31request: 

32 

33.. code-block:: python 

34 

35 class CustomRouter(Router): 

36 def find_handler(self, request, **kwargs): 

37 # some routing logic providing a suitable HTTPMessageDelegate instance 

38 return MessageDelegate(request.connection) 

39 

40 class MessageDelegate(HTTPMessageDelegate): 

41 def __init__(self, connection): 

42 self.connection = connection 

43 

44 def finish(self): 

45 self.connection.write_headers( 

46 ResponseStartLine("HTTP/1.1", 200, "OK"), 

47 HTTPHeaders({"Content-Length": "2"}), 

48 b"OK") 

49 self.connection.finish() 

50 

51 router = CustomRouter() 

52 server = HTTPServer(router) 

53 

54The main responsibility of `Router` implementation is to provide a 

55mapping from a request to `~.httputil.HTTPMessageDelegate` instance 

56that will handle this request. In the example above we can see that 

57routing is possible even without instantiating an `~.web.Application`. 

58 

59For routing to `~.web.RequestHandler` implementations we need an 

60`~.web.Application` instance. `~.web.Application.get_handler_delegate` 

61provides a convenient way to create `~.httputil.HTTPMessageDelegate` 

62for a given request and `~.web.RequestHandler`. 

63 

64Here is a simple example of how we can we route to 

65`~.web.RequestHandler` subclasses by HTTP method: 

66 

67.. code-block:: python 

68 

69 resources = {} 

70 

71 class GetResource(RequestHandler): 

72 def get(self, path): 

73 if path not in resources: 

74 raise HTTPError(404) 

75 

76 self.finish(resources[path]) 

77 

78 class PostResource(RequestHandler): 

79 def post(self, path): 

80 resources[path] = self.request.body 

81 

82 class HTTPMethodRouter(Router): 

83 def __init__(self, app): 

84 self.app = app 

85 

86 def find_handler(self, request, **kwargs): 

87 handler = GetResource if request.method == "GET" else PostResource 

88 return self.app.get_handler_delegate(request, handler, path_args=[request.path]) 

89 

90 router = HTTPMethodRouter(Application()) 

91 server = HTTPServer(router) 

92 

93`ReversibleRouter` interface adds the ability to distinguish between 

94the routes and reverse them to the original urls using route's name 

95and additional arguments. `~.web.Application` is itself an 

96implementation of `ReversibleRouter` class. 

97 

98`RuleRouter` and `ReversibleRuleRouter` are implementations of 

99`Router` and `ReversibleRouter` interfaces and can be used for 

100creating rule-based routing configurations. 

101 

102Rules are instances of `Rule` class. They contain a `Matcher`, which 

103provides the logic for determining whether the rule is a match for a 

104particular request and a target, which can be one of the following. 

105 

1061) An instance of `~.httputil.HTTPServerConnectionDelegate`: 

107 

108.. code-block:: python 

109 

110 router = RuleRouter([ 

111 Rule(PathMatches("/handler"), ConnectionDelegate()), 

112 # ... more rules 

113 ]) 

114 

115 class ConnectionDelegate(HTTPServerConnectionDelegate): 

116 def start_request(self, server_conn, request_conn): 

117 return MessageDelegate(request_conn) 

118 

1192) A callable accepting a single argument of `~.httputil.HTTPServerRequest` type: 

120 

121.. code-block:: python 

122 

123 router = RuleRouter([ 

124 Rule(PathMatches("/callable"), request_callable) 

125 ]) 

126 

127 def request_callable(request): 

128 request.write(b"HTTP/1.1 200 OK\\r\\nContent-Length: 2\\r\\n\\r\\nOK") 

129 request.finish() 

130 

1313) Another `Router` instance: 

132 

133.. code-block:: python 

134 

135 router = RuleRouter([ 

136 Rule(PathMatches("/router.*"), CustomRouter()) 

137 ]) 

138 

139Of course a nested `RuleRouter` or a `~.web.Application` is allowed: 

140 

141.. code-block:: python 

142 

143 router = RuleRouter([ 

144 Rule(HostMatches("example.com"), RuleRouter([ 

145 Rule(PathMatches("/app1/.*"), Application([(r"/app1/handler", Handler)])), 

146 ])) 

147 ]) 

148 

149 server = HTTPServer(router) 

150 

151In the example below `RuleRouter` is used to route between applications: 

152 

153.. code-block:: python 

154 

155 app1 = Application([ 

156 (r"/app1/handler", Handler1), 

157 # other handlers ... 

158 ]) 

159 

160 app2 = Application([ 

161 (r"/app2/handler", Handler2), 

162 # other handlers ... 

163 ]) 

164 

165 router = RuleRouter([ 

166 Rule(PathMatches("/app1.*"), app1), 

167 Rule(PathMatches("/app2.*"), app2) 

168 ]) 

169 

170 server = HTTPServer(router) 

171 

172For more information on application-level routing see docs for `~.web.Application`. 

173 

174.. versionadded:: 4.5 

175 

176""" 

177 

178import re 

179from collections.abc import Awaitable, Sequence 

180from functools import partial 

181from re import Pattern 

182from typing import ( 

183 Any, 

184 Union, 

185 overload, 

186) 

187 

188from tornado import httputil 

189from tornado.escape import url_escape, url_unescape, utf8 

190from tornado.httpserver import _CallableAdapter 

191from tornado.log import app_log 

192from tornado.util import basestring_type, import_object, re_unescape, unicode_type 

193 

194 

195class Router(httputil.HTTPServerConnectionDelegate): 

196 """Abstract router interface.""" 

197 

198 def find_handler( 

199 self, request: httputil.HTTPServerRequest, **kwargs: Any 

200 ) -> httputil.HTTPMessageDelegate | None: 

201 """Must be implemented to return an appropriate instance of `~.httputil.HTTPMessageDelegate` 

202 that can serve the request. 

203 Routing implementations may pass additional kwargs to extend the routing logic. 

204 

205 :arg httputil.HTTPServerRequest request: current HTTP request. 

206 :arg kwargs: additional keyword arguments passed by routing implementation. 

207 :returns: an instance of `~.httputil.HTTPMessageDelegate` that will be used to 

208 process the request. 

209 """ 

210 raise NotImplementedError() 

211 

212 def start_request( 

213 self, server_conn: object, request_conn: httputil.HTTPConnection 

214 ) -> httputil.HTTPMessageDelegate: 

215 return _RoutingDelegate(self, server_conn, request_conn) 

216 

217 

218class ReversibleRouter(Router): 

219 """Abstract router interface for routers that can handle named routes 

220 and support reversing them to original urls. 

221 """ 

222 

223 def reverse_url(self, name: str, *args: Any) -> str | None: 

224 """Returns url string for a given route name and arguments 

225 or ``None`` if no match is found. 

226 

227 :arg str name: route name. 

228 :arg args: url parameters. 

229 :returns: parametrized url string for a given route name (or ``None``). 

230 """ 

231 raise NotImplementedError() 

232 

233 

234class _RoutingDelegate(httputil.HTTPMessageDelegate): 

235 def __init__( 

236 self, router: Router, server_conn: object, request_conn: httputil.HTTPConnection 

237 ) -> None: 

238 self.server_conn = server_conn 

239 self.request_conn = request_conn 

240 self.delegate: httputil.HTTPMessageDelegate | None = None 

241 self.router: Router = router 

242 

243 def headers_received( 

244 self, 

245 start_line: httputil.RequestStartLine | httputil.ResponseStartLine, 

246 headers: httputil.HTTPHeaders, 

247 ) -> Awaitable[None] | None: 

248 assert isinstance(start_line, httputil.RequestStartLine) 

249 request = httputil.HTTPServerRequest( 

250 connection=self.request_conn, 

251 server_connection=self.server_conn, 

252 start_line=start_line, 

253 headers=headers, 

254 ) 

255 

256 self.delegate = self.router.find_handler(request) 

257 if self.delegate is None: 

258 app_log.debug( 

259 "Delegate for %s %s request not found", 

260 start_line.method, 

261 start_line.path, 

262 ) 

263 self.delegate = _DefaultMessageDelegate(self.request_conn) 

264 

265 return self.delegate.headers_received(start_line, headers) 

266 

267 def data_received(self, chunk: bytes) -> Awaitable[None] | None: 

268 assert self.delegate is not None 

269 return self.delegate.data_received(chunk) 

270 

271 def finish(self) -> None: 

272 assert self.delegate is not None 

273 self.delegate.finish() 

274 

275 def on_connection_close(self) -> None: 

276 if self.delegate is not None: 

277 self.delegate.on_connection_close() 

278 

279 

280class _DefaultMessageDelegate(httputil.HTTPMessageDelegate): 

281 def __init__(self, connection: httputil.HTTPConnection) -> None: 

282 self.connection = connection 

283 

284 def finish(self) -> None: 

285 self.connection.write_headers( 

286 httputil.ResponseStartLine("HTTP/1.1", 404, "Not Found"), 

287 httputil.HTTPHeaders(), 

288 ) 

289 self.connection.finish() 

290 

291 

292# _RuleList can either contain pre-constructed Rules or a sequence of 

293# arguments to be passed to the Rule constructor. 

294_RuleList = Sequence[ 

295 Union[ 

296 "Rule", 

297 list[Any], # Can't do detailed typechecking of lists. 

298 tuple[Union[str, "Matcher"], Any], 

299 tuple[Union[str, "Matcher"], Any, dict[str, Any]], 

300 tuple[Union[str, "Matcher"], Any, dict[str, Any], str], 

301 ] 

302] 

303 

304 

305class RuleRouter(Router): 

306 """Rule-based router implementation.""" 

307 

308 def __init__(self, rules: _RuleList | None = None) -> None: 

309 """Constructs a router from an ordered list of rules:: 

310 

311 RuleRouter([ 

312 Rule(PathMatches("/handler"), Target), 

313 # ... more rules 

314 ]) 

315 

316 You can also omit explicit `Rule` constructor and use tuples of arguments:: 

317 

318 RuleRouter([ 

319 (PathMatches("/handler"), Target), 

320 ]) 

321 

322 `PathMatches` is a default matcher, so the example above can be simplified:: 

323 

324 RuleRouter([ 

325 ("/handler", Target), 

326 ]) 

327 

328 In the examples above, ``Target`` can be a nested `Router` instance, an instance of 

329 `~.httputil.HTTPServerConnectionDelegate` or an old-style callable, 

330 accepting a request argument. 

331 

332 :arg rules: a list of `Rule` instances or tuples of `Rule` 

333 constructor arguments. 

334 """ 

335 self.rules: list[Rule] = [] 

336 if rules: 

337 self.add_rules(rules) 

338 

339 def add_rules(self, rules: _RuleList) -> None: 

340 """Appends new rules to the router. 

341 

342 :arg rules: a list of Rule instances (or tuples of arguments, which are 

343 passed to Rule constructor). 

344 """ 

345 for rule in rules: 

346 if isinstance(rule, (tuple, list)): 

347 assert len(rule) in (2, 3, 4) 

348 if isinstance(rule[0], basestring_type): 

349 rule = Rule(PathMatches(rule[0]), *rule[1:]) 

350 else: 

351 rule = Rule(*rule) 

352 

353 self.rules.append(self.process_rule(rule)) 

354 

355 def process_rule(self, rule: "Rule") -> "Rule": 

356 """Override this method for additional preprocessing of each rule. 

357 

358 :arg Rule rule: a rule to be processed. 

359 :returns: the same or modified Rule instance. 

360 """ 

361 return rule 

362 

363 def find_handler( 

364 self, request: httputil.HTTPServerRequest, **kwargs: Any 

365 ) -> httputil.HTTPMessageDelegate | None: 

366 for rule in self.rules: 

367 target_params = rule.matcher.match(request) 

368 if target_params is not None: 

369 if rule.target_kwargs: 

370 target_params["target_kwargs"] = rule.target_kwargs 

371 

372 delegate = self.get_target_delegate( 

373 rule.target, request, **target_params 

374 ) 

375 

376 if delegate is not None: 

377 return delegate 

378 

379 return None 

380 

381 def get_target_delegate( 

382 self, target: Any, request: httputil.HTTPServerRequest, **target_params: Any 

383 ) -> httputil.HTTPMessageDelegate | None: 

384 """Returns an instance of `~.httputil.HTTPMessageDelegate` for a 

385 Rule's target. This method is called by `~.find_handler` and can be 

386 extended to provide additional target types. 

387 

388 :arg target: a Rule's target. 

389 :arg httputil.HTTPServerRequest request: current request. 

390 :arg target_params: additional parameters that can be useful 

391 for `~.httputil.HTTPMessageDelegate` creation. 

392 """ 

393 if isinstance(target, Router): 

394 return target.find_handler(request, **target_params) 

395 

396 elif isinstance(target, httputil.HTTPServerConnectionDelegate): 

397 assert request.connection is not None 

398 return target.start_request(request.server_connection, request.connection) 

399 

400 elif callable(target): 

401 assert request.connection is not None 

402 return _CallableAdapter( 

403 partial(target, **target_params), request.connection 

404 ) 

405 

406 return None 

407 

408 

409class ReversibleRuleRouter(ReversibleRouter, RuleRouter): 

410 """A rule-based router that implements ``reverse_url`` method. 

411 

412 Each rule added to this router may have a ``name`` attribute that can be 

413 used to reconstruct an original uri. The actual reconstruction takes place 

414 in a rule's matcher (see `Matcher.reverse`). 

415 """ 

416 

417 def __init__(self, rules: _RuleList | None = None) -> None: 

418 self.named_rules: dict[str, Any] = {} 

419 super().__init__(rules) 

420 

421 def process_rule(self, rule: "Rule") -> "Rule": 

422 rule = super().process_rule(rule) 

423 

424 if rule.name: 

425 if rule.name in self.named_rules: 

426 app_log.warning( 

427 "Multiple handlers named %s; replacing previous value", rule.name 

428 ) 

429 self.named_rules[rule.name] = rule 

430 

431 return rule 

432 

433 def reverse_url(self, name: str, *args: Any) -> str | None: 

434 if name in self.named_rules: 

435 return self.named_rules[name].matcher.reverse(*args) 

436 

437 for rule in self.rules: 

438 if isinstance(rule.target, ReversibleRouter): 

439 reversed_url = rule.target.reverse_url(name, *args) 

440 if reversed_url is not None: 

441 return reversed_url 

442 

443 return None 

444 

445 

446class Rule: 

447 """A routing rule.""" 

448 

449 def __init__( 

450 self, 

451 matcher: "Matcher", 

452 target: Any, 

453 target_kwargs: dict[str, Any] | None = None, 

454 name: str | None = None, 

455 ) -> None: 

456 """Constructs a Rule instance. 

457 

458 :arg Matcher matcher: a `Matcher` instance used for determining 

459 whether the rule should be considered a match for a specific 

460 request. 

461 :arg target: a Rule's target (typically a ``RequestHandler`` or 

462 `~.httputil.HTTPServerConnectionDelegate` subclass or even a nested `Router`, 

463 depending on routing implementation). 

464 :arg dict target_kwargs: a dict of parameters that can be useful 

465 at the moment of target instantiation (for example, ``status_code`` 

466 for a ``RequestHandler`` subclass). They end up in 

467 ``target_params['target_kwargs']`` of `RuleRouter.get_target_delegate` 

468 method. 

469 :arg str name: the name of the rule that can be used to find it 

470 in `ReversibleRouter.reverse_url` implementation. 

471 """ 

472 if isinstance(target, str): 

473 # import the Module and instantiate the class 

474 # Must be a fully qualified name (module.ClassName) 

475 target = import_object(target) 

476 

477 self.matcher: Matcher = matcher 

478 self.target = target 

479 self.target_kwargs = target_kwargs if target_kwargs else {} 

480 self.name = name 

481 

482 def reverse(self, *args: Any) -> str | None: 

483 return self.matcher.reverse(*args) 

484 

485 def __repr__(self) -> str: 

486 return "{}({!r}, {}, kwargs={!r}, name={!r})".format( 

487 self.__class__.__name__, 

488 self.matcher, 

489 self.target, 

490 self.target_kwargs, 

491 self.name, 

492 ) 

493 

494 

495class Matcher: 

496 """Represents a matcher for request features.""" 

497 

498 def match(self, request: httputil.HTTPServerRequest) -> dict[str, Any] | None: 

499 """Matches current instance against the request. 

500 

501 :arg httputil.HTTPServerRequest request: current HTTP request 

502 :returns: a dict of parameters to be passed to the target handler 

503 (for example, ``handler_kwargs``, ``path_args``, ``path_kwargs`` 

504 can be passed for proper `~.web.RequestHandler` instantiation). 

505 An empty dict is a valid (and common) return value to indicate a match 

506 when the argument-passing features are not used. 

507 ``None`` must be returned to indicate that there is no match.""" 

508 raise NotImplementedError() 

509 

510 def reverse(self, *args: Any) -> str | None: 

511 """Reconstructs full url from matcher instance and additional arguments.""" 

512 return None 

513 

514 

515class AnyMatches(Matcher): 

516 """Matches any request.""" 

517 

518 def match(self, request: httputil.HTTPServerRequest) -> dict[str, Any] | None: 

519 return {} 

520 

521 

522class HostMatches(Matcher): 

523 """Matches requests from hosts specified by ``host_pattern`` regex.""" 

524 

525 def __init__(self, host_pattern: str | Pattern) -> None: 

526 if isinstance(host_pattern, basestring_type): 

527 if not host_pattern.endswith("$"): 

528 host_pattern += "$" 

529 self.host_pattern = re.compile(host_pattern) 

530 else: 

531 self.host_pattern = host_pattern 

532 

533 def match(self, request: httputil.HTTPServerRequest) -> dict[str, Any] | None: 

534 if self.host_pattern.match(request.host_name): 

535 return {} 

536 

537 return None 

538 

539 

540class DefaultHostMatches(Matcher): 

541 """Matches requests from host that is equal to application's default_host. 

542 Always returns no match if ``X-Real-Ip`` header is present. 

543 """ 

544 

545 def __init__(self, application: Any, host_pattern: Pattern) -> None: 

546 self.application = application 

547 self.host_pattern = host_pattern 

548 

549 def match(self, request: httputil.HTTPServerRequest) -> dict[str, Any] | None: 

550 # Look for default host if not behind load balancer (for debugging) 

551 if "X-Real-Ip" not in request.headers: 

552 if self.host_pattern.match(self.application.default_host): 

553 return {} 

554 return None 

555 

556 

557class PathMatches(Matcher): 

558 """Matches requests with paths specified by ``path_pattern`` regex.""" 

559 

560 def __init__(self, path_pattern: str | Pattern) -> None: 

561 if isinstance(path_pattern, basestring_type): 

562 if not path_pattern.endswith("$"): 

563 path_pattern += "$" 

564 self.regex = re.compile(path_pattern) 

565 else: 

566 self.regex = path_pattern 

567 

568 assert len(self.regex.groupindex) in (0, self.regex.groups), ( 

569 "groups in url regexes must either be all named or all " 

570 "positional: %r" % self.regex.pattern 

571 ) 

572 

573 self._path, self._group_count = self._find_groups() 

574 

575 def match(self, request: httputil.HTTPServerRequest) -> dict[str, Any] | None: 

576 match = self.regex.match(request.path) 

577 if match is None: 

578 return None 

579 if not self.regex.groups: 

580 return {} 

581 

582 path_args: list[bytes] = [] 

583 path_kwargs: dict[str, bytes] = {} 

584 

585 # Pass matched groups to the handler. Since 

586 # match.groups() includes both named and 

587 # unnamed groups, we want to use either groups 

588 # or groupdict but not both. 

589 if self.regex.groupindex: 

590 path_kwargs = { 

591 str(k): _unquote_or_none(v) for (k, v) in match.groupdict().items() 

592 } 

593 else: 

594 path_args = [_unquote_or_none(s) for s in match.groups()] 

595 

596 return dict(path_args=path_args, path_kwargs=path_kwargs) 

597 

598 def reverse(self, *args: Any) -> str | None: 

599 if self._path is None: 

600 raise ValueError("Cannot reverse url regex " + self.regex.pattern) 

601 assert len(args) == self._group_count, ( 

602 "required number of arguments " "not found" 

603 ) 

604 if not len(args): 

605 return self._path 

606 converted_args = [] 

607 for a in args: 

608 if not isinstance(a, (unicode_type, bytes)): 

609 a = str(a) 

610 converted_args.append(url_escape(utf8(a), plus=False)) 

611 return self._path % tuple(converted_args) 

612 

613 def _find_groups(self) -> tuple[str | None, int | None]: 

614 """Returns a tuple (reverse string, group count) for a url. 

615 

616 For example: Given the url pattern /([0-9]{4})/([a-z-]+)/, this method 

617 would return ('/%s/%s/', 2). 

618 """ 

619 pattern = self.regex.pattern 

620 if pattern.startswith("^"): 

621 pattern = pattern[1:] 

622 if pattern.endswith("$"): 

623 pattern = pattern[:-1] 

624 

625 if self.regex.groups != pattern.count("("): 

626 # The pattern is too complicated for our simplistic matching, 

627 # so we can't support reversing it. 

628 return None, None 

629 

630 pieces = [] 

631 for fragment in pattern.split("("): 

632 if ")" in fragment: 

633 paren_loc = fragment.index(")") 

634 if paren_loc >= 0: 

635 try: 

636 unescaped_fragment = re_unescape(fragment[paren_loc + 1 :]) 

637 except ValueError: 

638 # If we can't unescape part of it, we can't 

639 # reverse this url. 

640 return (None, None) 

641 pieces.append("%s" + unescaped_fragment) 

642 else: 

643 try: 

644 unescaped_fragment = re_unescape(fragment) 

645 except ValueError: 

646 # If we can't unescape part of it, we can't 

647 # reverse this url. 

648 return (None, None) 

649 pieces.append(unescaped_fragment) 

650 

651 return "".join(pieces), self.regex.groups 

652 

653 

654class URLSpec(Rule): 

655 """Specifies mappings between URLs and handlers. 

656 

657 .. versionchanged: 4.5 

658 `URLSpec` is now a subclass of a `Rule` with `PathMatches` matcher and is preserved for 

659 backwards compatibility. 

660 """ 

661 

662 def __init__( 

663 self, 

664 pattern: str | Pattern, 

665 handler: Any, 

666 kwargs: dict[str, Any] | None = None, 

667 name: str | None = None, 

668 ) -> None: 

669 """Parameters: 

670 

671 * ``pattern``: Regular expression to be matched. Any capturing 

672 groups in the regex will be passed in to the handler's 

673 get/post/etc methods as arguments (by keyword if named, by 

674 position if unnamed. Named and unnamed capturing groups 

675 may not be mixed in the same rule). 

676 

677 * ``handler``: `~.web.RequestHandler` subclass to be invoked. 

678 

679 * ``kwargs`` (optional): A dictionary of additional arguments 

680 to be passed to the handler's constructor. 

681 

682 * ``name`` (optional): A name for this handler. Used by 

683 `~.web.Application.reverse_url`. 

684 

685 """ 

686 matcher = PathMatches(pattern) 

687 super().__init__(matcher, handler, kwargs, name) 

688 

689 self.regex = matcher.regex 

690 self.handler_class = self.target 

691 self.kwargs = kwargs 

692 

693 def __repr__(self) -> str: 

694 return "{}({!r}, {}, kwargs={!r}, name={!r})".format( 

695 self.__class__.__name__, 

696 self.regex.pattern, 

697 self.handler_class, 

698 self.kwargs, 

699 self.name, 

700 ) 

701 

702 

703@overload 

704def _unquote_or_none(s: str) -> bytes: 

705 pass 

706 

707 

708@overload 

709def _unquote_or_none(s: None) -> None: 

710 pass 

711 

712 

713def _unquote_or_none(s: str | None) -> bytes | None: 

714 """None-safe wrapper around url_unescape to handle unmatched optional 

715 groups correctly. 

716 

717 Note that args are passed as bytes so the handler can decide what 

718 encoding to use. 

719 """ 

720 if s is None: 

721 return s 

722 return url_unescape(s, encoding=None, plus=False)