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