url.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
  2. # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
  3. """
  4. This module implements a class for handling URLs.
  5. """
  6. import urllib
  7. import cgi
  8. from paste import request
  9. # Imported lazily from FormEncode:
  10. variabledecode = None
  11. __all__ = ["URL", "Image"]
  12. def html_quote(v):
  13. if v is None:
  14. return ''
  15. return cgi.escape(str(v), 1)
  16. def url_quote(v):
  17. if v is None:
  18. return ''
  19. return urllib.quote(str(v))
  20. url_unquote = urllib.unquote
  21. def js_repr(v):
  22. if v is None:
  23. return 'null'
  24. elif v is False:
  25. return 'false'
  26. elif v is True:
  27. return 'true'
  28. elif isinstance(v, list):
  29. return '[%s]' % ', '.join(map(js_repr, v))
  30. elif isinstance(v, dict):
  31. return '{%s}' % ', '.join(
  32. ['%s: %s' % (js_repr(key), js_repr(value))
  33. for key, value in v])
  34. elif isinstance(v, str):
  35. return repr(v)
  36. elif isinstance(v, unicode):
  37. # @@: how do you do Unicode literals in Javascript?
  38. return repr(v.encode('UTF-8'))
  39. elif isinstance(v, (float, int)):
  40. return repr(v)
  41. elif isinstance(v, long):
  42. return repr(v).lstrip('L')
  43. elif hasattr(v, '__js_repr__'):
  44. return v.__js_repr__()
  45. else:
  46. raise ValueError(
  47. "I don't know how to turn %r into a Javascript representation"
  48. % v)
  49. class URLResource(object):
  50. """
  51. This is an abstract superclass for different kinds of URLs
  52. """
  53. default_params = {}
  54. def __init__(self, url, vars=None, attrs=None,
  55. params=None):
  56. self.url = url or '/'
  57. self.vars = vars or []
  58. self.attrs = attrs or {}
  59. self.params = self.default_params.copy()
  60. self.original_params = params or {}
  61. if params:
  62. self.params.update(params)
  63. #@classmethod
  64. def from_environ(cls, environ, with_query_string=True,
  65. with_path_info=True, script_name=None,
  66. path_info=None, querystring=None):
  67. url = request.construct_url(
  68. environ, with_query_string=False,
  69. with_path_info=with_path_info, script_name=script_name,
  70. path_info=path_info)
  71. if with_query_string:
  72. if querystring is None:
  73. vars = request.parse_querystring(environ)
  74. else:
  75. vars = cgi.parse_qsl(
  76. querystring,
  77. keep_blank_values=True,
  78. strict_parsing=False)
  79. else:
  80. vars = None
  81. v = cls(url, vars=vars)
  82. return v
  83. from_environ = classmethod(from_environ)
  84. def __call__(self, *args, **kw):
  85. res = self._add_positional(args)
  86. res = res._add_vars(kw)
  87. return res
  88. def __getitem__(self, item):
  89. if '=' in item:
  90. name, value = item.split('=', 1)
  91. return self._add_vars({url_unquote(name): url_unquote(value)})
  92. return self._add_positional((item,))
  93. def attr(self, **kw):
  94. for key in kw.keys():
  95. if key.endswith('_'):
  96. kw[key[:-1]] = kw[key]
  97. del kw[key]
  98. new_attrs = self.attrs.copy()
  99. new_attrs.update(kw)
  100. return self.__class__(self.url, vars=self.vars,
  101. attrs=new_attrs,
  102. params=self.original_params)
  103. def param(self, **kw):
  104. new_params = self.original_params.copy()
  105. new_params.update(kw)
  106. return self.__class__(self.url, vars=self.vars,
  107. attrs=self.attrs,
  108. params=new_params)
  109. def coerce_vars(self, vars):
  110. global variabledecode
  111. need_variable_encode = False
  112. for key, value in vars.items():
  113. if isinstance(value, dict):
  114. need_variable_encode = True
  115. if key.endswith('_'):
  116. vars[key[:-1]] = vars[key]
  117. del vars[key]
  118. if need_variable_encode:
  119. if variabledecode is None:
  120. from formencode import variabledecode
  121. vars = variabledecode.variable_encode(vars)
  122. return vars
  123. def var(self, **kw):
  124. kw = self.coerce_vars(kw)
  125. new_vars = self.vars + kw.items()
  126. return self.__class__(self.url, vars=new_vars,
  127. attrs=self.attrs,
  128. params=self.original_params)
  129. def setvar(self, **kw):
  130. """
  131. Like ``.var(...)``, except overwrites keys, where .var simply
  132. extends the keys. Setting a variable to None here will
  133. effectively delete it.
  134. """
  135. kw = self.coerce_vars(kw)
  136. new_vars = []
  137. for name, values in self.vars:
  138. if name in kw:
  139. continue
  140. new_vars.append((name, values))
  141. new_vars.extend(kw.items())
  142. return self.__class__(self.url, vars=new_vars,
  143. attrs=self.attrs,
  144. params=self.original_params)
  145. def setvars(self, **kw):
  146. """
  147. Creates a copy of this URL, but with all the variables set/reset
  148. (like .setvar(), except clears past variables at the same time)
  149. """
  150. return self.__class__(self.url, vars=kw.items(),
  151. attrs=self.attrs,
  152. params=self.original_params)
  153. def addpath(self, *paths):
  154. u = self
  155. for path in paths:
  156. path = str(path).lstrip('/')
  157. new_url = u.url
  158. if not new_url.endswith('/'):
  159. new_url += '/'
  160. u = u.__class__(new_url+path, vars=u.vars,
  161. attrs=u.attrs,
  162. params=u.original_params)
  163. return u
  164. __div__ = addpath
  165. def become(self, OtherClass):
  166. return OtherClass(self.url, vars=self.vars,
  167. attrs=self.attrs,
  168. params=self.original_params)
  169. def href__get(self):
  170. s = self.url
  171. if self.vars:
  172. s += '?'
  173. vars = []
  174. for name, val in self.vars:
  175. if isinstance(val, (list, tuple)):
  176. val = [v for v in val if v is not None]
  177. elif val is None:
  178. continue
  179. vars.append((name, val))
  180. s += urllib.urlencode(vars, True)
  181. return s
  182. href = property(href__get)
  183. def __repr__(self):
  184. base = '<%s %s' % (self.__class__.__name__,
  185. self.href or "''")
  186. if self.attrs:
  187. base += ' attrs(%s)' % (
  188. ' '.join(['%s="%s"' % (html_quote(n), html_quote(v))
  189. for n, v in self.attrs.items()]))
  190. if self.original_params:
  191. base += ' params(%s)' % (
  192. ', '.join(['%s=%r' % (n, v)
  193. for n, v in self.attrs.items()]))
  194. return base + '>'
  195. def html__get(self):
  196. if not self.params.get('tag'):
  197. raise ValueError(
  198. "You cannot get the HTML of %r until you set the "
  199. "'tag' param'" % self)
  200. content = self._get_content()
  201. tag = '<%s' % self.params.get('tag')
  202. attrs = ' '.join([
  203. '%s="%s"' % (html_quote(n), html_quote(v))
  204. for n, v in self._html_attrs()])
  205. if attrs:
  206. tag += ' ' + attrs
  207. tag += self._html_extra()
  208. if content is None:
  209. return tag + ' />'
  210. else:
  211. return '%s>%s</%s>' % (tag, content, self.params.get('tag'))
  212. html = property(html__get)
  213. def _html_attrs(self):
  214. return self.attrs.items()
  215. def _html_extra(self):
  216. return ''
  217. def _get_content(self):
  218. """
  219. Return the content for a tag (for self.html); return None
  220. for an empty tag (like ``<img />``)
  221. """
  222. raise NotImplementedError
  223. def _add_vars(self, vars):
  224. raise NotImplementedError
  225. def _add_positional(self, args):
  226. raise NotImplementedError
  227. class URL(URLResource):
  228. r"""
  229. >>> u = URL('http://localhost')
  230. >>> u
  231. <URL http://localhost>
  232. >>> u = u['view']
  233. >>> str(u)
  234. 'http://localhost/view'
  235. >>> u['//foo'].param(content='view').html
  236. '<a href="http://localhost/view/foo">view</a>'
  237. >>> u.param(confirm='Really?', content='goto').html
  238. '<a href="http://localhost/view" onclick="return confirm(\'Really?\')">goto</a>'
  239. >>> u(title='See "it"', content='goto').html
  240. '<a href="http://localhost/view?title=See+%22it%22">goto</a>'
  241. >>> u('another', var='fuggetaboutit', content='goto').html
  242. '<a href="http://localhost/view/another?var=fuggetaboutit">goto</a>'
  243. >>> u.attr(content='goto').html
  244. Traceback (most recent call last):
  245. ....
  246. ValueError: You must give a content param to <URL http://localhost/view attrs(content="goto")> generate anchor tags
  247. >>> str(u['foo=bar%20stuff'])
  248. 'http://localhost/view?foo=bar+stuff'
  249. """
  250. default_params = {'tag': 'a'}
  251. def __str__(self):
  252. return self.href
  253. def _get_content(self):
  254. if not self.params.get('content'):
  255. raise ValueError(
  256. "You must give a content param to %r generate anchor tags"
  257. % self)
  258. return self.params['content']
  259. def _add_vars(self, vars):
  260. url = self
  261. for name in ('confirm', 'content'):
  262. if name in vars:
  263. url = url.param(**{name: vars.pop(name)})
  264. if 'target' in vars:
  265. url = url.attr(target=vars.pop('target'))
  266. return url.var(**vars)
  267. def _add_positional(self, args):
  268. return self.addpath(*args)
  269. def _html_attrs(self):
  270. attrs = self.attrs.items()
  271. attrs.insert(0, ('href', self.href))
  272. if self.params.get('confirm'):
  273. attrs.append(('onclick', 'return confirm(%s)'
  274. % js_repr(self.params['confirm'])))
  275. return attrs
  276. def onclick_goto__get(self):
  277. return 'location.href=%s; return false' % js_repr(self.href)
  278. onclick_goto = property(onclick_goto__get)
  279. def button__get(self):
  280. return self.become(Button)
  281. button = property(button__get)
  282. def js_popup__get(self):
  283. return self.become(JSPopup)
  284. js_popup = property(js_popup__get)
  285. class Image(URLResource):
  286. r"""
  287. >>> i = Image('/images')
  288. >>> i = i / '/foo.png'
  289. >>> i.html
  290. '<img src="/images/foo.png" />'
  291. >>> str(i['alt=foo'])
  292. '<img src="/images/foo.png" alt="foo" />'
  293. >>> i.href
  294. '/images/foo.png'
  295. """
  296. default_params = {'tag': 'img'}
  297. def __str__(self):
  298. return self.html
  299. def _get_content(self):
  300. return None
  301. def _add_vars(self, vars):
  302. return self.attr(**vars)
  303. def _add_positional(self, args):
  304. return self.addpath(*args)
  305. def _html_attrs(self):
  306. attrs = self.attrs.items()
  307. attrs.insert(0, ('src', self.href))
  308. return attrs
  309. class Button(URLResource):
  310. r"""
  311. >>> u = URL('/')
  312. >>> u = u / 'delete'
  313. >>> b = u.button['confirm=Sure?'](id=5, content='del')
  314. >>> str(b)
  315. '<button onclick="if (confirm(\'Sure?\')) {location.href=\'/delete?id=5\'}; return false">del</button>'
  316. """
  317. default_params = {'tag': 'button'}
  318. def __str__(self):
  319. return self.html
  320. def _get_content(self):
  321. if self.params.get('content'):
  322. return self.params['content']
  323. if self.attrs.get('value'):
  324. return self.attrs['content']
  325. # @@: Error?
  326. return None
  327. def _add_vars(self, vars):
  328. button = self
  329. if 'confirm' in vars:
  330. button = button.param(confirm=vars.pop('confirm'))
  331. if 'content' in vars:
  332. button = button.param(content=vars.pop('content'))
  333. return button.var(**vars)
  334. def _add_positional(self, args):
  335. return self.addpath(*args)
  336. def _html_attrs(self):
  337. attrs = self.attrs.items()
  338. onclick = 'location.href=%s' % js_repr(self.href)
  339. if self.params.get('confirm'):
  340. onclick = 'if (confirm(%s)) {%s}' % (
  341. js_repr(self.params['confirm']), onclick)
  342. onclick += '; return false'
  343. attrs.insert(0, ('onclick', onclick))
  344. return attrs
  345. class JSPopup(URLResource):
  346. r"""
  347. >>> u = URL('/')
  348. >>> u = u / 'view'
  349. >>> j = u.js_popup(content='view')
  350. >>> j.html
  351. '<a href="/view" onclick="window.open(\'/view\', \'_blank\'); return false" target="_blank">view</a>'
  352. """
  353. default_params = {'tag': 'a', 'target': '_blank'}
  354. def _add_vars(self, vars):
  355. button = self
  356. for var in ('width', 'height', 'stripped', 'content'):
  357. if var in vars:
  358. button = button.param(**{var: vars.pop(var)})
  359. return button.var(**vars)
  360. def _window_args(self):
  361. p = self.params
  362. features = []
  363. if p.get('stripped'):
  364. p['location'] = p['status'] = p['toolbar'] = '0'
  365. for param in 'channelmode directories fullscreen location menubar resizable scrollbars status titlebar'.split():
  366. if param not in p:
  367. continue
  368. v = p[param]
  369. if v not in ('yes', 'no', '1', '0'):
  370. if v:
  371. v = '1'
  372. else:
  373. v = '0'
  374. features.append('%s=%s' % (param, v))
  375. for param in 'height left top width':
  376. if not p.get(param):
  377. continue
  378. features.append('%s=%s' % (param, p[param]))
  379. args = [self.href, p['target']]
  380. if features:
  381. args.append(','.join(features))
  382. return ', '.join(map(js_repr, args))
  383. def _html_attrs(self):
  384. attrs = self.attrs.items()
  385. onclick = ('window.open(%s); return false'
  386. % self._window_args())
  387. attrs.insert(0, ('target', self.params['target']))
  388. attrs.insert(0, ('onclick', onclick))
  389. attrs.insert(0, ('href', self.href))
  390. return attrs
  391. def _get_content(self):
  392. if not self.params.get('content'):
  393. raise ValueError(
  394. "You must give a content param to %r generate anchor tags"
  395. % self)
  396. return self.params['content']
  397. def _add_positional(self, args):
  398. return self.addpath(*args)
  399. if __name__ == '__main__':
  400. import doctest
  401. doctest.testmod()