cubicweb/uilib.py
changeset 11057 0b59724cb3f2
parent 10817 7b154e0fa194
child 11767 432f87a63057
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
       
     1 # -*- coding: utf-8 -*-
       
     2 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     3 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     4 #
       
     5 # This file is part of CubicWeb.
       
     6 #
       
     7 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     8 # terms of the GNU Lesser General Public License as published by the Free
       
     9 # Software Foundation, either version 2.1 of the License, or (at your option)
       
    10 # any later version.
       
    11 #
       
    12 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    13 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    14 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    15 # details.
       
    16 #
       
    17 # You should have received a copy of the GNU Lesser General Public License along
       
    18 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    19 """user interface libraries
       
    20 
       
    21 contains some functions designed to help implementation of cubicweb user
       
    22 interface.
       
    23 """
       
    24 
       
    25 __docformat__ = "restructuredtext en"
       
    26 
       
    27 import csv
       
    28 import re
       
    29 from io import StringIO
       
    30 
       
    31 from six import PY2, PY3, text_type, binary_type, string_types, integer_types
       
    32 
       
    33 from logilab.mtconverter import xml_escape, html_unescape
       
    34 from logilab.common.date import ustrftime
       
    35 from logilab.common.deprecation import deprecated
       
    36 
       
    37 from cubicweb import _
       
    38 from cubicweb.utils import js_dumps
       
    39 
       
    40 
       
    41 def rql_for_eid(eid):
       
    42     """return the rql query necessary to fetch entity with the given eid.  This
       
    43     function should only be used to generate link with rql inside, not to give
       
    44     to cursor.execute (in which case you won't benefit from rql cache).
       
    45 
       
    46     :Parameters:
       
    47       - `eid`: the eid of the entity we should search
       
    48     :rtype: str
       
    49     :return: the rql query
       
    50     """
       
    51     return 'Any X WHERE X eid %s' % eid
       
    52 
       
    53 def eid_param(name, eid):
       
    54     assert name is not None
       
    55     assert eid is not None
       
    56     return '%s:%s' % (name, eid)
       
    57 
       
    58 def print_bytes(value, req, props, displaytime=True):
       
    59     return u''
       
    60 
       
    61 def print_string(value, req, props, displaytime=True):
       
    62     # don't translate empty value if you don't want strange results
       
    63     if props is not None and value and props.get('internationalizable'):
       
    64         return req._(value)
       
    65     return value
       
    66 
       
    67 def print_int(value, req, props, displaytime=True):
       
    68     return text_type(value)
       
    69 
       
    70 def print_date(value, req, props, displaytime=True):
       
    71     return ustrftime(value, req.property_value('ui.date-format'))
       
    72 
       
    73 def print_time(value, req, props, displaytime=True):
       
    74     return ustrftime(value, req.property_value('ui.time-format'))
       
    75 
       
    76 def print_tztime(value, req, props, displaytime=True):
       
    77     return ustrftime(value, req.property_value('ui.time-format')) + u' UTC'
       
    78 
       
    79 def print_datetime(value, req, props, displaytime=True):
       
    80     if displaytime:
       
    81         return ustrftime(value, req.property_value('ui.datetime-format'))
       
    82     return ustrftime(value, req.property_value('ui.date-format'))
       
    83 
       
    84 def print_tzdatetime(value, req, props, displaytime=True):
       
    85     if displaytime:
       
    86         return ustrftime(value, req.property_value('ui.datetime-format')) + u' UTC'
       
    87     return ustrftime(value, req.property_value('ui.date-format'))
       
    88 
       
    89 _('%d years')
       
    90 _('%d months')
       
    91 _('%d weeks')
       
    92 _('%d days')
       
    93 _('%d hours')
       
    94 _('%d minutes')
       
    95 _('%d seconds')
       
    96 
       
    97 def print_timedelta(value, req, props, displaytime=True):
       
    98     if isinstance(value, integer_types):
       
    99         # `date - date`, unlike `datetime - datetime` gives an int
       
   100         # (number of days), not a timedelta
       
   101         # XXX should rql be fixed to return Int instead of Interval in
       
   102         #     that case? that would be probably the proper fix but we
       
   103         #     loose information on the way...
       
   104         value = timedelta(days=value)
       
   105     if value.days > 730 or value.days < -730: # 2 years
       
   106         return req._('%d years') % (value.days // 365)
       
   107     elif value.days > 60 or value.days < -60: # 2 months
       
   108         return req._('%d months') % (value.days // 30)
       
   109     elif value.days > 14 or value.days < -14: # 2 weeks
       
   110         return req._('%d weeks') % (value.days // 7)
       
   111     elif value.days > 2 or value.days < -2:
       
   112         return req._('%d days') % int(value.days)
       
   113     else:
       
   114         minus = 1 if value.days >= 0 else -1
       
   115         if value.seconds > 3600:
       
   116             return req._('%d hours') % (int(value.seconds // 3600) * minus)
       
   117         elif value.seconds >= 120:
       
   118             return req._('%d minutes') % (int(value.seconds // 60) * minus)
       
   119         else:
       
   120             return req._('%d seconds') % (int(value.seconds) * minus)
       
   121 
       
   122 def print_boolean(value, req, props, displaytime=True):
       
   123     if value:
       
   124         return req._('yes')
       
   125     return req._('no')
       
   126 
       
   127 def print_float(value, req, props, displaytime=True):
       
   128     return text_type(req.property_value('ui.float-format') % value) # XXX cast needed ?
       
   129 
       
   130 PRINTERS = {
       
   131     'Bytes': print_bytes,
       
   132     'String': print_string,
       
   133     'Int': print_int,
       
   134     'BigInt': print_int,
       
   135     'Date': print_date,
       
   136     'Time': print_time,
       
   137     'TZTime': print_tztime,
       
   138     'Datetime': print_datetime,
       
   139     'TZDatetime': print_tzdatetime,
       
   140     'Boolean': print_boolean,
       
   141     'Float': print_float,
       
   142     'Decimal': print_float,
       
   143     'Interval': print_timedelta,
       
   144     }
       
   145 
       
   146 @deprecated('[3.14] use req.printable_value(attrtype, value, ...)')
       
   147 def printable_value(req, attrtype, value, props=None, displaytime=True):
       
   148     return req.printable_value(attrtype, value, props, displaytime)
       
   149 
       
   150 def css_em_num_value(vreg, propname, default):
       
   151     """ we try to read an 'em' css property
       
   152     if we get another unit we're out of luck and resort to the given default
       
   153     (hence, it is strongly advised not to specify but ems for this css prop)
       
   154     """
       
   155     propvalue = vreg.config.uiprops[propname].lower().strip()
       
   156     if propvalue.endswith('em'):
       
   157         try:
       
   158             return float(propvalue[:-2])
       
   159         except Exception:
       
   160             vreg.warning('css property %s looks malformed (%r)',
       
   161                          propname, propvalue)
       
   162     else:
       
   163         vreg.warning('css property %s should use em (currently is %r)',
       
   164                      propname, propvalue)
       
   165     return default
       
   166 
       
   167 # text publishing #############################################################
       
   168 
       
   169 from cubicweb.ext.markdown import markdown_publish # pylint: disable=W0611
       
   170 
       
   171 try:
       
   172     from cubicweb.ext.rest import rest_publish # pylint: disable=W0611
       
   173 except ImportError:
       
   174     def rest_publish(entity, data):
       
   175         """default behaviour if docutils was not found"""
       
   176         return xml_escape(data)
       
   177 
       
   178 
       
   179 TAG_PROG = re.compile(r'</?.*?>', re.U)
       
   180 def remove_html_tags(text):
       
   181     """Removes HTML tags from text
       
   182 
       
   183     >>> remove_html_tags('<td>hi <a href="http://www.google.fr">world</a></td>')
       
   184     'hi world'
       
   185     >>>
       
   186     """
       
   187     return TAG_PROG.sub('', text)
       
   188 
       
   189 
       
   190 REF_PROG = re.compile(r"<ref\s+rql=([\'\"])([^\1]*?)\1\s*>([^<]*)</ref>", re.U)
       
   191 def _subst_rql(view, obj):
       
   192     delim, rql, descr = obj.groups()
       
   193     return u'<a href="%s">%s</a>' % (view._cw.build_url(rql=rql), descr)
       
   194 
       
   195 def html_publish(view, text):
       
   196     """replace <ref rql=''> links by <a href="...">"""
       
   197     if not text:
       
   198         return u''
       
   199     return REF_PROG.sub(lambda obj, view=view:_subst_rql(view, obj), text)
       
   200 
       
   201 # fallback implementation, nicer one defined below if lxml> 2.0 is available
       
   202 def safe_cut(text, length):
       
   203     """returns a string of length <length> based on <text>, removing any html
       
   204     tags from given text if cut is necessary."""
       
   205     if text is None:
       
   206         return u''
       
   207     noenttext = html_unescape(text)
       
   208     text_nohtml = remove_html_tags(noenttext)
       
   209     # try to keep html tags if text is short enough
       
   210     if len(text_nohtml) <= length:
       
   211         return text
       
   212     # else if un-tagged text is too long, cut it
       
   213     return xml_escape(text_nohtml[:length] + u'...')
       
   214 
       
   215 fallback_safe_cut = safe_cut
       
   216 
       
   217 REM_ROOT_HTML_TAGS = re.compile('</(body|html)>', re.U)
       
   218 
       
   219 from lxml import etree, html
       
   220 from lxml.html import clean, defs
       
   221 
       
   222 ALLOWED_TAGS = (defs.general_block_tags | defs.list_tags | defs.table_tags |
       
   223                 defs.phrase_tags | defs.font_style_tags |
       
   224                 set(('span', 'a', 'br', 'img', 'map', 'area', 'sub', 'sup', 'canvas'))
       
   225                 )
       
   226 
       
   227 CLEANER = clean.Cleaner(allow_tags=ALLOWED_TAGS, remove_unknown_tags=False,
       
   228                         style=True, safe_attrs_only=True,
       
   229                         add_nofollow=False,
       
   230                         )
       
   231 
       
   232 def soup2xhtml(data, encoding):
       
   233     """tidy html soup by allowing some element tags and return the result
       
   234     """
       
   235     # remove spurious </body> and </html> tags, then normalize line break
       
   236     # (see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1)
       
   237     data = REM_ROOT_HTML_TAGS.sub('', u'\n'.join(data.splitlines()))
       
   238     xmltree = etree.HTML(CLEANER.clean_html('<div>%s</div>' % data))
       
   239     # NOTE: lxml 2.0 does support encoding='unicode', but last time I (syt)
       
   240     # tried I got weird results (lxml 2.2.8)
       
   241     body = etree.tostring(xmltree[0], encoding=encoding)
       
   242     # remove <body> and </body> and decode to unicode
       
   243     snippet = body[6:-7].decode(encoding)
       
   244     # take care to bad xhtml (for instance starting with </div>) which
       
   245     # may mess with the <div> we added below. Only remove it if it's
       
   246     # still there...
       
   247     if snippet.startswith('<div>') and snippet.endswith('</div>'):
       
   248         snippet = snippet[5:-6]
       
   249     return snippet
       
   250 
       
   251     # lxml.Cleaner envelops text elements by internal logic (not accessible)
       
   252     # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
       
   253     # TODO drop attributes in elements
       
   254     # TODO add policy configuration (content only, embedded content, ...)
       
   255     # XXX this is buggy for "<p>text1</p><p>text2</p>"...
       
   256     # XXX drop these two snippets action and follow the lxml behaviour
       
   257     # XXX (tests need to be updated)
       
   258     # if snippet.startswith('<div>') and snippet.endswith('</div>'):
       
   259     #     snippet = snippet[5:-6]
       
   260     # if snippet.startswith('<p>') and snippet.endswith('</p>'):
       
   261     #     snippet = snippet[3:-4]
       
   262     return snippet.decode(encoding)
       
   263 
       
   264 if hasattr(etree.HTML('<div>test</div>'), 'iter'): # XXX still necessary?
       
   265     # pylint: disable=E0102
       
   266     def safe_cut(text, length):
       
   267         """returns an html document of length <length> based on <text>,
       
   268         and cut is necessary.
       
   269         """
       
   270         if text is None:
       
   271             return u''
       
   272         dom = etree.HTML(text)
       
   273         curlength = 0
       
   274         add_ellipsis = False
       
   275         for element in dom.iter():
       
   276             if curlength >= length:
       
   277                 parent = element.getparent()
       
   278                 parent.remove(element)
       
   279                 if curlength == length and (element.text or element.tail):
       
   280                     add_ellipsis = True
       
   281             else:
       
   282                 if element.text is not None:
       
   283                     element.text = cut(element.text, length - curlength)
       
   284                     curlength += len(element.text)
       
   285                 if element.tail is not None:
       
   286                     if curlength < length:
       
   287                         element.tail = cut(element.tail, length - curlength)
       
   288                         curlength += len(element.tail)
       
   289                     elif curlength == length:
       
   290                         element.tail = '...'
       
   291                     else:
       
   292                         element.tail = ''
       
   293         text = etree.tounicode(dom[0])[6:-7] # remove wrapping <body></body>
       
   294         if add_ellipsis:
       
   295             return text + u'...'
       
   296         return text
       
   297 
       
   298 def text_cut(text, nbwords=30, gotoperiod=True):
       
   299     """from the given plain text, return a text with at least <nbwords> words,
       
   300     trying to go to the end of the current sentence.
       
   301 
       
   302     :param nbwords: the minimum number of words required
       
   303     :param gotoperiod: specifies if the function should try to go to
       
   304                        the first period after the cut (i.e. finish
       
   305                        the sentence if possible)
       
   306 
       
   307     Note that spaces are normalized.
       
   308     """
       
   309     if text is None:
       
   310         return u''
       
   311     words = text.split()
       
   312     text = u' '.join(words) # normalize spaces
       
   313     textlength = minlength = len(' '.join(words[:nbwords]))
       
   314     if gotoperiod:
       
   315         textlength = text.find('.', minlength) + 1
       
   316         if textlength == 0: # no period found
       
   317             textlength = minlength
       
   318     return text[:textlength]
       
   319 
       
   320 def cut(text, length):
       
   321     """returns a string of a maximum length <length> based on <text>
       
   322     (approximatively, since if text has been  cut, '...' is added to the end of the string,
       
   323     resulting in a string of len <length> + 3)
       
   324     """
       
   325     if text is None:
       
   326         return u''
       
   327     if len(text) <= length:
       
   328         return text
       
   329     # else if un-tagged text is too long, cut it
       
   330     return text[:length] + u'...'
       
   331 
       
   332 
       
   333 
       
   334 # HTML generation helper functions ############################################
       
   335 
       
   336 class _JSId(object):
       
   337     def __init__(self, id, parent=None):
       
   338         self.id = id
       
   339         self.parent = parent
       
   340     def __unicode__(self):
       
   341         if self.parent:
       
   342             return u'%s.%s' % (self.parent, self.id)
       
   343         return text_type(self.id)
       
   344     __str__ = __unicode__ if PY3 else lambda self: self.__unicode__().encode('utf-8')
       
   345     def __getattr__(self, attr):
       
   346         return _JSId(attr, self)
       
   347     def __call__(self, *args):
       
   348         return _JSCallArgs(args, self)
       
   349 
       
   350 class _JSCallArgs(_JSId):
       
   351     def __init__(self, args, parent=None):
       
   352         assert isinstance(args, tuple)
       
   353         self.args = args
       
   354         self.parent = parent
       
   355     def __unicode__(self):
       
   356         args = []
       
   357         for arg in self.args:
       
   358             args.append(js_dumps(arg))
       
   359         if self.parent:
       
   360             return u'%s(%s)' % (self.parent, ','.join(args))
       
   361         return ','.join(args)
       
   362     __str__ = __unicode__ if PY3 else lambda self: self.__unicode__().encode('utf-8')
       
   363 
       
   364 class _JS(object):
       
   365     def __getattr__(self, attr):
       
   366         return _JSId(attr)
       
   367 
       
   368 js = _JS()
       
   369 js.__doc__ = """\
       
   370 magic object to return strings suitable to call some javascript function with
       
   371 the given arguments (which should be correctly typed).
       
   372 
       
   373 >>> str(js.pouet(1, "2"))
       
   374 'pouet(1,"2")'
       
   375 >>> str(js.cw.pouet(1, "2"))
       
   376 'cw.pouet(1,"2")'
       
   377 >>> str(js.cw.pouet(1, "2").pouet(None))
       
   378 'cw.pouet(1,"2").pouet(null)'
       
   379 >>> str(js.cw.pouet(1, JSString("$")).pouet(None))
       
   380 'cw.pouet(1,$).pouet(null)'
       
   381 >>> str(js.cw.pouet(1, {'callback': JSString("cw.cb")}).pouet(None))
       
   382 'cw.pouet(1,{callback: cw.cb}).pouet(null)'
       
   383 """
       
   384 
       
   385 def domid(string):
       
   386     """return a valid DOM id from a string (should also be usable in jQuery
       
   387     search expression...)
       
   388     """
       
   389     return string.replace('.', '_').replace('-', '_')
       
   390 
       
   391 HTML4_EMPTY_TAGS = frozenset(('base', 'meta', 'link', 'hr', 'br', 'param',
       
   392                               'img', 'area', 'input', 'col'))
       
   393 
       
   394 def sgml_attributes(attrs):
       
   395     return u' '.join(u'%s="%s"' % (attr, xml_escape(text_type(value)))
       
   396                      for attr, value in sorted(attrs.items())
       
   397                      if value is not None)
       
   398 
       
   399 def simple_sgml_tag(tag, content=None, escapecontent=True, **attrs):
       
   400     """generation of a simple sgml tag (eg without children tags) easier
       
   401 
       
   402     content and attri butes will be escaped
       
   403     """
       
   404     value = u'<%s' % tag
       
   405     if attrs:
       
   406         try:
       
   407             attrs['class'] = attrs.pop('klass')
       
   408         except KeyError:
       
   409             pass
       
   410         value += u' ' + sgml_attributes(attrs)
       
   411     if content:
       
   412         if escapecontent:
       
   413             content = xml_escape(text_type(content))
       
   414         value += u'>%s</%s>' % (content, tag)
       
   415     else:
       
   416         if tag in HTML4_EMPTY_TAGS:
       
   417             value += u' />'
       
   418         else:
       
   419             value += u'></%s>' % tag
       
   420     return value
       
   421 
       
   422 def tooltipize(text, tooltip, url=None):
       
   423     """make an HTML tooltip"""
       
   424     url = url or '#'
       
   425     return u'<a href="%s" title="%s">%s</a>' % (url, tooltip, text)
       
   426 
       
   427 def toggle_action(nodeid):
       
   428     """builds a HTML link that uses the js toggleVisibility function"""
       
   429     return u"javascript: toggleVisibility('%s')" % nodeid
       
   430 
       
   431 def toggle_link(nodeid, label):
       
   432     """builds a HTML link that uses the js toggleVisibility function"""
       
   433     return u'<a href="%s">%s</a>' % (toggle_action(nodeid), label)
       
   434 
       
   435 
       
   436 def ureport_as_html(layout):
       
   437     from logilab.common.ureports import HTMLWriter
       
   438     formater = HTMLWriter(True)
       
   439     stream = StringIO() #UStringIO() don't want unicode assertion
       
   440     formater.format(layout, stream)
       
   441     res = stream.getvalue()
       
   442     if isinstance(res, binary_type):
       
   443         res = res.decode('UTF8')
       
   444     return res
       
   445 
       
   446 # traceback formatting ########################################################
       
   447 
       
   448 import traceback
       
   449 
       
   450 def exc_message(ex, encoding):
       
   451     if PY3:
       
   452         excmsg = str(ex)
       
   453     else:
       
   454         try:
       
   455             excmsg = unicode(ex)
       
   456         except Exception:
       
   457             try:
       
   458                 excmsg = unicode(str(ex), encoding, 'replace')
       
   459             except Exception:
       
   460                 excmsg = unicode(repr(ex), encoding, 'replace')
       
   461     exctype = ex.__class__.__name__
       
   462     return u'%s: %s' % (exctype, excmsg)
       
   463 
       
   464 
       
   465 def rest_traceback(info, exception):
       
   466     """return a unicode ReST formated traceback"""
       
   467     res = [u'Traceback\n---------\n::\n']
       
   468     for stackentry in traceback.extract_tb(info[2]):
       
   469         res.append(u'\tFile %s, line %s, function %s' % tuple(stackentry[:3]))
       
   470         if stackentry[3]:
       
   471             data = xml_escape(stackentry[3])
       
   472             if PY2:
       
   473                 data = data.decode('utf-8', 'replace')
       
   474             res.append(u'\t  %s' % data)
       
   475     res.append(u'\n')
       
   476     try:
       
   477         res.append(u'\t Error: %s\n' % exception)
       
   478     except Exception:
       
   479         pass
       
   480     return u'\n'.join(res)
       
   481 
       
   482 
       
   483 def html_traceback(info, exception, title='',
       
   484                    encoding='ISO-8859-1', body=''):
       
   485     """ return an html formatted traceback from python exception infos.
       
   486     """
       
   487     tcbk = info[2]
       
   488     stacktb = traceback.extract_tb(tcbk)
       
   489     strings = []
       
   490     if body:
       
   491         strings.append(u'<div class="error_body">')
       
   492         # FIXME
       
   493         strings.append(body)
       
   494         strings.append(u'</div>')
       
   495     if title:
       
   496         strings.append(u'<h1 class="error">%s</h1>'% xml_escape(title))
       
   497     try:
       
   498         strings.append(u'<p class="error">%s</p>' % xml_escape(str(exception)).replace("\n","<br />"))
       
   499     except UnicodeError:
       
   500         pass
       
   501     strings.append(u'<div class="error_traceback">')
       
   502     for index, stackentry in enumerate(stacktb):
       
   503         strings.append(u'<b>File</b> <b class="file">%s</b>, <b>line</b> '
       
   504                        u'<b class="line">%s</b>, <b>function</b> '
       
   505                        u'<b class="function">%s</b>:<br/>'%(
       
   506             xml_escape(stackentry[0]), stackentry[1], xml_escape(stackentry[2])))
       
   507         if stackentry[3]:
       
   508             string = xml_escape(stackentry[3])
       
   509             if PY2:
       
   510                 string = string.decode('utf-8', 'replace')
       
   511             strings.append(u'&#160;&#160;%s<br/>\n' % (string))
       
   512         # add locals info for each entry
       
   513         try:
       
   514             local_context = tcbk.tb_frame.f_locals
       
   515             html_info = []
       
   516             chars = 0
       
   517             for name, value in local_context.items():
       
   518                 value = xml_escape(repr(value))
       
   519                 info = u'<span class="name">%s</span>=%s, ' % (name, value)
       
   520                 line_length = len(name) + len(value)
       
   521                 chars += line_length
       
   522                 # 150 is the result of *years* of research ;-) (CSS might be helpful here)
       
   523                 if chars > 150:
       
   524                     info = u'<br/>' + info
       
   525                     chars = line_length
       
   526                 html_info.append(info)
       
   527             boxid = 'ctxlevel%d' % index
       
   528             strings.append(u'[%s]' % toggle_link(boxid, '+'))
       
   529             strings.append(u'<div id="%s" class="pycontext hidden">%s</div>' %
       
   530                            (boxid, ''.join(html_info)))
       
   531             tcbk = tcbk.tb_next
       
   532         except Exception:
       
   533             pass # doesn't really matter if we have no context info
       
   534     strings.append(u'</div>')
       
   535     return '\n'.join(strings)
       
   536 
       
   537 # csv files / unicode support #################################################
       
   538 
       
   539 class UnicodeCSVWriter:
       
   540     """proxies calls to csv.writer.writerow to be able to deal with unicode
       
   541 
       
   542     Under Python 3, this code no longer encodes anything."""
       
   543 
       
   544     def __init__(self, wfunc, encoding, **kwargs):
       
   545         self.writer = csv.writer(self, **kwargs)
       
   546         self.wfunc = wfunc
       
   547         self.encoding = encoding
       
   548 
       
   549     def write(self, data):
       
   550         self.wfunc(data)
       
   551 
       
   552     def writerow(self, row):
       
   553         if PY3:
       
   554             self.writer.writerow(row)
       
   555             return
       
   556         csvrow = []
       
   557         for elt in row:
       
   558             if isinstance(elt, text_type):
       
   559                 csvrow.append(elt.encode(self.encoding))
       
   560             else:
       
   561                 csvrow.append(str(elt))
       
   562         self.writer.writerow(csvrow)
       
   563 
       
   564     def writerows(self, rows):
       
   565         for row in rows:
       
   566             self.writerow(row)
       
   567 
       
   568 
       
   569 # some decorators #############################################################
       
   570 
       
   571 class limitsize(object):
       
   572     def __init__(self, maxsize):
       
   573         self.maxsize = maxsize
       
   574 
       
   575     def __call__(self, function):
       
   576         def newfunc(*args, **kwargs):
       
   577             ret = function(*args, **kwargs)
       
   578             if isinstance(ret, string_types):
       
   579                 return ret[:self.maxsize]
       
   580             return ret
       
   581         return newfunc
       
   582 
       
   583 
       
   584 def htmlescape(function):
       
   585     def newfunc(*args, **kwargs):
       
   586         ret = function(*args, **kwargs)
       
   587         assert isinstance(ret, string_types)
       
   588         return xml_escape(ret)
       
   589     return newfunc