url.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  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. from six.moves.urllib.parse import quote, unquote, urlencode
  7. import cgi
  8. from paste import request
  9. import six
  10. # Imported lazily from FormEncode:
  11. variabledecode = None
  12. __all__ = ["URL", "Image"]
  13. def html_quote(v):
  14. if v is None:
  15. return ''
  16. return cgi.escape(str(v), 1)
  17. def url_quote(v):
  18. if v is None:
  19. return ''
  20. return quote(str(v))
  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({unquote(name): 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 + list(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. if six.PY3:
  165. __truediv__ = addpath
  166. else:
  167. __div__ = addpath
  168. def become(self, OtherClass):
  169. return OtherClass(self.url, vars=self.vars,
  170. attrs=self.attrs,
  171. params=self.original_params)
  172. def href__get(self):
  173. s = self.url
  174. if self.vars:
  175. s += '?'
  176. vars = []
  177. for name, val in self.vars:
  178. if isinstance(val, (list, tuple)):
  179. val = [v for v in val if v is not None]
  180. elif val is None:
  181. continue
  182. vars.append((name, val))
  183. s += urlencode(vars, True)
  184. return s
  185. href = property(href__get)
  186. def __repr__(self):
  187. base = '<%s %s' % (self.__class__.__name__,
  188. self.href or "''")
  189. if self.attrs:
  190. base += ' attrs(%s)' % (
  191. ' '.join(['%s="%s"' % (html_quote(n), html_quote(v))
  192. for n, v in self.attrs.items()]))
  193. if self.original_params:
  194. base += ' params(%s)' % (
  195. ', '.join(['%s=%r' % (n, v)
  196. for n, v in self.attrs.items()]))
  197. return base + '>'
  198. def html__get(self):
  199. if not self.params.get('tag'):
  200. raise ValueError(
  201. "You cannot get the HTML of %r until you set the "
  202. "'tag' param'" % self)
  203. content = self._get_content()
  204. tag = '<%s' % self.params.get('tag')
  205. attrs = ' '.join([
  206. '%s="%s"' % (html_quote(n), html_quote(v))
  207. for n, v in self._html_attrs()])
  208. if attrs:
  209. tag += ' ' + attrs
  210. tag += self._html_extra()
  211. if content is None:
  212. return tag + ' />'
  213. else:
  214. return '%s>%s</%s>' % (tag, content, self.params.get('tag'))
  215. html = property(html__get)
  216. def _html_attrs(self):
  217. return self.attrs.items()
  218. def _html_extra(self):
  219. return ''
  220. def _get_content(self):
  221. """
  222. Return the content for a tag (for self.html); return None
  223. for an empty tag (like ``<img />``)
  224. """
  225. raise NotImplementedError
  226. def _add_vars(self, vars):
  227. raise NotImplementedError
  228. def _add_positional(self, args):
  229. raise NotImplementedError
  230. class URL(URLResource):
  231. r"""
  232. >>> u = URL('http://localhost')
  233. >>> u
  234. <URL http://localhost>
  235. >>> u = u['view']
  236. >>> str(u)
  237. 'http://localhost/view'
  238. >>> u['//foo'].param(content='view').html
  239. '<a href="http://localhost/view/foo">view</a>'
  240. >>> u.param(confirm='Really?', content='goto').html
  241. '<a href="http://localhost/view" onclick="return confirm(\'Really?\')">goto</a>'
  242. >>> u(title='See "it"', content='goto').html
  243. '<a href="http://localhost/view?title=See+%22it%22">goto</a>'
  244. >>> u('another', var='fuggetaboutit', content='goto').html
  245. '<a href="http://localhost/view/another?var=fuggetaboutit">goto</a>'
  246. >>> u.attr(content='goto').html
  247. Traceback (most recent call last):
  248. ....
  249. ValueError: You must give a content param to <URL http://localhost/view attrs(content="goto")> generate anchor tags
  250. >>> str(u['foo=bar%20stuff'])
  251. 'http://localhost/view?foo=bar+stuff'
  252. """
  253. default_params = {'tag': 'a'}
  254. def __str__(self):
  255. return self.href
  256. def _get_content(self):
  257. if not self.params.get('content'):
  258. raise ValueError(
  259. "You must give a content param to %r generate anchor tags"
  260. % self)
  261. return self.params['content']
  262. def _add_vars(self, vars):
  263. url = self
  264. for name in ('confirm', 'content'):
  265. if name in vars:
  266. url = url.param(**{name: vars.pop(name)})
  267. if 'target' in vars:
  268. url = url.attr(target=vars.pop('target'))
  269. return url.var(**vars)
  270. def _add_positional(self, args):
  271. return self.addpath(*args)
  272. def _html_attrs(self):
  273. attrs = list(self.attrs.items())
  274. attrs.insert(0, ('href', self.href))
  275. if self.params.get('confirm'):
  276. attrs.append(('onclick', 'return confirm(%s)'
  277. % js_repr(self.params['confirm'])))
  278. return attrs
  279. def onclick_goto__get(self):
  280. return 'location.href=%s; return false' % js_repr(self.href)
  281. onclick_goto = property(onclick_goto__get)
  282. def button__get(self):
  283. return self.become(Button)
  284. button = property(button__get)
  285. def js_popup__get(self):
  286. return self.become(JSPopup)
  287. js_popup = property(js_popup__get)
  288. class Image(URLResource):
  289. r"""
  290. >>> i = Image('/images')
  291. >>> i = i / '/foo.png'
  292. >>> i.html
  293. '<img src="/images/foo.png" />'
  294. >>> str(i['alt=foo'])
  295. '<img src="/images/foo.png" alt="foo" />'
  296. >>> i.href
  297. '/images/foo.png'
  298. """
  299. default_params = {'tag': 'img'}
  300. def __str__(self):
  301. return self.html
  302. def _get_content(self):
  303. return None
  304. def _add_vars(self, vars):
  305. return self.attr(**vars)
  306. def _add_positional(self, args):
  307. return self.addpath(*args)
  308. def _html_attrs(self):
  309. attrs = list(self.attrs.items())
  310. attrs.insert(0, ('src', self.href))
  311. return attrs
  312. class Button(URLResource):
  313. r"""
  314. >>> u = URL('/')
  315. >>> u = u / 'delete'
  316. >>> b = u.button['confirm=Sure?'](id=5, content='del')
  317. >>> str(b)
  318. '<button onclick="if (confirm(\'Sure?\')) {location.href=\'/delete?id=5\'}; return false">del</button>'
  319. """
  320. default_params = {'tag': 'button'}
  321. def __str__(self):
  322. return self.html
  323. def _get_content(self):
  324. if self.params.get('content'):
  325. return self.params['content']
  326. if self.attrs.get('value'):
  327. return self.attrs['content']
  328. # @@: Error?
  329. return None
  330. def _add_vars(self, vars):
  331. button = self
  332. if 'confirm' in vars:
  333. button = button.param(confirm=vars.pop('confirm'))
  334. if 'content' in vars:
  335. button = button.param(content=vars.pop('content'))
  336. return button.var(**vars)
  337. def _add_positional(self, args):
  338. return self.addpath(*args)
  339. def _html_attrs(self):
  340. attrs = list(self.attrs.items())
  341. onclick = 'location.href=%s' % js_repr(self.href)
  342. if self.params.get('confirm'):
  343. onclick = 'if (confirm(%s)) {%s}' % (
  344. js_repr(self.params['confirm']), onclick)
  345. onclick += '; return false'
  346. attrs.insert(0, ('onclick', onclick))
  347. return attrs
  348. class JSPopup(URLResource):
  349. r"""
  350. >>> u = URL('/')
  351. >>> u = u / 'view'
  352. >>> j = u.js_popup(content='view')
  353. >>> j.html
  354. '<a href="/view" onclick="window.open(\'/view\', \'_blank\'); return false" target="_blank">view</a>'
  355. """
  356. default_params = {'tag': 'a', 'target': '_blank'}
  357. def _add_vars(self, vars):
  358. button = self
  359. for var in ('width', 'height', 'stripped', 'content'):
  360. if var in vars:
  361. button = button.param(**{var: vars.pop(var)})
  362. return button.var(**vars)
  363. def _window_args(self):
  364. p = self.params
  365. features = []
  366. if p.get('stripped'):
  367. p['location'] = p['status'] = p['toolbar'] = '0'
  368. for param in 'channelmode directories fullscreen location menubar resizable scrollbars status titlebar'.split():
  369. if param not in p:
  370. continue
  371. v = p[param]
  372. if v not in ('yes', 'no', '1', '0'):
  373. if v:
  374. v = '1'
  375. else:
  376. v = '0'
  377. features.append('%s=%s' % (param, v))
  378. for param in 'height left top width':
  379. if not p.get(param):
  380. continue
  381. features.append('%s=%s' % (param, p[param]))
  382. args = [self.href, p['target']]
  383. if features:
  384. args.append(','.join(features))
  385. return ', '.join(map(js_repr, args))
  386. def _html_attrs(self):
  387. attrs = list(self.attrs.items())
  388. onclick = ('window.open(%s); return false'
  389. % self._window_args())
  390. attrs.insert(0, ('target', self.params['target']))
  391. attrs.insert(0, ('onclick', onclick))
  392. attrs.insert(0, ('href', self.href))
  393. return attrs
  394. def _get_content(self):
  395. if not self.params.get('content'):
  396. raise ValueError(
  397. "You must give a content param to %r generate anchor tags"
  398. % self)
  399. return self.params['content']
  400. def _add_positional(self, args):
  401. return self.addpath(*args)
  402. if __name__ == '__main__':
  403. import doctest
  404. doctest.testmod()