Package trac :: Package web :: Module href

Source Code for Module trac.web.href

  1  # -*- coding: utf-8 -*- 
  2  # 
  3  # Copyright (C) 2003-2023 Edgewall Software 
  4  # Copyright (C) 2003-2004 Jonas Borgström <[email protected]> 
  5  # Copyright (C) 2005 Christopher Lenz <[email protected]> 
  6  # All rights reserved. 
  7  # 
  8  # This software is licensed as described in the file COPYING, which 
  9  # you should have received as part of this distribution. The terms 
 10  # are also available at https://trac.edgewall.org/wiki/TracLicense. 
 11  # 
 12  # This software consists of voluntary contributions made by many 
 13  # individuals. For the exact contribution history, see the revision 
 14  # history and logs, available at https://trac.edgewall.org/log/. 
 15  # 
 16  # Author: Jonas Borgström <[email protected]> 
 17  #         Christopher Lenz <[email protected]> 
 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   
29 -class Href(object):
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
151 - def __repr__(self):
152 return '<%s %r>' % (self.__class__.__name__, self.base)
153
154 - def __call__(self, *args, **kw):
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 # Skip Jinja2 context (#13244) 166 # Only needed for Jinja versions 2.11.0 and 2.11.1 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 # build the path 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 # assemble the query string 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
196 - def __getattr__(self, name):
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
203 - def __add__(self, rhs):
204 if not rhs: 205 return self.base or '/' 206 if rhs.startswith('?'): 207 return (self.base or '/') + \ 208 unicode_quote(rhs, self._printable_safe) 209 if not rhs.startswith('/'): 210 rhs = '/' + rhs 211 return self.base + unicode_quote(rhs, self._printable_safe)
212 213 214 if __name__ == '__main__': 215 import doctest, sys 216 doctest.testmod(sys.modules[__name__]) 217