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