summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xbottle.py281
-rwxr-xr-xdocs/changelog.rst4
-rwxr-xr-xdocs/index.rst1
-rw-r--r--docs/routing.rst111
-rwxr-xr-xdocs/tutorial.rst122
-rwxr-xr-xtest/test_router.py24
6 files changed, 341 insertions, 202 deletions
diff --git a/bottle.py b/bottle.py
index e294287..03b6be7 100755
--- a/bottle.py
+++ b/bottle.py
@@ -120,7 +120,6 @@ def makelist(data):
elif data: return [data]
else: return []
-
class DictProperty(object):
''' Property that maps to a key in a local dict-like attribute. '''
def __init__(self, attr, key=None, read_only=False):
@@ -233,15 +232,14 @@ class RouteReset(BottleException):
""" If raised by a plugin or request handler, the route is reset and all
plugins are re-applied. """
+class RouterUnknownModeError(RouteError): pass
class RouteSyntaxError(RouteError):
""" The route parser found something not supported by this router """
-
class RouteBuildError(RouteError):
""" The route could not been built """
-
class Router(object):
''' A Router is an ordered collection of route->target pairs. It is used to
efficiently match WSGI requests against a number of routes and return
@@ -250,81 +248,153 @@ class Router(object):
and a HTTP method.
The path-rule is either a static path (e.g. `/contact`) or a dynamic
- path that contains wildcards (e.g. `/wiki/:page`). By default, wildcards
- consume characters up to the next slash (`/`). To change that, you may
- add a regular expression pattern (e.g. `/wiki/:page#[a-z]+#`).
-
- For performance reasons, static routes (rules without wildcards) are
- checked first. Dynamic routes are searched in order. Try to avoid
- ambiguous or overlapping rules.
-
- The HTTP method string matches only on equality, with two exceptions:
- * ´GET´ routes also match ´HEAD´ requests if there is no appropriate
- ´HEAD´ route installed.
- * ´ANY´ routes do match if there is no other suitable route installed.
-
- An optional ``name`` parameter is used by :meth:`build` to identify
- routes.
+ path that contains wildcards (e.g. `/wiki/<page>`). The wildcard syntax
+ and details on the matching order are described in docs:`routing`.
'''
- default = '[^/]+'
-
- @lazy_attribute
- def syntax(cls):
- return re.compile(r'(?<!\\):([a-zA-Z_][a-zA-Z_0-9]*)?(?:#(.*?)#)?')
+ default_pattern = '[^/]+'
+ #: Sorry for the mess. It works. Trust me.
+ rule_syntax = re.compile('(\\\\*)'\
+ '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)'\
+ '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)'\
+ '(?::((?:\\\\.|[^\\\\>]+)+)?)?)?>))')
def __init__(self, strict=False):
- self.routes = {} # A {rule: {method: target}} mapping
- self.rules = [] # An ordered list of rules
- self.named = {} # A name->(rule, build_info) mapping
- self.static = {} # Cache for static routes: {path: {method: target}}
- self.dynamic = [] # Cache for dynamic routes. See _compile()
+ self.rules = {} # A {rule: Rule} mapping
+ self.builder = {} # A rule/name->build_info mapping
+ self.static = {} # Cache for static routes: {path: {method: target}}
+ self.dynamic = [] # Cache for dynamic routes. See _compile()
#: If true, static routes are no longer checked first.
self.strict_order = strict
+ self.modes = {'re': self.re_filter, 'int': self.int_filter,
+ 'float': self.re_filter, 'path': self.path_filter}
+
+ def re_filter(self, conf):
+ return conf or self.default_pattern, None, None
+
+ def int_filter(self, conf):
+ return r'-?\d+', int, lambda x: str(int(x))
+
+ def float_filter(self, conf):
+ return r'-?\d*\.\d+', float, lambda x: str(float(x))
+
+ def path_filter(self, conf):
+ return r'.*?', None, None
+
+ def add_filter(self, name, func):
+ ''' Add a filter. The provided function is called with the configuration
+ string as parameter and must return a (regexp, to_python, to_url) tuple.
+ The first element is a string, the last two are callables or None. '''
+ self.modes[name] = func
+
+ def parse_rule(self, rule):
+ ''' Parses a rule into a (name, mode, conf) token stream. If mode is
+ None, name contains a static rule part. '''
+ offset, prefix = 0, ''
+ for match in self.rule_syntax.finditer(rule):
+ prefix += rule[offset:match.start()]
+ g = match.groups()
+ if len(g[0])%2: # Escaped wildcard
+ print prefix, offset, g[0]
+ prefix += match.group(0)[len(g[0]):]
+ offset = match.end()
+ continue
+ if prefix: yield prefix, None, None
+ name, mode, conf = g[1:4] if not g[2] is None else g[4:7]
+ if not mode: mode = 'default'
+ yield name, mode, conf or None
+ offset, prefix = match.end(), ''
+ if offset <= len(rule) or prefix:
+ yield prefix+rule[offset:], None, None
def add(self, rule, method, target, name=None):
''' Add a new route or replace the target for an existing route. '''
- if rule in self.routes:
- self.routes[rule][method.upper()] = target
- else:
- self.routes[rule] = {method.upper(): target}
- self.rules.append(rule)
- if self.static or self.dynamic: # Clear precompiler cache.
- self.static, self.dynamic = {}, {}
- if name:
- self.named[name] = (rule, None)
-
- def build(self, _name, *anon, **args):
- ''' Return a string that matches a named route. Use keyword arguments
- to fill out named wildcards. Remaining arguments are appended as a
- query string. Raises RouteBuildError or KeyError.'''
- if _name not in self.named:
- raise RouteBuildError("No route with that name.", _name)
- rule, pairs = self.named[_name]
- if not pairs:
- token = self.syntax.split(rule)
- parts = [p.replace('\\:',':') for p in token[::3]]
- names = token[1::3]
- if len(parts) > len(names): names.append(None)
- pairs = zip(parts, names)
- self.named[_name] = (rule, pairs)
+ if rule in self.rules:
+ self.rules[rule][method] = target
+ if name: self.builder[name] = self.builder[rule]
+ return
+
+ target = self.rules[rule] = {method: target}
+
+ # Build pattern and other structures for dynamic routes
+ anons = 0 # Number of anonymous wildcards
+ pattern = '' # Regular expression pattern
+ filters = [] # Lists of wildcard input filters
+ builder = [] # Data structure for the URL builder
+ is_static = True
+ for key, mode, conf in self.parse_rule(rule):
+ if mode:
+ is_static = False
+ mask, in_filter, out_filter = self.modes[mode](conf)
+ if key:
+ pattern += '(?P<%s>%s)' % (key, mask)
+ else:
+ pattern += '(?:%s)' % mask
+ key = 'anon%d' % anons; anons += 1
+ if in_filter: filters.append((key, in_filter))
+ builder.append((key, out_filter or str))
+ elif key:
+ pattern += re.escape(key)
+ builder.append((None, key))
+ self.builder[rule] = builder
+ if name: self.builder[name] = builder
+
+ if is_static and not self.strict_order:
+ self.static[self.build(rule)] = target
+ return
+
+ def fpat_sub(m):
+ return m.group(0) if len(m.group(1)) % 2 else m.group(1) + '(?:'
+ flat_pattern = re.sub(r'(\\*)(\(\?P<[^>]*>|\((?!\?))', fpat_sub, pattern)
+
try:
- anon = list(anon)
- url = [s if k is None
- else s+str(args.pop(k)) if k else s+str(anon.pop())
- for s, k in pairs]
- except IndexError:
- msg = "Not enough arguments to fill out anonymous wildcards."
- raise RouteBuildError(msg)
- except KeyError, e:
- raise RouteBuildError(*e.args)
+ re_match = re.compile('^(%s)$' % pattern).match
+ except re.error, e:
+ raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, e))
+
+ def match(path):
+ """ Return an url-argument dictionary. """
+ url_args = re_match(path).groupdict()
+ for name, wildcard_filter in filters:
+ try:
+ url_args[name] = wildcard_filter(url_args[name])
+ except ValueError:
+ raise HTTPError(400, 'Path has wrong format.')
+ return url_args
- if args: url += ['?', urlencode(args)]
- return ''.join(url)
+ try:
+ combined = '%s|(^%s$)' % (self.dynamic[-1][0].pattern, flat_pattern)
+ self.dynamic[-1] = (re.compile(combined), self.dynamic[-1][1])
+ self.dynamic[-1][1].append((match, target))
+ except (AssertionError, IndexError), e: # AssertionError: Too many groups
+ self.dynamic.append((re.compile('(^%s$)' % flat_pattern),
+ [(match, target)]))
+ return match
+
+ def build(self, _name, *anons, **query):
+ ''' Build an URL by filling the wildcards in a rule. '''
+ builder = self.builder.get(_name)
+ if not builder: raise RouteBuildError("No route with that name.", _name)
+ try:
+ for i, value in enumerate(anons): query['anon%d'%i] = value
+ url = ''.join([f(query.pop(n)) if n else f for (n,f) in builder])
+ return url if not query else url+'?'+urlencode(query)
+ except KeyError, e:
+ raise RouteBuildError('Missing URL argument: %r' % e.args[0])
def match(self, environ):
- ''' Return a (target, url_agrs) tuple or raise HTTPError(404/405). '''
- targets, urlargs = self._match_path(environ)
+ ''' Return a (target, url_agrs) tuple or raise HTTPError(400/404/405). '''
+ path, targets, urlargs = environ['PATH_INFO'] or '/', None, {}
+ if path in self.static:
+ targets = self.static[path]
+ else:
+ for combined, rules in self.dynamic:
+ match = combined.match(path)
+ if not match: continue
+ getargs, targets = rules[match.lastindex - 1]
+ urlargs = getargs(path) if getargs else {}
+ break
+
if not targets:
raise HTTPError(404, "Not found: " + repr(environ['PATH_INFO']))
method = environ['REQUEST_METHOD'].upper()
@@ -340,71 +410,14 @@ class Router(object):
raise HTTPError(405, "Method not allowed.",
header=[('Allow',",".join(allowed))])
- def _match_path(self, environ):
- ''' Optimized PATH_INFO matcher. '''
- path = environ['PATH_INFO'] or '/'
- # Assume we are in a warm state. Search compiled rules first.
- match = self.static.get(path)
- if match: return match, {}
- for combined, rules in self.dynamic:
- match = combined.match(path)
- if not match: continue
- gpat, match = rules[match.lastindex - 1]
- return match, gpat(path).groupdict() if gpat else {}
- # Lazy-check if we are really in a warm state. If yes, stop here.
- if self.static or self.dynamic or not self.routes: return None, {}
- # Cold state: We have not compiled any rules yet. Do so and try again.
- if not environ.get('wsgi.run_once'):
- self._compile()
- return self._match_path(environ)
- # For run_once (CGI) environments, don't compile. Just check one by one.
- epath = path.replace(':','\\:') # Turn path into its own static rule.
- match = self.routes.get(epath) # This returns static rule only.
- if match: return match, {}
- for rule in self.rules:
- #: Skip static routes to reduce re.compile() calls.
- if rule.count(':') < rule.count('\\:'): continue
- match = self._compile_pattern(rule).match(path)
- if match: return self.routes[rule], match.groupdict()
- return None, {}
-
- def _compile(self):
- ''' Prepare static and dynamic search structures. '''
- self.static = {}
- self.dynamic = []
- def fpat_sub(m):
- return m.group(0) if len(m.group(1)) % 2 else m.group(1) + '(?:'
- for rule in self.rules:
- target = self.routes[rule]
- if not self.syntax.search(rule) and not self.strict_order:
- self.static[rule.replace('\\:',':')] = target
- continue
- gpat = self._compile_pattern(rule)
- fpat = re.sub(r'(\\*)(\(\?P<[^>]*>|\((?!\?))', fpat_sub, gpat.pattern)
- gpat = gpat.match if gpat.groupindex else None
- try:
- combined = '%s|(%s)' % (self.dynamic[-1][0].pattern, fpat)
- self.dynamic[-1] = (re.compile(combined), self.dynamic[-1][1])
- self.dynamic[-1][1].append((gpat, target))
- except (AssertionError, IndexError), e: # AssertionError: Too many groups
- self.dynamic.append((re.compile('(^%s$)'%fpat),
- [(gpat, target)]))
- except re.error, e:
- raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, e))
-
- def _compile_pattern(self, rule):
- ''' Return a regular expression with named groups for each wildcard. '''
- out = ''
- for i, part in enumerate(self.syntax.split(rule)):
- if i%3 == 0: out += re.escape(part.replace('\\:',':'))
- elif i%3 == 1: out += '(?P<%s>' % part if part else '(?:'
- else: out += '%s)' % (part or '[^/]+')
- return re.compile('^%s$'%out)
class Route(object):
''' This class wraps a route callback along with route specific metadata and
- configuration and applies Plugins on demand. '''
+ configuration and applies Plugins on demand. It is also responsible for
+ turing an URL path rule into a regular expression usable by the Router.
+ '''
+
def __init__(self, app, rule, method, callback, name=None,
plugins=None, skiplist=None, **config):
@@ -504,8 +517,8 @@ class Bottle(object):
self.error_handler = {}
#: If true, most exceptions are catched and returned as :exc:`HTTPError`
- self.catchall = catchall
self.config = ConfigDict(config or {})
+ self.catchall = catchall
#: An instance of :class:`HooksPlugin`. Empty by default.
self.hooks = HooksPlugin()
self.install(self.hooks)
@@ -1952,6 +1965,7 @@ def validate(**vkargs):
Validates and manipulates keyword arguments by user defined callables.
Handles ValueError and missing arguments by raising HTTPError(403).
"""
+ dept('Use route wildcard filters instead.')
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kargs):
@@ -2265,11 +2279,11 @@ def load_app(target):
application object. See :func:`load` for the target parameter. """
global NORUN; NORUN, nr_old = True, NORUN
try:
- tmp = app.push() # Create a new "default application"
+ tmp = default_app.push() # Create a new "default application"
rv = load(target) # Import the target module
- app.remove(tmp) # Remove the temporary added default application
return rv if isinstance(rv, Bottle) else tmp
finally:
+ default_app.remove(tmp) # Remove the temporary added default application
NORUN = nr_old
@@ -2293,12 +2307,9 @@ def run(app=None, server='wsgiref', host='127.0.0.1', port=8080,
"""
if NORUN: return
app = app or default_app()
- if isinstance(app, basestring):
- app = load_app(app)
- if isinstance(server, basestring):
- server = server_names.get(server)
- if isinstance(server, type):
- server = server(host=host, port=port, **kargs)
+ if isinstance(app, basestring): app = load_app(app)
+ if isinstance(server, basestring): server = server_names.get(server)
+ if isinstance(server, type): server = server(host=host, port=port, **kargs)
if not isinstance(server, ServerAdapter):
raise RuntimeError("Server must be a subclass of ServerAdapter")
server.quiet = server.quiet or quiet
diff --git a/docs/changelog.rst b/docs/changelog.rst
index f0f3abb..21bc8c1 100755
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -27,6 +27,10 @@ Not released yet.
* Added three new functions to the SimpleTemplate default namespace that handle undefined variables: :func:`stpl.defined`, :func:`stpl.get` and :func:`stpl.setdefault`.
* The default escape function for SimpleTemplate now additionally escapes single and double quotes.
+* Routing
+ * A new route syntax (e.g. ``/object/<id:int>``) and support for route wildcard filters.
+ * Four new wildcard filters: `int`, `float`, `path` and `re`.
+
* Oher changes
* Added command line interface to load applications and start servers.
diff --git a/docs/index.rst b/docs/index.rst
index 41a644c..313b3a8 100755
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -57,6 +57,7 @@ Start here if you want to learn how to use the bottle framework for web developm
:maxdepth: 2
tutorial
+ routing
stpl
api
plugins/index
diff --git a/docs/routing.rst b/docs/routing.rst
new file mode 100644
index 0000000..160baf4
--- /dev/null
+++ b/docs/routing.rst
@@ -0,0 +1,111 @@
+================================================================================
+Request Routing
+================================================================================
+
+Bottle uses a powerful routing engine to find the right callback for each request. The :ref:`tutorial <tutorial-routing>` shows you the basics. This document covers advanced techniques and rule mechanics in detail.
+
+Rule Syntax
+--------------------------------------------------------------------------------
+
+The :class:`Router` distinguishes between two basic types of routes: **static routes** (e.g. ``/contact``) and **dynamic routes** (e.g. ``/hello/<name>``). A route that contains one or more *wildcards* it is considered dynamic. All other routes are static.
+
+.. versionchanged:: 0.10
+
+The simplest form of a wildcard consists of a name enclosed in angle brackets (e.g. ``<name>``). The name should be unique for a given route and form a valid python identifier (alphanumeric, staring with a letter). This is because wildcards are used as keyword arguments for the request callback later.
+
+Each wildcard matches one or more characters, but stops at the first slash (``/``). This equals a regular expression of ``[^/]+`` and ensures that only one path segment is matched and routes with more than one wildcard stay unambiguous.
+
+The rule ``/<action>/<item>`` matches as follows:
+
+============ =========================================
+Path Result
+============ =========================================
+/save/123 ``{'action': 'save', 'item': '123'}``
+/save/123/ `No Match`
+/save/ `No Match`
+//123 `No Match`
+============ =========================================
+
+You can change the exact behaviour in many ways using filters. This is described in the next section.
+
+Wildcard Filters
+--------------------------------------------------------------------------------
+
+.. versionadded:: 0.10
+
+Filters are used to define more specific wildcards, and/or transform the matched part of the URL before it is passed to the callback. A filtered wildcard is declared as ``<name:filter>`` or ``<name:filter:config>``. The syntax for the optional config part depends on the filter used.
+
+The following standard filters are implemented:
+
+* **:int** matches (signed) digits and converts the value to integer.
+* **:float** similar to :int but for decimal numbers.
+* **:path** matches all characters including the slash character in a non-greedy way and may be used to match more than one path segment.
+* **:re[:exp]** allows you to specify a custom regular expression in the config field. The matched value is not modified.
+
+You can add your own filters to the router. All you need is a function that returns three elements: A regular expression string, a callable to convert the URL fragment to a python value, and a callable that does the opposite. The filter function is called with the configuration string as the only parameter and may parse it as needed::
+
+ app = Bottle()
+
+ def list_filter(config):
+ ''' Matches a comma separated list of numbers. '''
+ delimiter = config or ','
+ regexp = r'\d+(%s\d)*' % re.escape(delimiter)
+
+ def to_python(match):
+ return map(int, match.split(delimiter))
+
+ def to_url(numbers):
+ return delimiter.join(map(str, numbers))
+
+ return regexp, to_python, to_user
+
+ app.router.add_filter('list', list_filter)
+
+ @app.route('/follow/<ids:list>')
+ def follow_users(ids):
+ for id in ids:
+ ...
+
+
+Legacy Syntax
+--------------------------------------------------------------------------------
+
+.. versionchanged:: 0.10
+
+The new rule syntax was introduce in **Bottle 0.10** to simplify some common use cases, but the old syntax still works and you can find lot code examples still using it. The differences are best described by example:
+
+=================== ====================
+Old Syntax New Syntax
+=================== ====================
+``:name`` ``<name>``
+``:name#regexp#`` ``<name:re:regexp>``
+``:#regexp#`` ``<:re:regexp>``
+``:##`` ``<:re>``
+=================== ====================
+
+Try to avoid the old syntax in future projects if you can. It is not deprecated for now, but will be eventually.
+
+
+Routing Order
+--------------------------------------------------------------------------------
+
+With the power of wildcards and regular expressions it is possible to define overlapping routes. If multiple routes match the same URL, things get a bit tricky. To fully understand what happens in this case, you need to know in which order routes are checked by the router.
+
+First you should know that routes are grouped by their path rule. Two routes with the same path rule but different methods are grouped together and the first route determines the position of both routes. Fully identical routes (same path rule and method) replace previously defined routes, but keep the position of their predecessor.
+
+Static routes are checked first. This is mostly for performance reasons and can be switched off, but is currently the default. If no static route matches the request, the dynamic routes are checked in the order they were defined. The first hit ends the search. If no rule matched, a "404 Page not found" error is returned.
+
+In a second step, the request method is checked. If no exact match is found, and the request method is HEAD, the router checks for a GET route. Otherwise, it checks for an ANY route. If that fails too, a "405 Method not allowed" error is returned.
+
+Here is an example where this might bite you::
+
+ @route('/:action/:name', method='GET')
+ @route('/save/:name', method='POST')
+
+The second route will never hit. Even POST requests don't arrive at the second route because the request method is checked in a separate step. The router stops at the first route which matches the request path, then checks for a valid request method, can't find one and raises a 405 error.
+
+Sounds complicated, and it is. That is the price for performance. It is best to avoid ambiguous routes at all and choose unique prefixes for each route. This implementation detail may change in the future, though. We are working on it.
+
+
+
+
diff --git a/docs/tutorial.rst b/docs/tutorial.rst
index 8c2356f..d867435 100755
--- a/docs/tutorial.rst
+++ b/docs/tutorial.rst
@@ -116,11 +116,11 @@ In the last chapter we built a very simple web application with only a single ro
The :func:`route` decorator links an URL path to a callback function, and adds a new route to the :ref:`default application <tutorial-default>`. An application with just one route is kind of boring, though. Let's add some more::
@route('/')
- @route('/hello/:name')
+ @route('/hello/<name>')
def greet(name='Stranger'):
return 'Hello %s, how are you?' % name
-This example demonstrates two important things: You can bind more than one route to a single callback, and you can add wildcards to URLs and extract parts of the URL as keyword arguments.
+This example demonstrates two things: You can bind more than one route to a single callback, and you can add wildcards to URLs and access them via keyword arguments.
@@ -129,44 +129,59 @@ This example demonstrates two important things: You can bind more than one route
Dynamic Routes
-------------------------------------------------------------------------------
-Routes with wildcards are called `dynamic routes` (as opposed to `static routes`) and match more than one URL at the same time. Wildcards start with a colon followed by a name and they match anything but the slash character. For example, the route ``/hello/:name`` accepts requests for ``/hello/alice`` as well as ``/hello/bob`` and any other URL that starts with ``/hello/`` followed by a name.
+Routes that contain wildcards are called `dynamic routes` (as opposed to `static routes`) and match more than one URL at the same time. A simple wildcard consists of a name enclosed in angle brackets (e.g. ``<name>``) and accepts one or more characters up to the next slash (``/``). For example, the route ``/hello/<name>`` accepts requests for ``/hello/alice`` as well as ``/hello/bob``, but not for ``/hello``, ``/hello/`` or ``/hello/mr/smith``.
-Each URL fragment covered by a wildcard is passed to the callback function as a keyword argument. Nice looking and meaningful URLs such as ``/blog/2010/04/21`` or ``/wiki/Page_Title`` are implemented this way. Here are some more examples along with the URLs they'd match::
+Each wildcard passes the covered part of the URL as a keyword argument to the request callback. You can use them right away and implement RESTful, nice looking and meaningful URLs with ease. Here are some other examples along with the URLs they'd match::
- @route('/:action/:user') # matches /follow/defnull
- def api(action, user):
+ @route('/wiki/<pagename>') # matches /wiki/Learning_Python
+ def show_wiki_page(pagename)):
...
- @route('/blog/:year-:month-:day') # matches /blog/2010-04-21
- def blog(year, month, day):
+ @route('/<action>/<user>') # matches /follow/defnull
+ def user_api(action, user):
...
-As mentioned above, normal wildcards consume any characters but the slash (``/``) if they are matched against a request URL. This corresponds to :regexp:`([^/]+)` as a regular expression. If you expect a specific type of information (e.g. a year or a numeric ID), you can customize the pattern and narrow down the range of accepted URLs as follows::
+.. versionadded:: 0.10
- @route('/archive/:year#[0-9]{4}#')
- def arcive(year):
- year = int(year)
- ...
+Filters are used to define more specific wildcards, and/or transform the covered part of the URL before it is passed to the callback. A filtered wildcard is declared as ``<name:filter>`` or ``<name:filter:config>``. The syntax for the optional config part depends on the filter used.
+
+The following filters are implemented by default and more may be added:
+
+* **:int** matches (signed) digits only and converts the value to integer.
+* **:float** similar to :int but for decimal numbers.
+* **:path** matches all characters including the slash character in a non-greedy way and can be used to match more than one path segment.
+* **:re** allows you to specify a custom regular expression in the config field. The matched value is not modified.
+
+Let's have a look at some practical examples::
+
+ @route('/object/<id:int>')
+ def callback(id):
+ assert isinstance(id, int)
-The custom regular pattern is enclosed in two hash characters (``#``). Please note that, even if the wildcard now only matches digits, the keyword argument is still a string. If you need a different type, you have to check and convert the value explicitly in your callback function.
+ @route('/show/<name:re:[a-z]+>')
+ def callback(name):
+ assert name.isalpha()
-Here are some more example to demonstrate the use of wildcards:
+ @route('/static/<path:path>')
+ def callback(path):
+ return static_file(path, ...)
-======================== ====================== ========================
-Route URL URL Arguments
-======================== ====================== ========================
-``/hello/:name`` ``/hello/world`` name='world'
-``/hello/:name`` ``/hello/world/`` `No match`
-``/hello/:name`` ``/hello/`` `No match`
-``/hello/:name#.*#`` ``/hello/`` name='' `(empty string)`
-``/hello/:name#.*#`` ``/hello/world/`` name='world/'
-``/:image.png`` ``/logo.png`` image='logo'
-``/:image#.+\.png#`` ``/img/logo.png`` image='img/logo.png'
-``/:action/:id#[0-9]+#`` ``/save/15`` action='save', id='15'
-``/:action/:id#[0-9]+#`` ``/save/xyz`` `No match`
-``/blog/:y-:m-:d`` ``/blog/2000-05-06`` y='2000', m='05', d='06'
-======================== ====================== ========================
+You can add your own filters as well. See :doc:`Routing` for details.
+.. versionchanged:: 0.10
+
+The new rule syntax was introduce in **Bottle 0.10** to simplify some common use cases, but the old syntax still works and you can find lot code examples still using it. The differences are best described by example:
+
+=================== ====================
+Old Syntax New Syntax
+=================== ====================
+``:name`` ``<name>``
+``:name#regexp#`` ``<name:re:regexp>``
+``:#regexp#`` ``<:re:regexp>``
+``:##`` ``<:re>``
+=================== ====================
+
+Try to avoid the old syntax in future projects if you can. It is not deprecated for now, but will be eventually.
HTTP Request Methods
@@ -214,15 +229,15 @@ Routing Static Files
Static files such as images or css files are not served automatically. You have to add a route and a callback to control which files get served and where to find them::
from bottle import static_file
- @route('/static/:filename')
+ @route('/static/<filename>')
def server_static(filename):
return static_file(filename, root='/path/to/your/static/files')
-The :func:`static_file` function is a helper to serve files in a safe and convenient way (see :ref:`tutorial-static-files`). This example is limited to files directly within the ``/path/to/your/static/files`` directory because the ``:filename`` wildcard won't match a path with a slash in it. To serve files in subdirectories too, we can loosen the wildcard a bit::
+The :func:`static_file` function is a helper to serve files in a safe and convenient way (see :ref:`tutorial-static-files`). This example is limited to files directly within the ``/path/to/your/static/files`` directory because the ``<filename>`` wildcard won't match a path with a slash in it. To serve files in subdirectories, change the wildcard to use the `path` filter::
- @route('/static/:path#.+#')
- def server_static(path):
- return static_file(path, root='/path/to/your/static/files')
+ @route('/static/<filepath:path>')
+ def server_static(filepath):
+ return static_file(filepath, root='/path/to/your/static/files')
Be careful when specifying a relative root-path such as ``root='./static/files'``. The working directory (``./``) and the project directory are not always the same.
@@ -247,27 +262,6 @@ Error handlers are used only if your application returns or raises an :exc:`HTTP
-Implementation Detail: Routing Order
-------------------------------------------------------------------------------
-
-With the power of wildcards and regular expressions it is possible to define overlapping routes. If multiple routes match the same URL, things get a bit tricky. To fully understand what happens in this case, you need to know in which order routes are checked by the router.
-
-First you should know that routes are grouped by their path rule. Two routes with the same path rule but different methods are grouped together and the first route determines the position of both routes. Fully identical routes (same path rule and method) replace previously defined routes, but keep the position of their predecessor.
-
-Static routes are checked first. This is mostly for performance reasons and can be switched off, but is currently the default. If no static route matches the request, the dynamic routes are checked in the order they were defined. The first hit ends the search. If no rule matched, a "404 Page not found" error is returned.
-
-In a second step, the request method is checked. If no exact match is found, and the request method is HEAD, the router checks for a GET route. Otherwise, it checks for an ANY route. If that fails too, a "405 Method not allowed" error is returned.
-
-Here is an example where this might bite you::
-
- @route('/:action/:name', method='GET')
- @route('/save/:name', method='POST')
-
-The second route will never hit. Even POST requests don't arrive at the second route because the request method is checked in a separate step. The router stops at the first route which matches the request path, then checks for a valid request method, can't find one and raises a 405 error.
-
-Sounds complicated, and it is. That is the price for performance. It is best to avoid ambiguous routes at all and choose unique prefixes for each route. This implementation detail may change in the future, though. We are working on it.
-
-
.. _tutorial-output:
@@ -331,11 +325,11 @@ You can directly return file objects, but :func:`static_file` is the recommended
::
from bottle import static_file
- @route('/images/:filename#.*\.png#')
+ @route('/images/<filename:re:.*\.png>#')
def send_image(filename):
return static_file(filename, root='/path/to/image/files', mimetype='image/png')
- @route('/static/:filename')
+ @route('/static/<filename:path>')
def send_static(filename):
return static_file(filename, root='/path/to/static/files')
@@ -345,7 +339,7 @@ You can raise the return value of :func:`static_file` as an exception if you rea
Most browsers try to open downloaded files if the MIME type is known and assigned to an application (e.g. PDF files). If this is not what you want, you can force a download-dialog and even suggest a filename to the user::
- @route('/download/:filename')
+ @route('/download/<filename:path>')
def download(filename):
return static_file(filename, root='/path/to/static/files', download=filename)
@@ -397,7 +391,7 @@ The `HTTP status code <http_code>`_ controls the behavior of the browser and def
Response headers such as ``Cache-Control`` or ``Location`` are defined via :meth:`Response.set_header`. This method takes two parameters, a header name and a value. The name part is case-insensitive::
- @route('/wiki/:page')
+ @route('/wiki/<page>')
def wiki(page):
response.set_header('Content-Language', 'en')
...
@@ -587,7 +581,7 @@ Templates
Bottle comes with a fast and powerful built-in template engine called :doc:`stpl`. To render a template you can use the :func:`template` function or the :func:`view` decorator. All you have to do is to provide the name of the template and the variables you want to pass to the template as keyword arguments. Here’s a simple example of how to render a template::
@route('/hello')
- @route('/hello/:name')
+ @route('/hello/<name>')
def hello(name='World'):
return template('hello_template', name=name)
@@ -596,7 +590,7 @@ This will load the template file ``hello_template.tpl`` and render it with the `
The :func:`view` decorator allows you to return a dictionary with the template variables instead of calling :func:`template`::
@route('/hello')
- @route('/hello/:name')
+ @route('/hello/<name>')
@view('hello_template')
def hello(name='World'):
return dict(name=name)
@@ -644,9 +638,9 @@ The effects and APIs of plugins are manifold and depend on the specific plugin.
install(SQLitePlugin(dbfile='/tmp/test.db'))
- @route('/show/:post_id')
+ @route('/show/<post_id:int>')
def show(db, post_id):
- c = db.execute('SELECT title, content FROM posts WHERE id = ?', (int(post_id),))
+ c = db.execute('SELECT title, content FROM posts WHERE id = ?', (post_id,))
row = c.fetchone()
return template('show_post', title=row['title'], text=row['content'])
@@ -713,7 +707,7 @@ You may want to explicitly disable a plugin for a number of routes. The :func:`r
sqlite_plugin = SQLitePlugin(dbfile='/tmp/test.db')
install(sqlite_plugin)
- @route('/open/:db', skip=[sqlite_plugin])
+ @route('/open/<db>', skip=[sqlite_plugin])
def open_db(db):
# The 'db' keyword argument is not touched by the plugin this time.
if db in ('test', 'test2'):
diff --git a/test/test_router.py b/test/test_router.py
index 7210fc5..cd57ecd 100755
--- a/test/test_router.py
+++ b/test/test_router.py
@@ -34,6 +34,22 @@ class TestRouter(unittest.TestCase):
self.assertMatches('/:#anon#/match', '/anon/match') # Anon wildcards
self.assertRaises(bottle.HTTPError, self.match, '//no/m/at/ch/')
+ def testNeewSyntax(self):
+ self.assertMatches('/static', '/static')
+ self.assertMatches('/\\<its>/<:re:.+>/<test>/<name:re:[a-z]+>/',
+ '/<its>/a/cruel/world/',
+ test='cruel', name='world')
+ self.assertMatches('/<test>', '/test', test='test') # No tail
+ self.assertMatches('<test>/', 'test/', test='test') # No head
+ self.assertMatches('/<test>/', '/test/', test='test') # Middle
+ self.assertMatches('<test>', 'test', test='test') # Full wildcard
+ self.assertMatches('/<:re:anon>/match', '/anon/match') # Anon wildcards
+ self.assertRaises(bottle.HTTPError, self.match, '//no/m/at/ch/')
+
+ def testIntFilter(self):
+ self.assertMatches('/object/<id:int>', '/object/567', id=567)
+ self.assertRaises(bottle.HTTPError, self.match, '/object/abc')
+
def testWildcardNames(self):
self.assertMatches('/alpha/:abc', '/alpha/alpha', abc='alpha')
self.assertMatches('/alnum/:md5', '/alnum/sha1', md5='sha1')
@@ -60,9 +76,10 @@ class TestRouter(unittest.TestCase):
# RouteBuildError: No route found with name 'test'.
self.assertRaises(bottle.RouteBuildError, build, 'testroute')
# RouteBuildError: Missing parameter 'test' in route 'testroute'
- #self.assertRaises(bottle.RouteBuildError, build, 'testroute', test='hello', name='1234')
- # RouteBuildError: Parameter 'name' does not match pattern for route 'testroute': '[a-z]+'
- #self.assertRaises(bottle.RouteBuildError, build, 'anonroute')
+ self.assertRaises(bottle.RouteBuildError, build, 'anonroute')
+ # RouteBuildError: Anonymous pattern found. Can't generate the route 'anonroute'.
+ url = build('anonroute', 'world')
+ self.assertEqual('/anon/world', url)
# RouteBuildError: Anonymous pattern found. Can't generate the route 'anonroute'.
def test_method(self):
@@ -71,6 +88,7 @@ class TestRouter(unittest.TestCase):
class TestRouterInCGIMode(TestRouter):
+ ''' Makes no sense since the default route does not optimize CGI anymore.'''
CGI = True