1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 import re
20
21 from jinja2.runtime import Context as Jinja2Context
22
23 from trac.util.text import unicode_quote, unicode_urlencode
24
25
26 slashes_re = re.compile(r'/{2,}')
27
28
30 """Implements a callable that constructs URLs with the given base. The
31 function can be called with any number of positional and keyword
32 arguments which then are used to assemble the URL.
33
34 Positional arguments are appended as individual segments to
35 the path of the URL:
36
37 >>> href = Href('/trac')
38 >>> repr(href)
39 "<Href '/trac'>"
40 >>> href('ticket', 540)
41 '/trac/ticket/540'
42 >>> href('ticket', 540, 'attachment', 'bugfix.patch')
43 '/trac/ticket/540/attachment/bugfix.patch'
44 >>> href('ticket', '540/attachment/bugfix.patch')
45 '/trac/ticket/540/attachment/bugfix.patch'
46
47 If a positional parameter evaluates to None, it will be skipped:
48
49 >>> href('ticket', 540, 'attachment', None)
50 '/trac/ticket/540/attachment'
51
52 The first path segment can also be specified by calling an attribute
53 of the instance, as follows:
54
55 >>> href.ticket(540)
56 '/trac/ticket/540'
57 >>> href.changeset(42, format='diff')
58 '/trac/changeset/42?format=diff'
59
60 Simply calling the Href object with no arguments will return the base URL:
61
62 >>> href()
63 '/trac'
64
65 Keyword arguments are added to the query string, unless the value is None:
66
67 >>> href = Href('/trac')
68 >>> href('timeline', format='rss')
69 '/trac/timeline?format=rss'
70 >>> href('timeline', format=None)
71 '/trac/timeline'
72 >>> href('search', q='foo bar')
73 '/trac/search?q=foo+bar'
74
75 Multiple values for one parameter are specified using a sequence (a list or
76 tuple) for the parameter:
77
78 >>> href('timeline', show=['ticket', 'wiki', 'changeset'])
79 '/trac/timeline?show=ticket&show=wiki&show=changeset'
80
81 Alternatively, query string parameters can be added by passing a dict or
82 list as last positional argument:
83
84 >>> href('timeline', {'from': '02/24/05', 'daysback': 30})
85 '/trac/timeline?daysback=30&from=02%2F24%2F05'
86 >>> href('timeline', {})
87 '/trac/timeline'
88 >>> href('timeline', [('from', '02/24/05')])
89 '/trac/timeline?from=02%2F24%2F05'
90 >>> href('timeline', ()) == href('timeline', []) == href('timeline', {})
91 True
92
93 The usual way of quoting arguments that would otherwise be interpreted
94 as Python keywords is supported too:
95
96 >>> href('timeline', from_='02/24/05', daysback=30)
97 '/trac/timeline?from=02%2F24%2F05&daysback=30'
98
99 If the order of query string parameters should be preserved, you may also
100 pass a sequence of (name, value) tuples as last positional argument:
101
102 >>> href('query', (('group', 'component'), ('groupdesc', 1)))
103 '/trac/query?group=component&groupdesc=1'
104
105 >>> params = []
106 >>> params.append(('group', 'component'))
107 >>> params.append(('groupdesc', 1))
108 >>> href('query', params)
109 '/trac/query?group=component&groupdesc=1'
110
111 By specifying an absolute base, the function returned will also generate
112 absolute URLs:
113
114 >>> href = Href('https://trac.edgewall.org')
115 >>> href('ticket', 540)
116 'https://trac.edgewall.org/ticket/540'
117
118 >>> href = Href('https://trac.edgewall.org')
119 >>> href('ticket', 540)
120 'https://trac.edgewall.org/ticket/540'
121
122 In common usage, it may improve readability to use the function-calling
123 ability for the first component of the URL as mentioned earlier:
124
125 >>> href = Href('/trac')
126 >>> href.ticket(540)
127 '/trac/ticket/540'
128 >>> href.browser('/trunk/README.txt', format='txt')
129 '/trac/browser/trunk/README.txt?format=txt'
130
131 The ``path_safe`` argument specifies the characters that don't
132 need to be quoted in the path arguments. Likewise, the
133 ``query_safe`` argument specifies the characters that don't need
134 to be quoted in the query string:
135
136 >>> href = Href('')
137 >>> href.milestone('<look,here>', param='<here,too>')
138 '/milestone/%3Clook%2Chere%3E?param=%3Chere%2Ctoo%3E'
139
140 >>> href = Href('', path_safe='/<,', query_safe=',>')
141 >>> href.milestone('<look,here>', param='<here,too>')
142 '/milestone/<look,here%3E?param=%3Chere,too>'
143 """
144
145 - def __init__(self, base, path_safe="/!~*'()", query_safe="!~*'()"):
146 self.base = base.rstrip('/')
147 self.path_safe = path_safe
148 self.query_safe = query_safe
149 self._derived = {}
150
152 return '<%s %r>' % (self.__class__.__name__, self.base)
153
155 href = self.base
156 params = []
157
158 def add_param(name, value):
159 if isinstance(value, (list, tuple)):
160 for i in [i for i in value if i is not None]:
161 params.append((name, i))
162 elif value is not None:
163 params.append((name, value))
164
165
166
167 if args and isinstance(args[0], Jinja2Context):
168 args = args[1:]
169 if args:
170 lastp = args[-1]
171 if isinstance(lastp, dict):
172 for k, v in lastp.items():
173 add_param(k, v)
174 args = args[:-1]
175 elif isinstance(lastp, (list, tuple)):
176 for k, v in lastp:
177 add_param(k, v)
178 args = args[:-1]
179
180
181 path = '/'.join(unicode_quote(unicode(arg).strip('/'), self.path_safe)
182 for arg in args if arg is not None)
183 if path:
184 href += '/' + slashes_re.sub('/', path).lstrip('/')
185 elif not href:
186 href = '/'
187
188
189 for k, v in kw.items():
190 add_param(k[:-1] if k.endswith('_') else k, v)
191 if params:
192 href += '?' + unicode_urlencode(params, self.query_safe)
193
194 return href
195
197 if name not in self._derived:
198 self._derived[name] = lambda *args, **kw: self(name, *args, **kw)
199 return self._derived[name]
200
201 _printable_safe = ''.join(map(chr, xrange(0x21, 0x7f)))
202
212
213
214 if __name__ == '__main__':
215 import doctest, sys
216 doctest.testmod(sys.modules[__name__])
217