Warning
This is an unfinished development document for discussion and experimental use only.
Where it contradicts the Routes 2 Spec, the latter prevails.
Routes tackles an interesting problem that comes up frequently in web
development, how do you map URLs to your application's actions? That is,
how do you say that this should be accessed as "/blog/2008/01/08", and
"/login" should do that? Many web frameworks have a fixed rule built in.
saying that "/A/B/C" means to read file "C" in directory "B", or to call
method "C" of class "B" in module "A.B". These work fine until you need
to refactor your code and realize that moving a method changes its public
URL and invalidates users' bookmarks. Likewise, if you want to reorganize
your URLs and make a section into a subsection, you have to change your
carefully-tested logic code.
Routes takes a different approach. You determine your URL hierarchy and
and actions separately, and then link them together in whichever ways you
decide. If you change your mind about a particular URL, just change one
line in your route map and never touch your action logic. You can
even have multiple URLs pointing to the same action; e.g., to support
legacy bookmarks.
Routes is the primary dispatching system in the Pylons web framework, and
an optional choice in CherryPy. It can be added to any framework without
too much fuss, and used for an entire site or a URL subtree. It can also
forward subtrees to other dispatching systems, such as a TurboGears2
controller in a Pylons application.
Routes 2 is a rewrite which makes Routes simpler and more deterministic,
following the Python slogan "Explicit is better than implicit", (Need to
explain this more thoroughly.)
Most of this manual is written from the user's perspective: how to use
Routes in a framework that already supports it. The last section
shows how to add Routes support to a new framework.
- Route
- A rule that links one URL pattern to an application action.
- Route map
- A set of routes for a website (or for portion a portion of a site).
- Matching
- The act of choosing the appropriate route for an incoming URL.
This produces a dict of route variables.
- Generating
- The opposite of matching; choosing a URL based on its name or
route variables.
- Route variables
- Named portions of a URL. For instance,
route "/blog/{year}/{month}/{day}" matches URL "/blog/2008/01/08"
and defines three numeric variables. A route rule can also
define extra variables not in the URL, which are passed to the
framework as constants. Routes does not know what the
variables mean, but the framework uses them to choose an action
and pass it arguments. Three variables important to most
frameworks are controller, action, and id.
- Controller
- Some frameworks (e.g., Pylons and TurboGears2) use a variable
named "controller" to choose a class in a similar-named module
to handle the web request. In these frameworks, all route rules
should define this variable.
- Action
- Some frameworks (e.g., Pylons) use a variable named "action" to
choose a method in the controller class to execute. In these
frameworks, all route rules should define this variable.
- ID
- Some actions use a variable named "id" to choose a database record
to operate on. This is merely a convention; the application writer
may choose to follow it or not.
- Default route
- The route used if no other route matches. By default there is no
default route, so Routes will raise an error if no route matches.
This is not usually what you want, so your first job should be to
set a default route.
Your web framework's documentation will tell you where to define your routes.
In Pylons it's the make_map function in module myapp.config.routing.
We'll assume here that the route map is in a Python variable m, and that
the default route has been assigned thus:
1 | m.default("{controller}/{action}/{id}", action="index", id=None. _minimize=True)
|
This will match URLs like "/news/display/2" and "/help/corporate/address.
Because _minimize is true, it will also match "/help/corporate" and
"/help", filling in the missing variables from the keyword arguments.
However, "{controller}" does not have a corresponding keyword argument, so
this route will not match URL "/". So our second task is to define a route
for the home page.
Note: matching minimized URLs will be done via redirects rather than calling the
action directly. This will allow Routes to handle relative URLs properly.
1 | m.home("", controller="main", action="index")
|
The attribute here, "home", is the route's name. Names are used in
route generation, and will be discussed below. (Unlike Routes 1, names
are required.) "default" is just a name too, but Routes uses it specially.
Again the first argument is the route path minus the leading slash. (Routes
will politely ignore the slash if you specify it.) Here the route path is "/"
because there is nothing after the slash. "controller" and "action" are
constants because there's no "{controller}"
or "{action}" in the route path, so they'll always have the values "main"
and "index" in the routing variables. Let's look at a few more routes:
1
2
3
4
5
6
7
8 | m.archives("archives/{year}/{month}/{day}", controller="archives",
action="view", year=2004,
requirements=dict(year=r"\d{2,4}", month=r"\d{1,2}"))
m.atom_feed("feeds/{category}/atom.xml", controller="feeds", action="atom")
m.history("archives/by_eon/{century}", controller="archives",
action="aggregate", century=1800)
m.article("article/{section}/{slug}/{page}.html",
controller="article", action="view")
|
So far, so good. The route rules are considered in order of definition until
one matches.
The requirements argument is a dict of regular expression
patterns which the corresponding parts of the URL must match, otherwise the URL
will not match the route.
Because these routes do not have _minimize=True, they will not match
URLs that are missing some variable components. So "/archives" and
"/archives/2006" would not be matched. You'll have to add additional
routes if you want to match these shorter routes, or turn on minimization.
This may seem like a burden compared to Routes 1, which automatically matched
the shorter ("minimized") URLs. But experience has shown that minimization is
so eager it can choose the wrong route, an earlier one in the map, so in
Routes 2 you have to explicitly enable minimization where you want it.
Minimization does no harm in the default route because it's
considered after all the others, so it's often used there.
Sometimes you want a route only for generation and not for matching. This
can be useful with off-site URLs, or those that will be served by Apache
or a Pylons "public" directory rather than an action. Add _match=False
to make a route generation-only.
1
2 | m.image("/images/{record_id{/{filename}.jpg", _match=False)
m.google("http://www.google.com/", _match=False)
|
A redirect route tells the framework to send a redirect to a different URL.
This saves you from having to write an action to do the same thing, and is
useful for legacy bookmarks and search-engine links.
1
2
3
4
5
6 | m.redirect("faq", "/static/faq/index")
m.redirect("outsourced_department", "http://outsourcing-r-us.example.com/acme/")
m.redirect("favicon.ico", "/images/temp-icon.png", 302)
m.redirect("faq/{section}", "/static/faq/{section}.html")
m.redirect("note/{letter}", "/musical-notes/{letter}",
requirements={"letter": r"[A-G]"})
|
The first argument is the route path. The second is the destination URL,
where to redirect to. Routes converts relative destination URLs to absolute
per the HTTP spec. Variables may be used to copy parts of the incoming URL
to the destination URL. requirements and _minimize operate as usual.
Other keyword arguments are returned as part of the match dict, though the
framework will probably ignore them.
The third positional argument is the HTTP status, which defaults to 301. If
specified, it must be an integer between 300 - 399. The following statuses are
defined by HTTP 1.1:
- 301: Moved permanently. User agent should update any bookmarks.
- 302: Found. This is the typical one for "moved temporarily", and is
equivalent to 303.
- 303: See Other. Retry using the specified URL with the GET method.
- 307: Temporary Redirect. Retry using the specified URL with the
original method (GET or POST).
Statuses 300, 304, 305, and 306 are allowed but not useful with this method.
A failure route tells the framework to send a 4xx or 5xx status to the browser.
1
2 | m.fail("something", 404)
m.fail("something2", 500, "message")
|
The second argument, an integer between 400 - 599, is required. The
most useful HTTP statuses are:
- 400 Bad Request: the application does not like the URL.
- 403 Forbidden: the application refuses to honor this request, and will
not say why.
- 404 Not Found: there is no such resource in this application.
- 405 Method Not Allowed: user did GET to a POST-only resource, etc.
(@@MO: can Routes do this automatically if a route failed due to a
method constraint, and no other route matched? But what about the
default route? Need another flag to indicate "fail all other methods"?
RFC 2616 says the response must include a list of allowed methods.)
- 410 Gone: the resource has been deleted and will not come back, and
there's no redirect URL. User agents should delete any bookmarks to
this URL.
- 500 Internal Server Error: something happened and it's not the client's fault.
- 503 Service Unavailable: the resource is down for maintenance or
otherwise unavailable.
The third argument, if specified, is a suggested error message. The framework
may incorporate this into the body of the response, but is free to ignore it.
Typically this will be an HTML snippet explaining the reason for the error.
Because 410 ("Gone") is underused and many people don't even know about it, Routes
provides a special syntax to encourage its use:
1 | m.gone("employees/that_loser")
|
Note
Differences from Routes 1
- Route names are required.
- Minimization does not occur unless enabled for a particular route.
The argument _minimize exists only in Routes 2.
- _match replaces _static, with the opposite boolean value.
Generation-only routes in Routes 2 can have variables, while static
named routes in Routes 1 cannot.
- Routes 1 does not have redirect routes or gone routes.
|
|