Merge pull request #387 from subyraman/url-for-v3

Add `url_for` method for simple routes, blueprints, HTTPMethodView
This commit is contained in:
Eli Uriegas
2017-02-08 19:20:09 -06:00
committed by GitHub
10 changed files with 489 additions and 13 deletions

View File

@@ -35,6 +35,9 @@ class Blueprint:
# Routes
for future in self.routes:
# attach the blueprint name to the handler so that it can be
# prefixed properly in the router
future.handler.__blueprintname__ = self.name
# Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri
app.route(

View File

@@ -120,6 +120,10 @@ class ServerError(SanicException):
status_code = 500
class URLBuildError(SanicException):
status_code = 500
class FileNotFound(NotFound):
status_code = 404

View File

@@ -4,7 +4,9 @@ from functools import lru_cache
from .exceptions import NotFound, InvalidUsage
from .views import CompositionView
Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters'])
Route = namedtuple(
'Route',
['handler', 'methods', 'pattern', 'parameters', 'name'])
Parameter = namedtuple('Parameter', ['name', 'cast'])
REGEX_TYPES = {
@@ -59,6 +61,7 @@ class Router:
routes_static = None
routes_dynamic = None
routes_always_check = None
parameter_pattern = re.compile(r'<(.+?)>')
def __init__(self):
self.routes_all = {}
@@ -67,6 +70,29 @@ class Router:
self.routes_always_check = []
self.hosts = None
def parse_parameter_string(self, parameter_string):
"""
Parse a parameter string into its constituent name, type, and pattern
For example:
`parse_parameter_string('<param_one:[A-z]')` ->
('param_one', str, '[A-z]')
:param parameter_string: String to parse
:return: tuple containing
(parameter_name, parameter_type, parameter_pattern)
"""
# We could receive NAME or NAME:PATTERN
name = parameter_string
pattern = 'string'
if ':' in parameter_string:
name, pattern = parameter_string.split(':', 1)
default = (str, pattern)
# Pull from pre-configured types
_type, pattern = REGEX_TYPES.get(pattern, default)
return name, _type, pattern
def add(self, uri, methods, handler, host=None):
"""
Adds a handler to the route list
@@ -104,16 +130,11 @@ class Router:
properties = {"unhashable": None}
def add_parameter(match):
# We could receive NAME or NAME:PATTERN
name = match.group(1)
pattern = 'string'
if ':' in name:
name, pattern = name.split(':', 1)
name, _type, pattern = self.parse_parameter_string(name)
default = (str, pattern)
# Pull from pre-configured types
_type, pattern = REGEX_TYPES.get(pattern, default)
parameter = Parameter(name=name, cast=_type)
parameter = Parameter(
name=name, cast=_type)
parameters.append(parameter)
# Mark the whole route as unhashable if it has the hash key in it
@@ -125,7 +146,7 @@ class Router:
return '({})'.format(pattern)
pattern_string = re.sub(r'<(.+?)>', add_parameter, uri)
pattern_string = re.sub(self.parameter_pattern, add_parameter, uri)
pattern = re.compile(r'^{}$'.format(pattern_string))
def merge_route(route, methods, handler):
@@ -169,9 +190,17 @@ class Router:
if route:
route = merge_route(route, methods, handler)
else:
# prefix the handler name with the blueprint name
# if available
if hasattr(handler, '__blueprintname__'):
handler_name = '{}.{}'.format(
handler.__blueprintname__, handler.__name__)
else:
handler_name = getattr(handler, '__name__', None)
route = Route(
handler=handler, methods=methods, pattern=pattern,
parameters=parameters)
parameters=parameters, name=handler_name)
self.routes_all[uri] = route
if properties['unhashable']:
@@ -208,6 +237,23 @@ class Router:
if clean_cache:
self._get.cache_clear()
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
def find_route_by_view_name(self, view_name):
"""
Find a route in the router based on the specified view name.
:param view_name: string of view name to search by
:return: tuple containing (uri, Route)
"""
if not view_name:
return (None, None)
for uri, route in self.routes_all.items():
if route.name == view_name:
return uri, route
return (None, None)
def get(self, request):
"""
Gets a request handler based on the URL of the request, or raises an

View File

@@ -3,13 +3,15 @@ from asyncio import get_event_loop
from collections import deque
from functools import partial
from inspect import isawaitable, stack, getmodulename
import re
from traceback import format_exc
from urllib.parse import urlencode, urlunparse
import warnings
from .config import Config
from .constants import HTTP_METHODS
from .exceptions import Handler
from .exceptions import ServerError
from .exceptions import ServerError, URLBuildError
from .log import log
from .response import HTTPResponse
from .router import Router
@@ -192,6 +194,89 @@ class Sanic:
DeprecationWarning)
return self.blueprint(*args, **kwargs)
def url_for(self, view_name: str, **kwargs):
"""Builds a URL based on a view name and the values provided.
In order to build a URL, all request parameters must be supplied as
keyword arguments, and each parameter must pass the test for the
specified parameter type. If these conditions are not met, a
`URLBuildError` will be thrown.
Keyword arguments that are not request parameters will be included in
the output URL's query string.
:param view_name: A string referencing the view name
:param **kwargs: keys and values that are used to build request
parameters and query string arguments.
:return: the built URL
Raises:
URLBuildError
"""
# find the route by the supplied view name
uri, route = self.router.find_route_by_view_name(view_name)
if not uri or not route:
raise URLBuildError(
'Endpoint with name `{}` was not found'.format(
view_name))
out = uri
# find all the parameters we will need to build in the URL
matched_params = re.findall(
self.router.parameter_pattern, uri)
for match in matched_params:
name, _type, pattern = self.router.parse_parameter_string(
match)
# we only want to match against each individual parameter
specific_pattern = '^{}$'.format(pattern)
supplied_param = None
if kwargs.get(name):
supplied_param = kwargs.get(name)
del kwargs[name]
else:
raise URLBuildError(
'Required parameter `{}` was not passed to url_for'.format(
name))
supplied_param = str(supplied_param)
# determine if the parameter supplied by the caller passes the test
# in the URL
passes_pattern = re.match(specific_pattern, supplied_param)
if not passes_pattern:
if _type != str:
msg = (
'Value "{}" for parameter `{}` does not '
'match pattern for type `{}`: {}'.format(
supplied_param, name, _type.__name__, pattern))
else:
msg = (
'Value "{}" for parameter `{}` '
'does not satisfy pattern {}'.format(
supplied_param, name, pattern))
raise URLBuildError(msg)
# replace the parameter in the URL with the supplied value
replacement_regex = '(<{}.*?>)'.format(name)
out = re.sub(
replacement_regex, supplied_param, out)
# parse the remainder of the keyword arguments into a querystring
if kwargs:
query_string = urlencode(kwargs)
out = urlunparse((
'', '', out,
'', query_string, ''
))
return out
# -------------------------------------------------------------------- #
# Request Handling
# -------------------------------------------------------------------- #

View File

@@ -64,6 +64,7 @@ class HTTPMethodView:
view.view_class = cls
view.__doc__ = cls.__doc__
view.__module__ = cls.__module__
view.__name__ = cls.__name__
return view