~ubuntu-branches/ubuntu/oneiric/moin/oneiric-security

« back to all changes in this revision

Viewing changes to MoinMoin/support/werkzeug/routing.py

  • Committer: Bazaar Package Importer
  • Author(s): Jamie Strandboge
  • Date: 2010-03-30 12:55:34 UTC
  • mfrom: (0.1.17 sid)
  • Revision ID: james.westby@ubuntu.com-20100330125534-4c2ufc1rok24447l
Tags: 1.9.2-2ubuntu1
* Merge from Debian testing (LP: #521834). Based on work by Stefan Ebner.
  Remaining changes:
 - Remove python-xml from Suggests field, the package isn't anymore in
   sys.path.
 - Demote fckeditor from Recommends to Suggests; the code was previously
   embedded in moin, but it was also disabled, so there's no reason for us
   to pull this in by default currently. Note: This isn't necessary anymore
   but needs a MIR for fckeditor, so postpone dropping this change until
   lucid+1
* debian/rules:
  - Replace hardcoded python2.5 with python* and hardcore python2.6 for ln
* debian/control.in: drop versioned depends on cdbs

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
"""
 
3
    werkzeug.routing
 
4
    ~~~~~~~~~~~~~~~~
 
5
 
 
6
    When it comes to combining multiple controller or view functions (however
 
7
    you want to call them) you need a dispatcher.  A simple way would be
 
8
    applying regular expression tests on the ``PATH_INFO`` and calling
 
9
    registered callback functions that return the value then.
 
10
 
 
11
    This module implements a much more powerful system than simple regular
 
12
    expression matching because it can also convert values in the URLs and
 
13
    build URLs.
 
14
 
 
15
    Here a simple example that creates an URL map for an application with
 
16
    two subdomains (www and kb) and some URL rules:
 
17
 
 
18
    >>> m = Map([
 
19
    ...     # Static URLs
 
20
    ...     Rule('/', endpoint='static/index'),
 
21
    ...     Rule('/about', endpoint='static/about'),
 
22
    ...     Rule('/help', endpoint='static/help'),
 
23
    ...     # Knowledge Base
 
24
    ...     Subdomain('kb', [
 
25
    ...         Rule('/', endpoint='kb/index'),
 
26
    ...         Rule('/browse/', endpoint='kb/browse'),
 
27
    ...         Rule('/browse/<int:id>/', endpoint='kb/browse'),
 
28
    ...         Rule('/browse/<int:id>/<int:page>', endpoint='kb/browse')
 
29
    ...     ])
 
30
    ... ], default_subdomain='www')
 
31
 
 
32
    If the application doesn't use subdomains it's perfectly fine to not set
 
33
    the default subdomain and use the `Subdomain` rule factory.  The endpoint
 
34
    in the rules can be anything, for example import paths or unique
 
35
    identifiers.  The WSGI application can use those endpoints to get the
 
36
    handler for that URL.  It doesn't have to be a string at all but it's
 
37
    recommended.
 
38
 
 
39
    Now it's possible to create a URL adapter for one of the subdomains and
 
40
    build URLs:
 
41
 
 
42
    >>> c = m.bind('example.com')
 
43
    >>> c.build("kb/browse", dict(id=42))
 
44
    'http://kb.example.com/browse/42/'
 
45
    >>> c.build("kb/browse", dict())
 
46
    'http://kb.example.com/browse/'
 
47
    >>> c.build("kb/browse", dict(id=42, page=3))
 
48
    'http://kb.example.com/browse/42/3'
 
49
    >>> c.build("static/about")
 
50
    '/about'
 
51
    >>> c.build("static/index", force_external=True)
 
52
    'http://www.example.com/'
 
53
 
 
54
    >>> c = m.bind('example.com', subdomain='kb')
 
55
    >>> c.build("static/about")
 
56
    'http://www.example.com/about'
 
57
 
 
58
    The first argument to bind is the server name *without* the subdomain.
 
59
    Per default it will assume that the script is mounted on the root, but
 
60
    often that's not the case so you can provide the real mount point as
 
61
    second argument:
 
62
 
 
63
    >>> c = m.bind('example.com', '/applications/example')
 
64
 
 
65
    The third argument can be the subdomain, if not given the default
 
66
    subdomain is used.  For more details about binding have a look at the
 
67
    documentation of the `MapAdapter`.
 
68
 
 
69
    And here is how you can match URLs:
 
70
 
 
71
    >>> c = m.bind('example.com')
 
72
    >>> c.match("/")
 
73
    ('static/index', {})
 
74
    >>> c.match("/about")
 
75
    ('static/about', {})
 
76
    >>> c = m.bind('example.com', '/', 'kb')
 
77
    >>> c.match("/")
 
78
    ('kb/index', {})
 
79
    >>> c.match("/browse/42/23")
 
80
    ('kb/browse', {'id': 42, 'page': 23})
 
81
 
 
82
    If matching fails you get a `NotFound` exception, if the rule thinks
 
83
    it's a good idea to redirect (for example because the URL was defined
 
84
    to have a slash at the end but the request was missing that slash) it
 
85
    will raise a `RequestRedirect` exception.  Both are subclasses of the
 
86
    `HTTPException` so you can use those errors as responses in the
 
87
    application.
 
88
 
 
89
    If matching succeeded but the URL rule was incompatible to the given
 
90
    method (for example there were only rules for `GET` and `HEAD` and
 
91
    routing system tried to match a `POST` request) a `MethodNotAllowed`
 
92
    method is raised.
 
93
 
 
94
 
 
95
    :copyright: (c) 2009 by the Werkzeug Team, see AUTHORS for more details.
 
96
                             Thomas Johansson.
 
97
    :license: BSD, see LICENSE for more details.
 
98
"""
 
99
import re
 
100
from urlparse import urljoin
 
101
from itertools import izip
 
102
 
 
103
from werkzeug.utils import url_encode, url_quote, redirect, format_string
 
104
from werkzeug.exceptions import HTTPException, NotFound, MethodNotAllowed
 
105
 
 
106
 
 
107
_rule_re = re.compile(r'''
 
108
    (?P<static>[^<]*)                           # static rule data
 
109
    <
 
110
    (?:
 
111
        (?P<converter>[a-zA-Z_][a-zA-Z0-9_]*)   # converter name
 
112
        (?:\((?P<args>.*?)\))?                  # converter arguments
 
113
        \:                                      # variable delimiter
 
114
    )?
 
115
    (?P<variable>[a-zA-Z][a-zA-Z0-9_]*)         # variable name
 
116
    >
 
117
''', re.VERBOSE)
 
118
_simple_rule_re = re.compile(r'<([^>]+)>')
 
119
 
 
120
 
 
121
def parse_rule(rule):
 
122
    """Parse a rule and return it as generator. Each iteration yields tuples
 
123
    in the form ``(converter, arguments, variable)``. If the converter is
 
124
    `None` it's a static url part, otherwise it's a dynamic one.
 
125
 
 
126
    :internal:
 
127
    """
 
128
    pos = 0
 
129
    end = len(rule)
 
130
    do_match = _rule_re.match
 
131
    used_names = set()
 
132
    while pos < end:
 
133
        m = do_match(rule, pos)
 
134
        if m is None:
 
135
            break
 
136
        data = m.groupdict()
 
137
        if data['static']:
 
138
            yield None, None, data['static']
 
139
        variable = data['variable']
 
140
        converter = data['converter'] or 'default'
 
141
        if variable in used_names:
 
142
            raise ValueError('variable name %r used twice.' % variable)
 
143
        used_names.add(variable)
 
144
        yield converter, data['args'] or None, variable
 
145
        pos = m.end()
 
146
    if pos < end:
 
147
        remaining = rule[pos:]
 
148
        if '>' in remaining or '<' in remaining:
 
149
            raise ValueError('malformed url rule: %r' % rule)
 
150
        yield None, None, remaining
 
151
 
 
152
 
 
153
def get_converter(map, name, args):
 
154
    """Create a new converter for the given arguments or raise
 
155
    exception if the converter does not exist.
 
156
 
 
157
    :internal:
 
158
    """
 
159
    if not name in map.converters:
 
160
        raise LookupError('the converter %r does not exist' % name)
 
161
    if args:
 
162
        storage = type('_Storage', (), {'__getitem__': lambda s, x: x})()
 
163
        args, kwargs = eval(u'(lambda *a, **kw: (a, kw))(%s)' % args, {}, storage)
 
164
    else:
 
165
        args = ()
 
166
        kwargs = {}
 
167
    return map.converters[name](map, *args, **kwargs)
 
168
 
 
169
 
 
170
class RoutingException(Exception):
 
171
    """Special exceptions that require the application to redirect, notifies
 
172
    him about missing urls etc.
 
173
 
 
174
    :internal:
 
175
    """
 
176
 
 
177
 
 
178
class RequestRedirect(HTTPException, RoutingException):
 
179
    """Raise if the map requests a redirect. This is for example the case if
 
180
    `strict_slashes` are activated and an url that requires a leading slash.
 
181
 
 
182
    The attribute `new_url` contains the absolute destination url.
 
183
    """
 
184
    code = 301
 
185
 
 
186
    def __init__(self, new_url):
 
187
        RoutingException.__init__(self, new_url)
 
188
        self.new_url = new_url
 
189
 
 
190
    def get_response(self, environ):
 
191
        return redirect(self.new_url, 301)
 
192
 
 
193
 
 
194
class RequestSlash(RoutingException):
 
195
    """Internal exception."""
 
196
 
 
197
 
 
198
class BuildError(RoutingException, LookupError):
 
199
    """Raised if the build system cannot find a URL for an endpoint with the
 
200
    values provided.
 
201
    """
 
202
 
 
203
    def __init__(self, endpoint, values, method):
 
204
        LookupError.__init__(self, endpoint, values, method)
 
205
        self.endpoint = endpoint
 
206
        self.values = values
 
207
        self.method = method
 
208
 
 
209
 
 
210
class ValidationError(ValueError):
 
211
    """Validation error.  If a rule converter raises this exception the rule
 
212
    does not match the current URL and the next URL is tried.
 
213
    """
 
214
 
 
215
 
 
216
class RuleFactory(object):
 
217
    """As soon as you have more complex URL setups it's a good idea to use rule
 
218
    factories to avoid repetitive tasks.  Some of them are builtin, others can
 
219
    be added by subclassing `RuleFactory` and overriding `get_rules`.
 
220
    """
 
221
 
 
222
    def get_rules(self, map):
 
223
        """Subclasses of `RuleFactory` have to override this method and return
 
224
        an iterable of rules."""
 
225
        raise NotImplementedError()
 
226
 
 
227
 
 
228
class Subdomain(RuleFactory):
 
229
    """All URLs provided by this factory have the subdomain set to a
 
230
    specific domain. For example if you want to use the subdomain for
 
231
    the current language this can be a good setup::
 
232
 
 
233
        url_map = Map([
 
234
            Rule('/', endpoint='#select_language'),
 
235
            Subdomain('<string(length=2):lang_code>', [
 
236
                Rule('/', endpoint='index'),
 
237
                Rule('/about', endpoint='about'),
 
238
                Rule('/help', endpoint='help')
 
239
            ])
 
240
        ])
 
241
 
 
242
    All the rules except for the ``'#select_language'`` endpoint will now
 
243
    listen on a two letter long subdomain that helds the language code
 
244
    for the current request.
 
245
    """
 
246
 
 
247
    def __init__(self, subdomain, rules):
 
248
        self.subdomain = subdomain
 
249
        self.rules = rules
 
250
 
 
251
    def get_rules(self, map):
 
252
        for rulefactory in self.rules:
 
253
            for rule in rulefactory.get_rules(map):
 
254
                rule.subdomain = self.subdomain
 
255
                yield rule
 
256
 
 
257
 
 
258
class Submount(RuleFactory):
 
259
    """Like `Subdomain` but prefixes the URL rule with a given string::
 
260
 
 
261
        url_map = Map([
 
262
            Rule('/', endpoint='index'),
 
263
            Submount('/blog', [
 
264
                Rule('/', endpoint='blog/index'),
 
265
                Rule('/entry/<entry_slug>', endpoint='blog/show')
 
266
            ])
 
267
        ])
 
268
 
 
269
    Now the rule ``'blog/show'`` matches ``/blog/entry/<entry_slug>``.
 
270
    """
 
271
 
 
272
    def __init__(self, path, rules):
 
273
        self.path = path.rstrip('/')
 
274
        self.rules = rules
 
275
 
 
276
    def get_rules(self, map):
 
277
        for rulefactory in self.rules:
 
278
            for rule in rulefactory.get_rules(map):
 
279
                rule.rule = self.path + rule.rule
 
280
                yield rule
 
281
 
 
282
 
 
283
class EndpointPrefix(RuleFactory):
 
284
    """Prefixes all endpoints (which must be strings for this factory) with
 
285
    another string. This can be useful for sub applications::
 
286
 
 
287
        url_map = Map([
 
288
            Rule('/', endpoint='index'),
 
289
            EndpointPrefix('blog/', [Submount('/blog', [
 
290
                Rule('/', endpoint='index'),
 
291
                Rule('/entry/<entry_slug>', endpoint='show')
 
292
            ])])
 
293
        ])
 
294
    """
 
295
 
 
296
    def __init__(self, prefix, rules):
 
297
        self.prefix = prefix
 
298
        self.rules = rules
 
299
 
 
300
    def get_rules(self, map):
 
301
        for rulefactory in self.rules:
 
302
            for rule in rulefactory.get_rules(map):
 
303
                rule.endpoint = self.prefix + rule.endpoint
 
304
                yield rule
 
305
 
 
306
 
 
307
class RuleTemplate(object):
 
308
    """Returns copies of the rules wrapped and expands string templates in
 
309
    the endpoint, rule, defaults or subdomain sections.
 
310
 
 
311
    Here a small example for such a rule template::
 
312
 
 
313
        from werkzeug.routing import Map, Rule, RuleTemplate
 
314
 
 
315
        resource = RuleTemplate([
 
316
            Rule('/$name/', endpoint='$name.list'),
 
317
            Rule('/$name/<int:id>', endpoint='$name.show')
 
318
        ])
 
319
 
 
320
        url_map = Map([resource(name='user'), resource(name='page')])
 
321
 
 
322
    When a rule template is called the keyword arguments are used to
 
323
    replace the placeholders in all the string parameters.
 
324
    """
 
325
 
 
326
    def __init__(self, rules):
 
327
        self.rules = list(rules)
 
328
 
 
329
    def __call__(self, *args, **kwargs):
 
330
        return RuleTemplateFactory(self.rules, dict(*args, **kwargs))
 
331
 
 
332
 
 
333
class RuleTemplateFactory(RuleFactory):
 
334
    """A factory that fills in template variables into rules.  Used by
 
335
    `RuleTemplate` internally.
 
336
 
 
337
    :internal:
 
338
    """
 
339
 
 
340
    def __init__(self, rules, context):
 
341
        self.rules = rules
 
342
        self.context = context
 
343
 
 
344
    def get_rules(self, map):
 
345
        for rulefactory in self.rules:
 
346
            for rule in rulefactory.get_rules(map):
 
347
                new_defaults = subdomain = None
 
348
                if rule.defaults is not None:
 
349
                    new_defaults = {}
 
350
                    for key, value in rule.defaults.iteritems():
 
351
                        if isinstance(value, basestring):
 
352
                            value = format_string(value, self.context)
 
353
                        new_defaults[key] = value
 
354
                if rule.subdomain is not None:
 
355
                    subdomain = format_string(rule.subdomain, self.context)
 
356
                new_endpoint = rule.endpoint
 
357
                if isinstance(new_endpoint, basestring):
 
358
                    new_endpoint = format_string(new_endpoint, self.context)
 
359
                yield Rule(
 
360
                    format_string(rule.rule, self.context),
 
361
                    new_defaults,
 
362
                    subdomain,
 
363
                    rule.methods,
 
364
                    rule.build_only,
 
365
                    new_endpoint,
 
366
                    rule.strict_slashes
 
367
                )
 
368
 
 
369
 
 
370
class Rule(RuleFactory):
 
371
    """A Rule represents one URL pattern.  There are some options for `Rule`
 
372
    that change the way it behaves and are passed to the `Rule` constructor.
 
373
    Note that besides the rule-string all arguments *must* be keyword arguments
 
374
    in order to not break the application on Werkzeug upgrades.
 
375
 
 
376
    `string`
 
377
        Rule strings basically are just normal URL paths with placeholders in
 
378
        the format ``<converter(arguments):name>`` where the converter and the
 
379
        arguments are optional.  If no converter is defined the `default`
 
380
        converter is used which means `string` in the normal configuration.
 
381
 
 
382
        URL rules that end with a slash are branch URLs, others are leaves.
 
383
        If you have `strict_slashes` enabled (which is the default), all
 
384
        branch URLs that are matched without a trailing slash will trigger a
 
385
        redirect to the same URL with the missing slash appended.
 
386
 
 
387
        The converters are defined on the `Map`.
 
388
 
 
389
    `endpoint`
 
390
        The endpoint for this rule. This can be anything. A reference to a
 
391
        function, a string, a number etc.  The preferred way is using a string
 
392
        as because the endpoint is used for URL generation.
 
393
 
 
394
    `defaults`
 
395
        An optional dict with defaults for other rules with the same endpoint.
 
396
        This is a bit tricky but useful if you want to have unique URLs::
 
397
 
 
398
            url_map = Map([
 
399
                Rule('/all/', defaults={'page': 1}, endpoint='all_entries'),
 
400
                Rule('/all/page/<int:page>', endpoint='all_entries')
 
401
            ])
 
402
 
 
403
        If a user now visits ``http://example.com/all/page/1`` he will be
 
404
        redirected to ``http://example.com/all/``.  If `redirect_defaults` is
 
405
        disabled on the `Map` instance this will only affect the URL
 
406
        generation.
 
407
 
 
408
    `subdomain`
 
409
        The subdomain rule string for this rule. If not specified the rule
 
410
        only matches for the `default_subdomain` of the map.  If the map is
 
411
        not bound to a subdomain this feature is disabled.
 
412
 
 
413
        Can be useful if you want to have user profiles on different subdomains
 
414
        and all subdomains are forwarded to your application::
 
415
 
 
416
            url_map = Map([
 
417
                Rule('/', subdomain='<username>', endpoint='user/homepage'),
 
418
                Rule('/stats', subdomain='<username>', endpoint='user/stats')
 
419
            ])
 
420
 
 
421
    `methods`
 
422
        A sequence of http methods this rule applies to.  If not specified, all
 
423
        methods are allowed. For example this can be useful if you want different
 
424
        endpoints for `POST` and `GET`.  If methods are defined and the path
 
425
        matches but the method matched against is not in this list or in the
 
426
        list of another rule for that path the error raised is of the type
 
427
        `MethodNotAllowed` rather than `NotFound`.
 
428
 
 
429
    `strict_slashes`
 
430
        Override the `Map` setting for `strict_slashes` only for this rule. If
 
431
        not specified the `Map` setting is used.
 
432
 
 
433
    `build_only`
 
434
        Set this to True and the rule will never match but will create a URL
 
435
        that can be build. This is useful if you have resources on a subdomain
 
436
        or folder that are not handled by the WSGI application (like static data)
 
437
 
 
438
    `redirect_to`
 
439
        If given this must be either a string or callable.  In case of a
 
440
        callable it's called with the url adapter that triggered the match and
 
441
        the values of the URL as keyword arguments and has to return the target
 
442
        for the redirect, otherwise it has to be a string with placeholders in
 
443
        rule syntax::
 
444
 
 
445
            def foo_with_slug(adapter, id):
 
446
                # ask the database for the slug for the old id.  this of
 
447
                # course has nothing to do with werkzeug.
 
448
                return 'foo/' + Foo.get_slug_for_id(id)
 
449
 
 
450
            url_map = Map([
 
451
                Rule('/foo/<slug>', endpoint='foo'),
 
452
                Rule('/some/old/url/<slug>', redirect_to='foo/<slug>'),
 
453
                Rule('/other/old/url/<int:id>', redirect_to=foo_with_slug)
 
454
            ])
 
455
 
 
456
        When the rule is matched the routing system will raise a
 
457
        `RequestRedirect` exception with the target for the redirect.
 
458
 
 
459
        Keep in mind that the URL will be joined against the URL root of the
 
460
        script so don't use a leading slash on the target URL unless you
 
461
        really mean root of that domain.
 
462
    """
 
463
 
 
464
    def __init__(self, string, defaults=None, subdomain=None, methods=None,
 
465
                 build_only=False, endpoint=None, strict_slashes=None,
 
466
                 redirect_to=None):
 
467
        if not string.startswith('/'):
 
468
            raise ValueError('urls must start with a leading slash')
 
469
        self.rule = string
 
470
        self.is_leaf = not string.endswith('/')
 
471
 
 
472
        self.map = None
 
473
        self.strict_slashes = strict_slashes
 
474
        self.subdomain = subdomain
 
475
        self.defaults = defaults
 
476
        self.build_only = build_only
 
477
        if methods is None:
 
478
            self.methods = None
 
479
        else:
 
480
            self.methods = set([x.upper() for x in methods])
 
481
        self.endpoint = endpoint
 
482
        self.greediness = 0
 
483
        self.redirect_to = redirect_to
 
484
 
 
485
        self._trace = []
 
486
        if defaults is not None:
 
487
            self.arguments = set(map(str, defaults))
 
488
        else:
 
489
            self.arguments = set()
 
490
        self._converters = {}
 
491
        self._regex = None
 
492
        self._weights = []
 
493
 
 
494
    def empty(self):
 
495
        """Return an unbound copy of this rule.  This can be useful if you
 
496
        want to reuse an already bound URL for another map."""
 
497
        return Rule(self.rule, self.defaults, self.subdomain, self.methods,
 
498
                    self.build_only, self.endpoint, self.strict_slashes,
 
499
                    self.redirect_to)
 
500
 
 
501
    def get_rules(self, map):
 
502
        yield self
 
503
 
 
504
    def bind(self, map):
 
505
        """Bind the url to a map and create a regular expression based on
 
506
        the information from the rule itself and the defaults from the map.
 
507
 
 
508
        :internal:
 
509
        """
 
510
        if self.map is not None:
 
511
            raise RuntimeError('url rule %r already bound to map %r' %
 
512
                               (self, self.map))
 
513
        self.map = map
 
514
        if self.strict_slashes is None:
 
515
            self.strict_slashes = map.strict_slashes
 
516
        if self.subdomain is None:
 
517
            self.subdomain = map.default_subdomain
 
518
 
 
519
        rule = self.subdomain + '|' + (self.is_leaf and self.rule
 
520
                                       or self.rule.rstrip('/'))
 
521
 
 
522
        regex_parts = []
 
523
        for converter, arguments, variable in parse_rule(rule):
 
524
            if converter is None:
 
525
                regex_parts.append(re.escape(variable))
 
526
                self._trace.append((False, variable))
 
527
                self._weights.append(len(variable))
 
528
            else:
 
529
                convobj = get_converter(map, converter, arguments)
 
530
                regex_parts.append('(?P<%s>%s)' % (variable, convobj.regex))
 
531
                self._converters[variable] = convobj
 
532
                self._trace.append((True, variable))
 
533
                self._weights.append(convobj.weight)
 
534
                self.arguments.add(str(variable))
 
535
                if convobj.is_greedy:
 
536
                    self.greediness += 1
 
537
        if not self.is_leaf:
 
538
            self._trace.append((False, '/'))
 
539
 
 
540
        if not self.build_only:
 
541
            regex = r'^%s%s$' % (
 
542
                u''.join(regex_parts),
 
543
                (not self.is_leaf or not self.strict_slashes) and \
 
544
                    '(?<!/)(?P<__suffix__>/?)' or ''
 
545
            )
 
546
            self._regex = re.compile(regex, re.UNICODE)
 
547
 
 
548
    def match(self, path):
 
549
        """Check if the rule matches a given path. Path is a string in the
 
550
        form ``"subdomain|/path(method)"`` and is assembled by the map.
 
551
 
 
552
        If the rule matches a dict with the converted values is returned,
 
553
        otherwise the return value is `None`.
 
554
 
 
555
        :internal:
 
556
        """
 
557
        if not self.build_only:
 
558
            m = self._regex.search(path)
 
559
            if m is not None:
 
560
                groups = m.groupdict()
 
561
                # we have a folder like part of the url without a trailing
 
562
                # slash and strict slashes enabled. raise an exception that
 
563
                # tells the map to redirect to the same url but with a
 
564
                # trailing slash
 
565
                if self.strict_slashes and not self.is_leaf and \
 
566
                   not groups.pop('__suffix__'):
 
567
                    raise RequestSlash()
 
568
                # if we are not in strict slashes mode we have to remove
 
569
                # a __suffix__
 
570
                elif not self.strict_slashes:
 
571
                    del groups['__suffix__']
 
572
 
 
573
                result = {}
 
574
                for name, value in groups.iteritems():
 
575
                    try:
 
576
                        value = self._converters[name].to_python(value)
 
577
                    except ValidationError:
 
578
                        return
 
579
                    result[str(name)] = value
 
580
                if self.defaults is not None:
 
581
                    result.update(self.defaults)
 
582
                return result
 
583
 
 
584
    def build(self, values):
 
585
        """Assembles the relative url for that rule and the subdomain.
 
586
        If building doesn't work for some reasons `None` is returned.
 
587
 
 
588
        :internal:
 
589
        """
 
590
        tmp = []
 
591
        add = tmp.append
 
592
        processed = set(self.arguments)
 
593
        for is_dynamic, data in self._trace:
 
594
            if is_dynamic:
 
595
                try:
 
596
                    add(self._converters[data].to_url(values[data]))
 
597
                except ValidationError:
 
598
                    return
 
599
                processed.add(data)
 
600
            else:
 
601
                add(data)
 
602
        subdomain, url = (u''.join(tmp)).split('|', 1)
 
603
 
 
604
        query_vars = {}
 
605
        for key in set(values) - processed:
 
606
            query_vars[key] = unicode(values[key])
 
607
        if query_vars:
 
608
            url += '?' + url_encode(query_vars, self.map.charset,
 
609
                                    sort=self.map.sort_parameters,
 
610
                                    key=self.map.sort_key)
 
611
 
 
612
        return subdomain, url
 
613
 
 
614
    def provides_defaults_for(self, rule):
 
615
        """Check if this rule has defaults for a given rule.
 
616
 
 
617
        :internal:
 
618
        """
 
619
        return not self.build_only and self.defaults is not None and \
 
620
               self.endpoint == rule.endpoint and self != rule and \
 
621
               self.arguments == rule.arguments
 
622
 
 
623
    def suitable_for(self, values, method):
 
624
        """Check if the dict of values has enough data for url generation.
 
625
 
 
626
        :internal:
 
627
        """
 
628
        if self.methods is not None and method not in self.methods:
 
629
            return False
 
630
 
 
631
        valueset = set(values)
 
632
 
 
633
        for key in self.arguments - set(self.defaults or ()):
 
634
            if key not in values:
 
635
                return False
 
636
 
 
637
        if self.arguments.issubset(valueset):
 
638
            if self.defaults is None:
 
639
                return True
 
640
            for key, value in self.defaults.iteritems():
 
641
                if value != values[key]:
 
642
                    return False
 
643
 
 
644
        return True
 
645
 
 
646
    def match_compare(self, other):
 
647
        """Compare this object with another one for matching.
 
648
 
 
649
        :internal:
 
650
        """
 
651
        for sw, ow in izip(self._weights, other._weights):
 
652
            if sw > ow:
 
653
                return -1
 
654
            elif sw < ow:
 
655
                return 1
 
656
        if len(self._weights) > len(other._weights):
 
657
            return -1
 
658
        if len(self._weights) < len(other._weights):
 
659
            return 1
 
660
        if not other.arguments and self.arguments:
 
661
            return 1
 
662
        elif other.arguments and not self.arguments:
 
663
            return -1
 
664
        elif other.defaults is None and self.defaults is not None:
 
665
            return 1
 
666
        elif other.defaults is not None and self.defaults is None:
 
667
            return -1
 
668
        elif self.greediness > other.greediness:
 
669
            return -1
 
670
        elif self.greediness < other.greediness:
 
671
            return 1
 
672
        elif len(self.arguments) > len(other.arguments):
 
673
            return 1
 
674
        elif len(self.arguments) < len(other.arguments):
 
675
            return -1
 
676
        return 1
 
677
 
 
678
    def build_compare(self, other):
 
679
        """Compare this object with another one for building.
 
680
 
 
681
        :internal:
 
682
        """
 
683
        if not other.arguments and self.arguments:
 
684
            return -1
 
685
        elif other.arguments and not self.arguments:
 
686
            return 1
 
687
        elif other.defaults is None and self.defaults is not None:
 
688
            return -1
 
689
        elif other.defaults is not None and self.defaults is None:
 
690
            return 1
 
691
        elif self.provides_defaults_for(other):
 
692
            return -1
 
693
        elif other.provides_defaults_for(self):
 
694
            return 1
 
695
        elif self.greediness > other.greediness:
 
696
            return -1
 
697
        elif self.greediness < other.greediness:
 
698
            return 1
 
699
        elif len(self.arguments) > len(other.arguments):
 
700
            return -1
 
701
        elif len(self.arguments) < len(other.arguments):
 
702
            return 1
 
703
        return -1
 
704
 
 
705
    def __eq__(self, other):
 
706
        return self.__class__ is other.__class__ and \
 
707
               self._trace == other._trace
 
708
 
 
709
    def __ne__(self, other):
 
710
        return not self.__eq__(other)
 
711
 
 
712
    def __unicode__(self):
 
713
        return self.rule
 
714
 
 
715
    def __str__(self):
 
716
        charset = self.map is not None and self.map.charset or 'utf-8'
 
717
        return unicode(self).encode(charset)
 
718
 
 
719
    def __repr__(self):
 
720
        if self.map is None:
 
721
            return '<%s (unbound)>' % self.__class__.__name__
 
722
        charset = self.map is not None and self.map.charset or 'utf-8'
 
723
        tmp = []
 
724
        for is_dynamic, data in self._trace:
 
725
            if is_dynamic:
 
726
                tmp.append('<%s>' % data)
 
727
            else:
 
728
                tmp.append(data)
 
729
        return '<%s %r%s -> %s>' % (
 
730
            self.__class__.__name__,
 
731
            (u''.join(tmp).encode(charset)).lstrip('|'),
 
732
            self.methods is not None and ' (%s)' % \
 
733
                ', '.join(self.methods) or '',
 
734
            self.endpoint
 
735
        )
 
736
 
 
737
 
 
738
class BaseConverter(object):
 
739
    """Base class for all converters."""
 
740
    regex = '[^/]+'
 
741
    is_greedy = False
 
742
    weight = 100
 
743
 
 
744
    def __init__(self, map):
 
745
        self.map = map
 
746
 
 
747
    def to_python(self, value):
 
748
        return value
 
749
 
 
750
    def to_url(self, value):
 
751
        return url_quote(value, self.map.charset)
 
752
 
 
753
 
 
754
class UnicodeConverter(BaseConverter):
 
755
    """This converter is the default converter and accepts any string but
 
756
    only one one path segment.  Thus the string can not include a slash.
 
757
 
 
758
    This is the default validator.
 
759
 
 
760
    Example::
 
761
 
 
762
        Rule('/pages/<page>'),
 
763
        Rule('/<string(length=2):lang_code>')
 
764
 
 
765
    :param map: the :class:`Map`.
 
766
    :param minlength: the minimum length of the string.  Must be greater
 
767
                      or equal 1.
 
768
    :param maxlength: the maximum length of the string.
 
769
    :param length: the exact length of the string.
 
770
    """
 
771
 
 
772
    def __init__(self, map, minlength=1, maxlength=None, length=None):
 
773
        BaseConverter.__init__(self, map)
 
774
        if length is not None:
 
775
            length = '{%d}' % int(length)
 
776
        else:
 
777
            if maxlength is None:
 
778
                maxlength = ''
 
779
            else:
 
780
                maxlength = int(maxlength)
 
781
            length = '{%s,%s}' % (
 
782
                int(minlength),
 
783
                maxlength
 
784
            )
 
785
        self.regex = '[^/]' + length
 
786
 
 
787
 
 
788
class AnyConverter(BaseConverter):
 
789
    """Matches one of the items provided.  Items can either be Python
 
790
    identifiers or unicode strings::
 
791
 
 
792
        Rule('/<any(about, help, imprint, u"class"):page_name>')
 
793
 
 
794
    :param map: the :class:`Map`.
 
795
    :param items: this function accepts the possible items as positional
 
796
                  arguments.
 
797
    """
 
798
 
 
799
    def __init__(self, map, *items):
 
800
        BaseConverter.__init__(self, map)
 
801
        self.regex = '(?:%s)' % '|'.join([re.escape(x) for x in items])
 
802
 
 
803
 
 
804
class PathConverter(BaseConverter):
 
805
    """Like the default :class:`UnicodeConverter`, but it also matches
 
806
    slashes.  This is useful for wikis and similar applications::
 
807
 
 
808
        Rule('/<path:wikipage>')
 
809
        Rule('/<path:wikipage>/edit')
 
810
 
 
811
    :param map: the :class:`Map`.
 
812
    :param minlength: the minimum length of the string.  Must be greater
 
813
                      or equal 1.
 
814
    :param maxlength: the maximum length of the string.
 
815
    :param length: the exact length of the string.
 
816
    """
 
817
    regex = '[^/].*?'
 
818
    is_greedy = True
 
819
    weight = 50
 
820
 
 
821
 
 
822
class NumberConverter(BaseConverter):
 
823
    """Baseclass for `IntegerConverter` and `FloatConverter`.
 
824
 
 
825
    :internal:
 
826
    """
 
827
 
 
828
    def __init__(self, map, fixed_digits=0, min=None, max=None):
 
829
        BaseConverter.__init__(self, map)
 
830
        self.fixed_digits = fixed_digits
 
831
        self.min = min
 
832
        self.max = max
 
833
 
 
834
    def to_python(self, value):
 
835
        if (self.fixed_digits and len(value) != self.fixed_digits):
 
836
            raise ValidationError()
 
837
        value = self.num_convert(value)
 
838
        if (self.min is not None and value < self.min) or \
 
839
           (self.max is not None and value > self.max):
 
840
            raise ValidationError()
 
841
        return value
 
842
 
 
843
    def to_url(self, value):
 
844
        value = self.num_convert(value)
 
845
        if self.fixed_digits:
 
846
            value = ('%%0%sd' % self.fixed_digits) % value
 
847
        return str(value)
 
848
 
 
849
 
 
850
class IntegerConverter(NumberConverter):
 
851
    """This converter only accepts integer values::
 
852
 
 
853
        Rule('/page/<int:page>')
 
854
 
 
855
    This converter does not support negative values.
 
856
 
 
857
    :param map: the :class:`Map`.
 
858
    :param fixed_digits: the number of fixed digits in the URL.  If you set
 
859
                         this to ``4`` for example, the application will
 
860
                         only match if the url looks like ``/0001/``.  The
 
861
                         default is variable length.
 
862
    :param min: the minimal value.
 
863
    :param max: the maximal value.
 
864
    """
 
865
    regex = r'\d+'
 
866
    num_convert = int
 
867
 
 
868
 
 
869
class FloatConverter(NumberConverter):
 
870
    """This converter only accepts floating point values::
 
871
 
 
872
        Rule('/probability/<float:probability>')
 
873
 
 
874
    This converter does not support negative values.
 
875
 
 
876
    :param map: the :class:`Map`.
 
877
    :param min: the minimal value.
 
878
    :param max: the maximal value.
 
879
    """
 
880
    regex = r'\d+\.\d+'
 
881
    num_convert = float
 
882
 
 
883
    def __init__(self, map, min=None, max=None):
 
884
        NumberConverter.__init__(self, map, 0, min, max)
 
885
 
 
886
 
 
887
class Map(object):
 
888
    """The map class stores all the URL rules and some configuration
 
889
    parameters.  Some of the configuration values are only stored on the
 
890
    `Map` instance since those affect all rules, others are just defaults
 
891
    and can be overridden for each rule.  Note that you have to specify all
 
892
    arguments besides the `rules` as keywords arguments!
 
893
 
 
894
    :param rules: sequence of url rules for this map.
 
895
    :param default_subdomain: The default subdomain for rules without a
 
896
                              subdomain defined.
 
897
    :param charset: charset of the url. defaults to ``"utf-8"``
 
898
    :param strict_slashes: Take care of trailing slashes.
 
899
    :param redirect_defaults: This will redirect to the default rule if it
 
900
                              wasn't visited that way. This helps creating
 
901
                              unique URLs.
 
902
    :param converters: A dict of converters that adds additional converters
 
903
                       to the list of converters. If you redefine one
 
904
                       converter this will override the original one.
 
905
    :param sort_parameters: If set to `True` the url parameters are sorted.
 
906
                            See `url_encode` for more details.
 
907
    :param sort_key: The sort key function for `url_encode`.
 
908
 
 
909
    .. versionadded:: 0.5
 
910
        `sort_parameters` and `sort_key` was added.
 
911
    """
 
912
 
 
913
    def __init__(self, rules=None, default_subdomain='', charset='utf-8',
 
914
                 strict_slashes=True, redirect_defaults=True,
 
915
                 converters=None, sort_parameters=False, sort_key=None):
 
916
        self._rules = []
 
917
        self._rules_by_endpoint = {}
 
918
        self._remap = True
 
919
 
 
920
        self.default_subdomain = default_subdomain
 
921
        self.charset = charset
 
922
        self.strict_slashes = strict_slashes
 
923
        self.redirect_defaults = redirect_defaults
 
924
 
 
925
        self.converters = DEFAULT_CONVERTERS.copy()
 
926
        if converters:
 
927
            self.converters.update(converters)
 
928
 
 
929
        self.sort_parameters = sort_parameters
 
930
        self.sort_key = sort_key
 
931
 
 
932
        for rulefactory in rules or ():
 
933
            self.add(rulefactory)
 
934
 
 
935
    def is_endpoint_expecting(self, endpoint, *arguments):
 
936
        """Iterate over all rules and check if the endpoint expects
 
937
        the arguments provided.  This is for example useful if you have
 
938
        some URLs that expect a language code and others that do not and
 
939
        you want to wrap the builder a bit so that the current language
 
940
        code is automatically added if not provided but endpoints expect
 
941
        it.
 
942
 
 
943
        :param endpoint: the endpoint to check.
 
944
        :param arguments: this function accepts one or more arguments
 
945
                          as positional arguments.  Each one of them is
 
946
                          checked.
 
947
        """
 
948
        self.update()
 
949
        arguments = set(arguments)
 
950
        for rule in self._rules_by_endpoint[endpoint]:
 
951
            if arguments.issubset(rule.arguments):
 
952
                return True
 
953
        return False
 
954
 
 
955
    def iter_rules(self, endpoint=None):
 
956
        """Iterate over all rules or the rules of an endpoint.
 
957
 
 
958
        :param endpoint: if provided only the rules for that endpoint
 
959
                         are returned.
 
960
        :return: an iterator
 
961
        """
 
962
        if endpoint is not None:
 
963
            return iter(self._rules_by_endpoint[endpoint])
 
964
        return iter(self._rules)
 
965
 
 
966
    def add(self, rulefactory):
 
967
        """Add a new rule or factory to the map and bind it.  Requires that the
 
968
        rule is not bound to another map.
 
969
 
 
970
        :param rulefactory: a :class:`Rule` or :class:`RuleFactory`
 
971
        """
 
972
        for rule in rulefactory.get_rules(self):
 
973
            rule.bind(self)
 
974
            self._rules.append(rule)
 
975
            self._rules_by_endpoint.setdefault(rule.endpoint, []).append(rule)
 
976
        self._remap = True
 
977
 
 
978
    def bind(self, server_name, script_name=None, subdomain=None,
 
979
             url_scheme='http', default_method='GET', path_info=None):
 
980
        """Return a new :class:`MapAdapter` with the details specified to the
 
981
        call.  Note that `script_name` will default to ``'/'`` if not further
 
982
        specified or `None`.  The `server_name` at least is a requirement
 
983
        because the HTTP RFC requires absolute URLs for redirects and so all
 
984
        redirect exceptions raised by Werkzeug will contain the full canonical
 
985
        URL.
 
986
 
 
987
        If no path_info is passed to :meth:`match` it will use the default path
 
988
        info passed to bind.  While this doesn't really make sense for
 
989
        manual bind calls, it's useful if you bind a map to a WSGI
 
990
        environment which already contains the path info.
 
991
 
 
992
        `subdomain` will default to the `default_subdomain` for this map if
 
993
        no defined. If there is no `default_subdomain` you cannot use the
 
994
        subdomain feature.
 
995
        """
 
996
        if subdomain is None:
 
997
            subdomain = self.default_subdomain
 
998
        if script_name is None:
 
999
            script_name = '/'
 
1000
        return MapAdapter(self, server_name, script_name, subdomain,
 
1001
                          url_scheme, path_info, default_method)
 
1002
 
 
1003
    def bind_to_environ(self, environ, server_name=None, subdomain=None):
 
1004
        """Like :meth:`bind` but you can pass it an WSGI environment and it
 
1005
        will fetch the information from that directory.  Note that because of
 
1006
        limitations in the protocol there is no way to get the current
 
1007
        subdomain and real `server_name` from the environment.  If you don't
 
1008
        provide it, Werkzeug will use `SERVER_NAME` and `SERVER_PORT` (or
 
1009
        `HTTP_HOST` if provided) as used `server_name` with disabled subdomain
 
1010
        feature.
 
1011
 
 
1012
        If `subdomain` is `None` but an environment and a server name is
 
1013
        provided it will calculate the current subdomain automatically.
 
1014
        Example: `server_name` is ``'example.com'`` and the `SERVER_NAME`
 
1015
        in the wsgi `environ` is ``'staging.dev.example.com'`` the calculated
 
1016
        subdomain will be ``'staging.dev'``.
 
1017
 
 
1018
        If the object passed as environ has an environ attribute, the value of
 
1019
        this attribute is used instead.  This allows you to pass request
 
1020
        objects.  Additionally `PATH_INFO` added as a default ot the
 
1021
        :class:`MapAdapter` so that you don't have to pass the path info to
 
1022
        the match method.
 
1023
 
 
1024
        .. versionchanged:: 0.5
 
1025
            previously this method accepted a bogus `calculate_subdomain`
 
1026
            parameter that did not have any effect.  It was removed because
 
1027
            of that.
 
1028
 
 
1029
        :param environ: a WSGI environment.
 
1030
        :param server_name: an optional server name hint (see above).
 
1031
        :param subdomain: optionally the current subdomain (see above).
 
1032
        """
 
1033
        if hasattr(environ, 'environ'):
 
1034
            environ = environ.environ
 
1035
        if server_name is None:
 
1036
            if 'HTTP_HOST' in environ:
 
1037
                server_name = environ['HTTP_HOST']
 
1038
            else:
 
1039
                server_name = environ['SERVER_NAME']
 
1040
                if (environ['wsgi.url_scheme'], environ['SERVER_PORT']) not \
 
1041
                   in (('https', '443'), ('http', '80')):
 
1042
                    server_name += ':' + environ['SERVER_PORT']
 
1043
        elif subdomain is None:
 
1044
            cur_server_name = environ.get('HTTP_HOST',
 
1045
                environ['SERVER_NAME']).split(':', 1)[0].split('.')
 
1046
            real_server_name = server_name.split(':', 1)[0].split('.')
 
1047
            offset = -len(real_server_name)
 
1048
            if cur_server_name[offset:] != real_server_name:
 
1049
                raise ValueError('the server name provided (%r) does not '
 
1050
                                 'match the server name from the WSGI '
 
1051
                                 'environment (%r)' %
 
1052
                                 (environ['SERVER_NAME'], server_name))
 
1053
            subdomain = '.'.join(filter(None, cur_server_name[:offset]))
 
1054
        return Map.bind(self, server_name, environ.get('SCRIPT_NAME'),
 
1055
                        subdomain, environ['wsgi.url_scheme'],
 
1056
                        environ['REQUEST_METHOD'], environ.get('PATH_INFO'))
 
1057
 
 
1058
    def update(self):
 
1059
        """Called before matching and building to keep the compiled rules
 
1060
        in the correct order after things changed.
 
1061
        """
 
1062
        if self._remap:
 
1063
            self._rules.sort(lambda a, b: a.match_compare(b))
 
1064
            for rules in self._rules_by_endpoint.itervalues():
 
1065
                rules.sort(lambda a, b: a.build_compare(b))
 
1066
            self._remap = False
 
1067
 
 
1068
 
 
1069
class MapAdapter(object):
 
1070
    """Returned by :meth:`Map.bind` or :meth:`Map.bind_to_environ` and does
 
1071
    the URL matching and building based on runtime information.
 
1072
    """
 
1073
 
 
1074
    def __init__(self, map, server_name, script_name, subdomain,
 
1075
                 url_scheme, path_info, default_method):
 
1076
        self.map = map
 
1077
        self.server_name = server_name
 
1078
        if not script_name.endswith('/'):
 
1079
            script_name += '/'
 
1080
        self.script_name = script_name
 
1081
        self.subdomain = subdomain
 
1082
        self.url_scheme = url_scheme
 
1083
        self.path_info = path_info or u''
 
1084
        self.default_method = default_method
 
1085
 
 
1086
    def dispatch(self, view_func, path_info=None, method=None,
 
1087
                 catch_http_exceptions=False):
 
1088
        """Does the complete dispatching process.  `view_func` is called with
 
1089
        the endpoint and a dict with the values for the view.  It should
 
1090
        look up the view function, call it, and return a response object
 
1091
        or WSGI application.  http exceptions are not catched by default
 
1092
        so that applications can display nicer error messages by just
 
1093
        catching them by hand.  If you want to stick with the default
 
1094
        error messages you can pass it ``catch_http_exceptions=True`` and
 
1095
        it will catch the http exceptions.
 
1096
 
 
1097
        Here a small example for the dispatch usage::
 
1098
 
 
1099
            from werkzeug import Request, Response, responder
 
1100
            from werkzeug.routing import Map, Rule
 
1101
 
 
1102
            def on_index(request):
 
1103
                return Response('Hello from the index')
 
1104
 
 
1105
            url_map = Map([Rule('/', endpoint='index')])
 
1106
            views = {'index': on_index}
 
1107
 
 
1108
            @responder
 
1109
            def application(environ, start_response):
 
1110
                request = Request(environ)
 
1111
                urls = url_map.bind_to_environ(environ)
 
1112
                return urls.dispatch(lambda e, v: views[e](request, **v),
 
1113
                                     catch_http_exceptions=True)
 
1114
 
 
1115
        Keep in mind that this method might return exception objects, too, so
 
1116
        use :class:`Response.force_type` to get a response object.
 
1117
 
 
1118
        :param view_func: a function that is called with the endpoint as
 
1119
                          first argument and the value dict as second.  Has
 
1120
                          to dispatch to the actual view function with this
 
1121
                          information.  (see above)
 
1122
        :param path_info: the path info to use for matching.  Overrides the
 
1123
                          path info specified on binding.
 
1124
        :param method: the HTTP method used for matching.  Overrides the
 
1125
                       method specified on binding.
 
1126
        :param catch_http_exceptions: set to `True` to catch any of the
 
1127
                                      werkzeug :class:`HTTPException`\s.
 
1128
        """
 
1129
        try:
 
1130
            try:
 
1131
                endpoint, args = self.match(path_info, method)
 
1132
            except RequestRedirect, e:
 
1133
                return e
 
1134
            return view_func(endpoint, args)
 
1135
        except HTTPException, e:
 
1136
            if catch_http_exceptions:
 
1137
                return e
 
1138
            raise
 
1139
 
 
1140
    def match(self, path_info=None, method=None):
 
1141
        """The usage is simple: you just pass the match method the current
 
1142
        path info as well as the method (which defaults to `GET`).  The
 
1143
        following things can then happen:
 
1144
 
 
1145
        - you receive a `NotFound` exception that indicates that no URL is
 
1146
          matching.  A `NotFound` exception is also a WSGI application you
 
1147
          can call to get a default page not found page (happens to be the
 
1148
          same object as `werkzeug.exceptions.NotFound`)
 
1149
 
 
1150
        - you receive a `MethodNotAllowed` exception that indicates that there
 
1151
          is a match for this URL but non for the current request method.
 
1152
          This is useful for RESTful applications.
 
1153
 
 
1154
        - you receive a `RequestRedirect` exception with a `new_url`
 
1155
          attribute.  This exception is used to notify you about a request
 
1156
          Werkzeug requests by your WSGI application.  This is for example the
 
1157
          case if you request ``/foo`` although the correct URL is ``/foo/``
 
1158
          You can use the `RequestRedirect` instance as response-like object
 
1159
          similar to all other subclasses of `HTTPException`.
 
1160
 
 
1161
        - you get a tuple in the form ``(endpoint, arguments)`` when there is
 
1162
          a match.
 
1163
 
 
1164
        If the path info is not passed to the match method the default path
 
1165
        info of the map is used (defaults to the root URL if not defined
 
1166
        explicitly).
 
1167
 
 
1168
        All of the exceptions raised are subclasses of `HTTPException` so they
 
1169
        can be used as WSGI responses.  The will all render generic error or
 
1170
        redirect pages.
 
1171
 
 
1172
        Here is a small example for matching:
 
1173
 
 
1174
        >>> m = Map([
 
1175
        ...     Rule('/', endpoint='index'),
 
1176
        ...     Rule('/downloads/', endpoint='downloads/index'), 
 
1177
        ...     Rule('/downloads/<int:id>', endpoint='downloads/show')
 
1178
        ... ])
 
1179
        >>> urls = m.bind("example.com", "/")
 
1180
        >>> urls.match("/", "GET")
 
1181
        ('index', {})
 
1182
        >>> urls.match("/downloads/42")
 
1183
        ('downloads/show', {'id': 42})
 
1184
 
 
1185
        And here is what happens on redirect and missing URLs:
 
1186
 
 
1187
        >>> urls.match("/downloads")
 
1188
        Traceback (most recent call last):
 
1189
          ...
 
1190
        RequestRedirect: http://example.com/downloads/
 
1191
        >>> urls.match("/missing")
 
1192
        Traceback (most recent call last):
 
1193
          ...
 
1194
        NotFound: 404 Not Found
 
1195
 
 
1196
        :param path_info: the path info to use for matching.  Overrides the
 
1197
                          path info specified on binding.
 
1198
        :param method: the HTTP method used for matching.  Overrides the
 
1199
                       method specified on binding.
 
1200
        """
 
1201
        self.map.update()
 
1202
        if path_info is None:
 
1203
            path_info = self.path_info
 
1204
        if not isinstance(path_info, unicode):
 
1205
            path_info = path_info.decode(self.map.charset, 'ignore')
 
1206
        method = (method or self.default_method).upper()
 
1207
        path = u'%s|/%s' % (self.subdomain, path_info.lstrip('/'))
 
1208
        have_match_for = set()
 
1209
        for rule in self.map._rules:
 
1210
            try:
 
1211
                rv = rule.match(path)
 
1212
            except RequestSlash:
 
1213
                raise RequestRedirect(str('%s://%s%s%s/%s/' % (
 
1214
                    self.url_scheme,
 
1215
                    self.subdomain and self.subdomain + '.' or '',
 
1216
                    self.server_name,
 
1217
                    self.script_name[:-1],
 
1218
                    url_quote(path_info.lstrip('/'), self.map.charset)
 
1219
                )))
 
1220
            if rv is None:
 
1221
                continue
 
1222
            if rule.methods is not None and method not in rule.methods:
 
1223
                have_match_for.update(rule.methods)
 
1224
                continue
 
1225
            if self.map.redirect_defaults:
 
1226
                for r in self.map._rules_by_endpoint[rule.endpoint]:
 
1227
                    if r.provides_defaults_for(rule) and \
 
1228
                       r.suitable_for(rv, method):
 
1229
                        rv.update(r.defaults)
 
1230
                        subdomain, path = r.build(rv)
 
1231
                        raise RequestRedirect(str('%s://%s%s%s/%s' % (
 
1232
                            self.url_scheme,
 
1233
                            subdomain and subdomain + '.' or '',
 
1234
                            self.server_name,
 
1235
                            self.script_name[:-1],
 
1236
                            url_quote(path.lstrip('/'), self.map.charset)
 
1237
                        )))
 
1238
            if rule.redirect_to is not None:
 
1239
                if isinstance(rule.redirect_to, basestring):
 
1240
                    def _handle_match(match):
 
1241
                        value = rv[match.group(1)]
 
1242
                        return rule._converters[match.group(1)].to_url(value)
 
1243
                    redirect_url = _simple_rule_re.sub(_handle_match,
 
1244
                                                       rule.redirect_to)
 
1245
                else:
 
1246
                    redirect_url = rule.redirect_to(self, **rv)
 
1247
                raise RequestRedirect(str(urljoin('%s://%s%s%s' % (
 
1248
                    self.url_scheme,
 
1249
                    self.subdomain and self.subdomain + '.' or '',
 
1250
                    self.server_name,
 
1251
                    self.script_name
 
1252
                ), redirect_url)))
 
1253
            return rule.endpoint, rv
 
1254
        if have_match_for:
 
1255
            raise MethodNotAllowed(valid_methods=list(have_match_for))
 
1256
        raise NotFound()
 
1257
 
 
1258
    def test(self, path_info=None, method=None):
 
1259
        """Test if a rule would match.  Works like `match` but returns `True`
 
1260
        if the URL matches, or `False` if it does not exist.
 
1261
 
 
1262
        :param path_info: the path info to use for matching.  Overrides the
 
1263
                          path info specified on binding.
 
1264
        :param method: the HTTP method used for matching.  Overrides the
 
1265
                       method specified on binding.
 
1266
        """
 
1267
        try:
 
1268
            self.match(path_info, method)
 
1269
        except RequestRedirect:
 
1270
            pass
 
1271
        except NotFound:
 
1272
            return False
 
1273
        return True
 
1274
 
 
1275
    def build(self, endpoint, values=None, method=None, force_external=False):
 
1276
        """Building URLs works pretty much the other way round.  Instead of
 
1277
        `match` you call `build` and pass it the endpoint and a dict of
 
1278
        arguments for the placeholders.
 
1279
 
 
1280
        The `build` function also accepts an argument called `force_external`
 
1281
        which, if you set it to `True` will force external URLs. Per default
 
1282
        external URLs (include the server name) will only be used if the
 
1283
        target URL is on a different subdomain.
 
1284
 
 
1285
        >>> m = Map([
 
1286
        ...     Rule('/', endpoint='index'),
 
1287
        ...     Rule('/downloads/', endpoint='downloads/index'), 
 
1288
        ...     Rule('/downloads/<int:id>', endpoint='downloads/show')
 
1289
        ... ])
 
1290
        >>> urls = m.bind("example.com", "/")
 
1291
        >>> urls.build("index", {})
 
1292
        '/'
 
1293
        >>> urls.build("downloads/show", {'id': 42})
 
1294
        '/downloads/42'
 
1295
        >>> urls.build("downloads/show", {'id': 42}, force_external=True)
 
1296
        'http://example.com/downloads/42'
 
1297
 
 
1298
        Because URLs cannot contain non ASCII data you will always get
 
1299
        bytestrings back.  Non ASCII characters are urlencoded with the
 
1300
        charset defined on the map instance.
 
1301
 
 
1302
        Additional values are converted to unicode and appended to the URL as
 
1303
        URL querystring parameters:
 
1304
 
 
1305
        >>> urls.build("index", {'q': 'My Searchstring'})
 
1306
        '/?q=My+Searchstring'
 
1307
 
 
1308
        If a rule does not exist when building a `BuildError` exception is
 
1309
        raised.
 
1310
 
 
1311
        The build method accepts an argument called `method` which allows you
 
1312
        to specify the method you want to have an URL built for if you have
 
1313
        different methods for the same endpoint specified.
 
1314
 
 
1315
        :param endpoint: the endpoint of the URL to build.
 
1316
        :param values: the values for the URL to build.  Unhandled values are
 
1317
                       appended to the URL as query parameters.
 
1318
        :param method: the HTTP method for the rule if there are different
 
1319
                       URLs for different methods on the same endpoint.
 
1320
        :param force_external: enforce full canonical external URLs.
 
1321
        """
 
1322
        self.map.update()
 
1323
        method = method or self.default_method
 
1324
        if values:
 
1325
            values = dict([(k, v) for k, v in values.items() if v is not None])
 
1326
        else:
 
1327
            values = {}
 
1328
 
 
1329
        for rule in self.map._rules_by_endpoint.get(endpoint, ()):
 
1330
            if rule.suitable_for(values, method):
 
1331
                rv = rule.build(values)
 
1332
                if rv is not None:
 
1333
                    break
 
1334
        else:
 
1335
            raise BuildError(endpoint, values, method)
 
1336
        subdomain, path = rv
 
1337
        if not force_external and subdomain == self.subdomain:
 
1338
            return str(urljoin(self.script_name, path.lstrip('/')))
 
1339
        return str('%s://%s%s%s/%s' % (
 
1340
            self.url_scheme,
 
1341
            subdomain and subdomain + '.' or '',
 
1342
            self.server_name,
 
1343
            self.script_name[:-1],
 
1344
            path.lstrip('/')
 
1345
        ))
 
1346
 
 
1347
 
 
1348
#: the default converter mapping for the map.
 
1349
DEFAULT_CONVERTERS = {
 
1350
    'default':          UnicodeConverter,
 
1351
    'string':           UnicodeConverter,
 
1352
    'any':              AnyConverter,
 
1353
    'path':             PathConverter,
 
1354
    'int':              IntegerConverter,
 
1355
    'float':            FloatConverter
 
1356
}