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