# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php """ This module implements a class for handling URLs. """ from six.moves.urllib.parse import parse_qsl, quote, unquote, urlencode import cgi from paste import request import six # Imported lazily from FormEncode: variabledecode = None __all__ = ["URL", "Image"] def html_quote(v): if v is None: return '' return cgi.escape(str(v), 1) def url_quote(v): if v is None: return '' return quote(str(v)) def js_repr(v): if v is None: return 'null' elif v is False: return 'false' elif v is True: return 'true' elif isinstance(v, list): return '[%s]' % ', '.join(map(js_repr, v)) elif isinstance(v, dict): return '{%s}' % ', '.join( ['%s: %s' % (js_repr(key), js_repr(value)) for key, value in v]) elif isinstance(v, str): return repr(v) elif isinstance(v, unicode): # @@: how do you do Unicode literals in Javascript? return repr(v.encode('UTF-8')) elif isinstance(v, (float, int)): return repr(v) elif isinstance(v, long): return repr(v).lstrip('L') elif hasattr(v, '__js_repr__'): return v.__js_repr__() else: raise ValueError( "I don't know how to turn %r into a Javascript representation" % v) class URLResource(object): """ This is an abstract superclass for different kinds of URLs """ default_params = {} def __init__(self, url, vars=None, attrs=None, params=None): self.url = url or '/' self.vars = vars or [] self.attrs = attrs or {} self.params = self.default_params.copy() self.original_params = params or {} if params: self.params.update(params) #@classmethod def from_environ(cls, environ, with_query_string=True, with_path_info=True, script_name=None, path_info=None, querystring=None): url = request.construct_url( environ, with_query_string=False, with_path_info=with_path_info, script_name=script_name, path_info=path_info) if with_query_string: if querystring is None: vars = request.parse_querystring(environ) else: vars = parse_qsl( querystring, keep_blank_values=True, strict_parsing=False) else: vars = None v = cls(url, vars=vars) return v from_environ = classmethod(from_environ) def __call__(self, *args, **kw): res = self._add_positional(args) res = res._add_vars(kw) return res def __getitem__(self, item): if '=' in item: name, value = item.split('=', 1) return self._add_vars({unquote(name): unquote(value)}) return self._add_positional((item,)) def attr(self, **kw): for key in kw.keys(): if key.endswith('_'): kw[key[:-1]] = kw[key] del kw[key] new_attrs = self.attrs.copy() new_attrs.update(kw) return self.__class__(self.url, vars=self.vars, attrs=new_attrs, params=self.original_params) def param(self, **kw): new_params = self.original_params.copy() new_params.update(kw) return self.__class__(self.url, vars=self.vars, attrs=self.attrs, params=new_params) def coerce_vars(self, vars): global variabledecode need_variable_encode = False for key, value in vars.items(): if isinstance(value, dict): need_variable_encode = True if key.endswith('_'): vars[key[:-1]] = vars[key] del vars[key] if need_variable_encode: if variabledecode is None: from formencode import variabledecode vars = variabledecode.variable_encode(vars) return vars def var(self, **kw): kw = self.coerce_vars(kw) new_vars = self.vars + list(kw.items()) return self.__class__(self.url, vars=new_vars, attrs=self.attrs, params=self.original_params) def setvar(self, **kw): """ Like ``.var(...)``, except overwrites keys, where .var simply extends the keys. Setting a variable to None here will effectively delete it. """ kw = self.coerce_vars(kw) new_vars = [] for name, values in self.vars: if name in kw: continue new_vars.append((name, values)) new_vars.extend(kw.items()) return self.__class__(self.url, vars=new_vars, attrs=self.attrs, params=self.original_params) def setvars(self, **kw): """ Creates a copy of this URL, but with all the variables set/reset (like .setvar(), except clears past variables at the same time) """ return self.__class__(self.url, vars=kw.items(), attrs=self.attrs, params=self.original_params) def addpath(self, *paths): u = self for path in paths: path = str(path).lstrip('/') new_url = u.url if not new_url.endswith('/'): new_url += '/' u = u.__class__(new_url+path, vars=u.vars, attrs=u.attrs, params=u.original_params) return u if six.PY3: __truediv__ = addpath else: __div__ = addpath def become(self, OtherClass): return OtherClass(self.url, vars=self.vars, attrs=self.attrs, params=self.original_params) def href__get(self): s = self.url if self.vars: s += '?' vars = [] for name, val in self.vars: if isinstance(val, (list, tuple)): val = [v for v in val if v is not None] elif val is None: continue vars.append((name, val)) s += urlencode(vars, True) return s href = property(href__get) def __repr__(self): base = '<%s %s' % (self.__class__.__name__, self.href or "''") if self.attrs: base += ' attrs(%s)' % ( ' '.join(['%s="%s"' % (html_quote(n), html_quote(v)) for n, v in self.attrs.items()])) if self.original_params: base += ' params(%s)' % ( ', '.join(['%s=%r' % (n, v) for n, v in self.attrs.items()])) return base + '>' def html__get(self): if not self.params.get('tag'): raise ValueError( "You cannot get the HTML of %r until you set the " "'tag' param'" % self) content = self._get_content() tag = '<%s' % self.params.get('tag') attrs = ' '.join([ '%s="%s"' % (html_quote(n), html_quote(v)) for n, v in self._html_attrs()]) if attrs: tag += ' ' + attrs tag += self._html_extra() if content is None: return tag + ' />' else: return '%s>%s' % (tag, content, self.params.get('tag')) html = property(html__get) def _html_attrs(self): return self.attrs.items() def _html_extra(self): return '' def _get_content(self): """ Return the content for a tag (for self.html); return None for an empty tag (like ````) """ raise NotImplementedError def _add_vars(self, vars): raise NotImplementedError def _add_positional(self, args): raise NotImplementedError class URL(URLResource): r""" >>> u = URL('http://localhost') >>> u >>> u = u['view'] >>> str(u) 'http://localhost/view' >>> u['//foo'].param(content='view').html 'view' >>> u.param(confirm='Really?', content='goto').html 'goto' >>> u(title='See "it"', content='goto').html 'goto' >>> u('another', var='fuggetaboutit', content='goto').html 'goto' >>> u.attr(content='goto').html Traceback (most recent call last): .... ValueError: You must give a content param to generate anchor tags >>> str(u['foo=bar%20stuff']) 'http://localhost/view?foo=bar+stuff' """ default_params = {'tag': 'a'} def __str__(self): return self.href def _get_content(self): if not self.params.get('content'): raise ValueError( "You must give a content param to %r generate anchor tags" % self) return self.params['content'] def _add_vars(self, vars): url = self for name in ('confirm', 'content'): if name in vars: url = url.param(**{name: vars.pop(name)}) if 'target' in vars: url = url.attr(target=vars.pop('target')) return url.var(**vars) def _add_positional(self, args): return self.addpath(*args) def _html_attrs(self): attrs = list(self.attrs.items()) attrs.insert(0, ('href', self.href)) if self.params.get('confirm'): attrs.append(('onclick', 'return confirm(%s)' % js_repr(self.params['confirm']))) return attrs def onclick_goto__get(self): return 'location.href=%s; return false' % js_repr(self.href) onclick_goto = property(onclick_goto__get) def button__get(self): return self.become(Button) button = property(button__get) def js_popup__get(self): return self.become(JSPopup) js_popup = property(js_popup__get) class Image(URLResource): r""" >>> i = Image('/images') >>> i = i / '/foo.png' >>> i.html '' >>> str(i['alt=foo']) 'foo' >>> i.href '/images/foo.png' """ default_params = {'tag': 'img'} def __str__(self): return self.html def _get_content(self): return None def _add_vars(self, vars): return self.attr(**vars) def _add_positional(self, args): return self.addpath(*args) def _html_attrs(self): attrs = list(self.attrs.items()) attrs.insert(0, ('src', self.href)) return attrs class Button(URLResource): r""" >>> u = URL('/') >>> u = u / 'delete' >>> b = u.button['confirm=Sure?'](id=5, content='del') >>> str(b) '' """ default_params = {'tag': 'button'} def __str__(self): return self.html def _get_content(self): if self.params.get('content'): return self.params['content'] if self.attrs.get('value'): return self.attrs['content'] # @@: Error? return None def _add_vars(self, vars): button = self if 'confirm' in vars: button = button.param(confirm=vars.pop('confirm')) if 'content' in vars: button = button.param(content=vars.pop('content')) return button.var(**vars) def _add_positional(self, args): return self.addpath(*args) def _html_attrs(self): attrs = list(self.attrs.items()) onclick = 'location.href=%s' % js_repr(self.href) if self.params.get('confirm'): onclick = 'if (confirm(%s)) {%s}' % ( js_repr(self.params['confirm']), onclick) onclick += '; return false' attrs.insert(0, ('onclick', onclick)) return attrs class JSPopup(URLResource): r""" >>> u = URL('/') >>> u = u / 'view' >>> j = u.js_popup(content='view') >>> j.html 'view' """ default_params = {'tag': 'a', 'target': '_blank'} def _add_vars(self, vars): button = self for var in ('width', 'height', 'stripped', 'content'): if var in vars: button = button.param(**{var: vars.pop(var)}) return button.var(**vars) def _window_args(self): p = self.params features = [] if p.get('stripped'): p['location'] = p['status'] = p['toolbar'] = '0' for param in 'channelmode directories fullscreen location menubar resizable scrollbars status titlebar'.split(): if param not in p: continue v = p[param] if v not in ('yes', 'no', '1', '0'): if v: v = '1' else: v = '0' features.append('%s=%s' % (param, v)) for param in 'height left top width': if not p.get(param): continue features.append('%s=%s' % (param, p[param])) args = [self.href, p['target']] if features: args.append(','.join(features)) return ', '.join(map(js_repr, args)) def _html_attrs(self): attrs = list(self.attrs.items()) onclick = ('window.open(%s); return false' % self._window_args()) attrs.insert(0, ('target', self.params['target'])) attrs.insert(0, ('onclick', onclick)) attrs.insert(0, ('href', self.href)) return attrs def _get_content(self): if not self.params.get('content'): raise ValueError( "You must give a content param to %r generate anchor tags" % self) return self.params['content'] def _add_positional(self, args): return self.addpath(*args) if __name__ == '__main__': import doctest doctest.testmod()