patch series (9428951.patch)

download
# HG changeset patch
# User Rémi Cardona <remi.cardona@logilab.fr>
# Date 1450871155 -3600
#      Wed Dec 23 12:45:55 2015 +0100
# Node ID dcbb8f460cc75b0dd040bcd178cf18ecd6bf9ee1
# Parent  079b32f4cd0dd27f094c95c6e3ca971b11f70827
[web/views] Use UStringIO instead of lists

u''.join(list_of_html_fragments) is anti-pattern to build HTML. Using
UStringIO is much better, and allows using cwtags in derived classes
(among other benefits).

Tests are modified because of '\n' removals.

Related to #9428951.

diff --git a/web/formfields.py b/web/formfields.py
--- a/web/formfields.py
+++ b/web/formfields.py
@@ -79,7 +79,7 @@ from yams.constraints import (SizeConstr
                               FormatConstraint)
 
 from cubicweb import Binary, tags, uilib
-from cubicweb.utils import support_args
+from cubicweb.utils import support_args, UStringIO
 from cubicweb.web import INTERNAL_FIELD_VALUE, ProcessFormError, eid_param, \
      formwidgets as fw
 from cubicweb.web.views import uicfg
@@ -716,29 +716,31 @@ class FileField(StringField):
         return super(FileField, self).typed_value(form, load_bytes)
 
     def render(self, form, renderer):
-        wdgs = [self.get_widget(form).render(form, self, renderer)]
+        data = UStringIO()
+        w = data.write
+        w(self.get_widget(form).render(form, self, renderer))
         if self.format_field or self.encoding_field:
             divid = '%s-advanced' % self.input_name(form)
-            wdgs.append(u'<a href="%s" title="%s"><img src="%s" alt="%s"/></a>' %
+            w(u'<a href="%s" title="%s"><img src="%s" alt="%s"/></a>' %
                         (xml_escape(uilib.toggle_action(divid)),
                          form._cw._('show advanced fields'),
                          xml_escape(form._cw.data_url('puce_down.png')),
                          form._cw._('show advanced fields')))
-            wdgs.append(u'<div id="%s" class="hidden">' % divid)
+            w(u'<div id="%s" class="hidden">' % divid)
             if self.name_field:
-                wdgs.append(self.render_subfield(form, self.name_field, renderer))
+                w(self.render_subfield(form, self.name_field, renderer))
             if self.format_field:
-                wdgs.append(self.render_subfield(form, self.format_field, renderer))
+                w(self.render_subfield(form, self.format_field, renderer))
             if self.encoding_field:
-                wdgs.append(self.render_subfield(form, self.encoding_field, renderer))
-            wdgs.append(u'</div>')
+                w(self.render_subfield(form, self.encoding_field, renderer))
+            w(u'</div>')
         if not self.required and self.typed_value(form):
             # trick to be able to delete an uploaded file
-            wdgs.append(u'<br/>')
-            wdgs.append(tags.input(name=self.input_name(form, u'__detach'),
+            w(u'<br/>')
+            w(tags.input(name=self.input_name(form, u'__detach'),
                                    type=u'checkbox'))
-            wdgs.append(form._cw._('detach attached file'))
-        return u'\n'.join(wdgs)
+            w(form._cw._('detach attached file'))
+        return data.getvalue()
 
     def render_subfield(self, form, field, renderer):
         return (renderer.render_label(form, field)
@@ -793,7 +795,9 @@ class EditableFileField(FileField):
         'text/plain', 'text/html', 'text/rest', 'text/markdown')
 
     def render(self, form, renderer):
-        wdgs = [super(EditableFileField, self).render(form, renderer)]
+        html = UStringIO()
+        w = html.write
+        w(super(EditableFileField, self).render(form, renderer))
         if self.format(form) in self.editable_formats:
             data = self.typed_value(form, load_bytes=True)
             if data:
@@ -813,10 +817,10 @@ class EditableFileField(FileField):
                         msg = form._cw._(
                             'You can either submit a new file using the browse button above'
                             ', or edit file content online with the widget below.')
-                    wdgs.append(u'<p><b>%s</b></p>' % msg)
-                    wdgs.append(fw.TextArea(setdomid=False).render(form, self, renderer))
+                    w(u'<p><b>%s</b></p>' % msg)
+                    w(fw.TextArea(setdomid=False).render(form, self, renderer))
                     # XXX restore form context?
-        return '\n'.join(wdgs)
+        return html.getvalue()
 
     def _process_form_value(self, form):
         value = form._cw.form.get(self.input_name(form))
diff --git a/web/test/unittest_form.py b/web/test/unittest_form.py
--- a/web/test/unittest_form.py
+++ b/web/test/unittest_form.py
@@ -224,15 +224,18 @@ if HAS_TAL else '') +
                                      data=Binary(b'new widgets system'))
             form = FFForm(req, redirect_path='perdu.com', entity=file)
             self.assertMultiLineEqual(self._render_entity_field(req, 'data', form),
-                              '''<input id="data-subject:%(eid)s" name="data-subject:%(eid)s" tabindex="1" type="file" value="" />
-<a href="javascript: toggleVisibility(&#39;data-subject:%(eid)s-advanced&#39;)" title="show advanced fields"><img src="http://testing.fr/cubicweb/data/puce_down.png" alt="show advanced fields"/></a>
-<div id="data-subject:%(eid)s-advanced" class="hidden">
-<label for="data_format-subject:%(eid)s">data_format</label><input id="data_format-subject:%(eid)s" maxlength="50" name="data_format-subject:%(eid)s" size="45" tabindex="2" type="text" value="text/plain" /><br/>
-<label for="data_encoding-subject:%(eid)s">data_encoding</label><input id="data_encoding-subject:%(eid)s" maxlength="20" name="data_encoding-subject:%(eid)s" size="20" tabindex="3" type="text" value="UTF-8" /><br/>
-</div>
-<br/>
-<input name="data-subject__detach:%(eid)s" type="checkbox" />
-detach attached file''' % {'eid': file.eid})
+                '<input id="data-subject:%(eid)s" name="data-subject:%(eid)s" tabindex="1" type="file" value="" />'
+                '<a href="javascript: toggleVisibility(&#39;data-subject:%(eid)s-advanced&#39;)" title="show advanced fields">'
+                '<img src="http://testing.fr/cubicweb/data/puce_down.png" alt="show advanced fields"/></a>'
+                '<div id="data-subject:%(eid)s-advanced" class="hidden">'
+                '<label for="data_format-subject:%(eid)s">data_format</label>'
+                '<input id="data_format-subject:%(eid)s" maxlength="50" name="data_format-subject:%(eid)s" size="45" tabindex="2" type="text" value="text/plain" /><br/>'
+                '<label for="data_encoding-subject:%(eid)s">data_encoding</label>'
+                '<input id="data_encoding-subject:%(eid)s" maxlength="20" name="data_encoding-subject:%(eid)s" size="20" tabindex="3" type="text" value="UTF-8" /><br/>'
+                '</div>'
+                '<br/>'
+                '<input name="data-subject__detach:%(eid)s" type="checkbox" />'
+                'detach attached file' % {'eid': file.eid})
 
 
     def test_editablefilefield(self):
@@ -248,17 +251,29 @@ detach attached file''' % {'eid': file.e
                                      data=Binary(b'new widgets system'))
             form = EFFForm(req, redirect_path='perdu.com', entity=file)
             self.assertMultiLineEqual(self._render_entity_field(req, 'data', form),
-                              '''<input id="data-subject:%(eid)s" name="data-subject:%(eid)s" tabindex="1" type="file" value="" />
-<a href="javascript: toggleVisibility(&#39;data-subject:%(eid)s-advanced&#39;)" title="show advanced fields"><img src="http://testing.fr/cubicweb/data/puce_down.png" alt="show advanced fields"/></a>
-<div id="data-subject:%(eid)s-advanced" class="hidden">
-<label for="data_format-subject:%(eid)s">data_format</label><input id="data_format-subject:%(eid)s" maxlength="50" name="data_format-subject:%(eid)s" size="45" tabindex="2" type="text" value="text/plain" /><br/>
-<label for="data_encoding-subject:%(eid)s">data_encoding</label><input id="data_encoding-subject:%(eid)s" maxlength="20" name="data_encoding-subject:%(eid)s" size="20" tabindex="3" type="text" value="UTF-8" /><br/>
-</div>
-<br/>
-<input name="data-subject__detach:%(eid)s" type="checkbox" />
-detach attached file
-<p><b>You can either submit a new file using the browse button above, or choose to remove already uploaded file by checking the "detach attached file" check-box, or edit file content online with the widget below.</b></p>
-<textarea cols="80" name="data-subject:%(eid)s" onkeyup="autogrow(this)" rows="3" tabindex="4">new widgets system</textarea>''' % {'eid': file.eid})
+                '<input id="data-subject:%(eid)s" name="data-subject:%(eid)s" tabindex="1" type="file" value="" />'
+                '<a href="javascript: toggleVisibility(&#39;data-subject:%(eid)s-advanced&#39;)" title="show advanced fields">'
+                '<img src="http://testing.fr/cubicweb/data/puce_down.png" alt="show advanced fields"/></a>'
+                '<div id="data-subject:%(eid)s-advanced" class="hidden">'
+                '<label for="data_format-subject:%(eid)s">data_format</label>'
+                '<input id="data_format-subject:%(eid)s" maxlength="50" '
+                'name="data_format-subject:%(eid)s" size="45" tabindex="2" '
+                'type="text" value="text/plain" /><br/>'
+                '<label for="data_encoding-subject:%(eid)s">data_encoding</label>'
+                '<input id="data_encoding-subject:%(eid)s" maxlength="20" '
+                'name="data_encoding-subject:%(eid)s" size="20" tabindex="3" '
+                'type="text" value="UTF-8" /><br/>'
+                '</div>'
+                '<br/>'
+                '<input name="data-subject__detach:%(eid)s" type="checkbox" />'
+                'detach attached file'
+                '<p><b>You can either submit a new file using the browse button '
+                'above, or choose to remove already uploaded file by checking '
+                'the "detach attached file" check-box, or edit file content '
+                'online with the widget below.</b></p>'
+                '<textarea cols="80" name="data-subject:%(eid)s" '
+                'onkeyup="autogrow(this)" rows="3" tabindex="4">'
+                'new widgets system</textarea>' % {'eid': file.eid})
 
 
     def test_passwordfield(self):
diff --git a/web/views/cwproperties.py b/web/views/cwproperties.py
--- a/web/views/cwproperties.py
+++ b/web/views/cwproperties.py
@@ -27,6 +27,7 @@ from logilab.common.decorators import ca
 from cubicweb import UnknownProperty
 from cubicweb.predicates import (one_line_rset, none_rset, is_instance,
                                  match_user_groups, logged_user_in_rset)
+from cubicweb.utils import UStringIO
 from cubicweb.view import StartupView
 from cubicweb.web import stdmsgs
 from cubicweb.web.form import FormViewMixIn
@@ -191,9 +192,9 @@ class SystemCWPropertiesForm(FormViewMix
             self.form_row(form, key, splitlabel)
         renderer = self._cw.vreg['formrenderers'].select('cwproperties', self._cw,
                                                      display_progress_div=False)
-        data = []
-        form.render(w=data.append, renderer=renderer)
-        return u'\n'.join(data)
+        data = UStringIO()
+        form.render(w=data.write, renderer=renderer)
+        return data.getvalue()
 
     def form_row(self, form, key, splitlabel):
         entity = self.entity_for_key(key)
diff --git a/web/views/formrenderers.py b/web/views/formrenderers.py
--- a/web/views/formrenderers.py
+++ b/web/views/formrenderers.py
@@ -45,7 +45,7 @@ from logilab.common.registry import yes
 from cubicweb import tags, uilib
 from cubicweb.appobject import AppObject
 from cubicweb.predicates import is_instance
-from cubicweb.utils import json_dumps, support_args
+from cubicweb.utils import json_dumps, support_args, UStringIO
 from cubicweb.web import eid_param, formwidgets as fwdgs
 
 
@@ -111,17 +111,17 @@ class FormRenderer(AppObject):
     def render(self, w, form, values):
         self._set_options(values)
         form.add_media()
-        data = []
-        _w = data.append
+        data = UStringIO()
+        _w = data.write
         _w(self.open_form(form, values))
         self.render_content(_w, form, values)
         _w(self.close_form(form, values))
         errormsg = self.error_message(form)
         if errormsg:
-            data.insert(0, errormsg)
+            w(errormsg)
         # NOTE: we call unicode because `tag` objects may be found within data
         #       e.g. from the cwtags library
-        w(''.join(text_type(x) for x in data))
+        w(data.getvalue())
 
     def render_content(self, w, form, values):
         if self.display_progress_div:
# HG changeset patch
# User Rémi Cardona <remi.cardona@logilab.fr>
# Date 1448464891 -3600
#      Wed Nov 25 16:21:31 2015 +0100
# Node ID c9a35d952efbfe081d5dd3014fb1744078967b52
# Parent  dcbb8f460cc75b0dd040bcd178cf18ecd6bf9ee1
[utils] Allow writing UStringIOs to UStringIO

This allows preserving the stack traces in the final output.

The major change is that UStringIO.write no longer builds the tracehtml
output, it only records 2 things:

* the actual data that was written
* a textual stack trace

The _cwtracehtml output is now completely generated in HTMLStream.

The stand-alone call to head.getvalue() is needed because this method
not only returns the <head> tag string, it actually generates it. So,
for the tracing mechanism to work, we need to call it, then the content
of the UStringIO is manually inspected to generate the _cwtracehtml
output (job done by _ustringio_with_stacks()).

Related to #9428951.

diff --git a/test/unittest_utils.py b/test/unittest_utils.py
--- a/test/unittest_utils.py
+++ b/test/unittest_utils.py
@@ -20,6 +20,7 @@
 import re
 import decimal
 import datetime
+from itertools import product
 
 from six.moves import range
 
@@ -118,6 +119,58 @@ class UStringIOTC(TestCase):
     def test_boolean_value(self):
         self.assertTrue(UStringIO())
 
+    def test_nested_write(self):
+        # make sure that .getvalue() always returns the original data that
+        # was written to the UStringIO, regardless of tracewrites
+        for trace_main, trace_sub in product((True, False), repeat=2):
+            main = UStringIO(tracewrites=trace_main)
+            sub = UStringIO(tracewrites=trace_sub)
+            main.write(u'ab')
+            sub.write(u'1234')
+            main.write(sub)
+            main.write(u'cd')
+            self.assertEqual(main.getvalue(), u'ab1234cd')
+
+    def test_tracewrites(self):
+        stream = UStringIO(tracewrites=True)
+        self.assertEqual(len(stream.stacks), 0)
+        stream.write(u'abcd')
+        self.assertEqual(stream.getvalue(), u'abcd')
+        self.assertEqual(len(stream.stacks), 1)
+
+    def test_main_notrace(self):
+        for trace_sub in (True, False):
+            main = UStringIO(tracewrites=False)
+            sub = UStringIO(tracewrites=trace_sub)
+            main.write(u'ab')
+            sub.write(u'1234')
+            main.write(sub)
+            main.write(u'cd')
+            self.assertEqual(main.getvalue(), u'ab1234cd')
+            self.assertIsNone(main.stacks)
+
+    def test_main_trace_sub_trace(self):
+        main = UStringIO(tracewrites=True)
+        sub = UStringIO(tracewrites=True)
+        main.write(u'ab')
+        sub.write(u'12')
+        sub.write(u'34')
+        main.write(sub)
+        main.write(u'cd')
+        self.assertEqual(main.getvalue(), u'ab1234cd')
+        self.assertEqual(len(main.stacks), 4)
+
+    def test_main_trace_sub_notrace(self):
+        main = UStringIO(tracewrites=True)
+        sub = UStringIO(tracewrites=False)
+        main.write(u'ab')
+        sub.write(u'12')
+        sub.write(u'34')
+        main.write(sub)
+        main.write(u'cd')
+        self.assertEqual(main.getvalue(), u'ab1234cd')
+        self.assertEqual(len(main.stacks), 3)
+
 
 class RepeatListTC(TestCase):
 
diff --git a/utils.py b/utils.py
--- a/utils.py
+++ b/utils.py
@@ -34,8 +34,10 @@ from uuid import uuid4
 from warnings import warn
 from threading import Lock
 from logging import getLogger
+from traceback import format_stack
 
 from six import text_type
+from six.moves import zip
 from six.moves.urllib.parse import urlparse
 
 from logilab.mtconverter import xml_escape
@@ -191,6 +193,7 @@ class UStringIO(list):
 
     def __init__(self, tracewrites=False, *args, **kwargs):
         self.tracewrites = tracewrites
+        self.stacks = [] if tracewrites else None
         super(UStringIO, self).__init__(*args, **kwargs)
 
     def __bool__(self):
@@ -199,15 +202,19 @@ class UStringIO(list):
     __nonzero__ = __bool__
 
     def write(self, value):
+        if isinstance(value, UStringIO):
+            nested = value
+            if self.tracewrites and nested.tracewrites:
+                self.extend(nested)
+                self.stacks.extend(nested.stacks)
+                return
+            value = nested.getvalue()
+
         assert isinstance(value, text_type), u"unicode required not %s : %s"\
                                      % (type(value).__name__, repr(value))
         if self.tracewrites:
-            from traceback import format_stack
-            stack = format_stack(None)[:-1]
-            escaped_stack = xml_escape(json_dumps(u'\n'.join(stack)))
-            escaped_html = xml_escape(value).replace('\n', '<br/>\n')
-            tpl = u'<span onclick="alert(%s)">%s</span>'
-            value = tpl % (escaped_stack, escaped_html)
+            stack = u'\n'.join(format_stack(None)[:-1])
+            self.stacks.append(stack)
         self.append(value)
 
     def getvalue(self):
@@ -401,15 +408,15 @@ class HTMLHead(UStringIO):
                 w(self.script_opening)
                 w(u'\n\n'.join(self.post_inlined_scripts))
                 w(self.script_closing)
-        # at the start of this function, the parent UStringIO may already have
-        # data in it, so we can't w(u'<head>\n') at the top. Instead, we create
-        # a temporary UStringIO to get the same debugging output formatting
-        # if debugging is enabled.
-        headtag = UStringIO(tracewrites=self.tracewrites)
         if not skiphead:
-            headtag.write(u'<head>\n')
+            # the <head> tag is put at the end and _then_ brought back at the
+            # beginning to generate a proper stack if tracewrites is enabled
+            w(u'<head>\n')
+            self.insert(0, self.pop())
+            if self.tracewrites:
+                self.stacks.insert(0, self.stacks.pop())
             w(u'</head>\n')
-        return headtag.getvalue() + super(HTMLHead, self).getvalue()
+        return super(HTMLHead, self).getvalue()
 
 
 class HTMLStream(object):
@@ -463,6 +470,16 @@ class HTMLStream(object):
             return '<html xmlns:cubicweb="http://www.cubicweb.org" %s>' % attrs
         return '<html xmlns:cubicweb="http://www.cubicweb.org">'
 
+    @staticmethod
+    def _ustringio_with_stacks(stream):
+        ret = u''
+        tpl = u'<span onclick="alert(%s)">%s</span>'
+        for data, stack in zip(stream, stream.stacks):
+            escaped_stack = xml_escape(json_dumps(stack))
+            escaped_data = xml_escape(data).replace('\n', '<br/>\n')
+            ret += tpl % (escaped_stack, escaped_data)
+        return ret
+
     def getvalue(self):
         """writes HTML headers, closes </head> tag and writes HTML body"""
         if self.tracehtml:
@@ -476,13 +493,14 @@ class HTMLStream(object):
                               u'  text-decoration: underline;',
                               u'}'))
             style = u'<style type="text/css">\n%s\n</style>\n' % css
+            self.head.getvalue()
             return (u'<!DOCTYPE html>\n'
                     + u'<html>\n<head>\n%s\n</head>\n' % style
                     + u'<body>\n'
                     + u'<span>' + xml_escape(self.doctype) + u'</span><br/>'
                     + u'<span>' + xml_escape(self.htmltag) + u'</span><br/>'
-                    + self.head.getvalue()
-                    + self.body.getvalue()
+                    + self._ustringio_with_stacks(self.head)
+                    + self._ustringio_with_stacks(self.body)
                     + u'<span>' + xml_escape(u'</html>') + u'</span>'
                     + u'</body>\n</html>')
         return u'%s\n%s\n%s\n%s\n</html>' % (self.doctype,
# HG changeset patch
# User Rémi Cardona <remi.cardona@logilab.fr>
# Date 1450881630 -3600
#      Wed Dec 23 15:40:30 2015 +0100
# Node ID ca760257826c19f99900511873d5978bcdd37a8d
# Parent  c9a35d952efbfe081d5dd3014fb1744078967b52
[web] Enable tracing on nested UStringIOs (closes #9428951)

Actually use the new UStringIO capability.  This commit does changes the
return type of SystemCWPropertiesForm.form (from a unicode to a
UStringIO).  Hopefully, this API break is minor enough.

diff --git a/web/views/basetemplates.py b/web/views/basetemplates.py
--- a/web/views/basetemplates.py
+++ b/web/views/basetemplates.py
@@ -155,14 +155,14 @@ class TheMainTemplate(MainTemplate):
             'etypenavigation', self._cw, rset=self.cw_rset)
         if etypefilter and etypefilter.cw_propval('visible'):
             etypefilter.render(w=w)
-        nav_html = UStringIO()
+        nav_html = UStringIO(tracewrites=self._cw.tracehtml)
         if view and not view.handle_pagination:
             view.paginate(w=nav_html.write)
-        w(nav_html.getvalue())
+        w(nav_html)
         w(u'<div id="contentmain">\n')
         view.render(w=w)
         w(u'</div>\n') # close id=contentmain
-        w(nav_html.getvalue())
+        w(nav_html)
         w(u'</div>\n') # closes id=pageContent
         self.template_footer(view)
 
diff --git a/web/views/cwproperties.py b/web/views/cwproperties.py
--- a/web/views/cwproperties.py
+++ b/web/views/cwproperties.py
@@ -192,9 +192,9 @@ class SystemCWPropertiesForm(FormViewMix
             self.form_row(form, key, splitlabel)
         renderer = self._cw.vreg['formrenderers'].select('cwproperties', self._cw,
                                                      display_progress_div=False)
-        data = UStringIO()
+        data = UStringIO(tracewrites=form._cw.tracehtml)
         form.render(w=data.write, renderer=renderer)
-        return data.getvalue()
+        return data
 
     def form_row(self, form, key, splitlabel):
         entity = self.entity_for_key(key)
diff --git a/web/views/formrenderers.py b/web/views/formrenderers.py
--- a/web/views/formrenderers.py
+++ b/web/views/formrenderers.py
@@ -111,7 +111,7 @@ class FormRenderer(AppObject):
     def render(self, w, form, values):
         self._set_options(values)
         form.add_media()
-        data = UStringIO()
+        data = UStringIO(tracewrites=form._cw.tracehtml)
         _w = data.write
         _w(self.open_form(form, values))
         self.render_content(_w, form, values)
@@ -121,7 +121,7 @@ class FormRenderer(AppObject):
             w(errormsg)
         # NOTE: we call unicode because `tag` objects may be found within data
         #       e.g. from the cwtags library
-        w(data.getvalue())
+        w(data)
 
     def render_content(self, w, form, values):
         if self.display_progress_div:
diff --git a/web/views/tableview.py b/web/views/tableview.py
--- a/web/views/tableview.py
+++ b/web/views/tableview.py
@@ -239,10 +239,10 @@ class TableLayout(component.Component):
             w(u'<div id="%s">' % divid)
         else:
             assert not (actions or paginate)
-        nav_html = UStringIO()
+        nav_html = UStringIO(tracewrites=self._cw.tracehtml)
         if paginate:
             view.paginate(w=nav_html.write, show_all_option=self.show_all_option)
-        w(nav_html.getvalue())
+        w(nav_html)
         if actions and self.display_actions == 'top':
             self.render_actions(w, actions)
         colrenderers = view.build_column_renderers()
@@ -254,7 +254,7 @@ class TableLayout(component.Component):
         w(u'</table>')
         if actions and self.display_actions == 'bottom':
             self.render_actions(w, actions)
-        w(nav_html.getvalue())
+        w(nav_html)
         if divid is not None:
             w(u'</div>')
 
# HG changeset patch
# User Rémi Cardona <remi.cardona@logilab.fr>
# Date 1450867845 -3600
#      Wed Dec 23 11:50:45 2015 +0100
# Node ID 6efb8e92ff7c019ed244fa4468c0034d71de019b
# Parent  ca760257826c19f99900511873d5978bcdd37a8d
[web] Enable tracing on nested UStringIOs (part 2)

This actually changes the API of some methods (the core is in
FieldWidget._render, but this propages to Field.render and thus may
impact FormRenderers) as they now return UStringIOs instead of unicode
objects, but hopefully this is for the best.

Related to #9428951.

diff --git a/web/formfields.py b/web/formfields.py
--- a/web/formfields.py
+++ b/web/formfields.py
@@ -716,7 +716,7 @@ class FileField(StringField):
         return super(FileField, self).typed_value(form, load_bytes)
 
     def render(self, form, renderer):
-        data = UStringIO()
+        data = UStringIO(tracewrites=form._cw.tracehtml)
         w = data.write
         w(self.get_widget(form).render(form, self, renderer))
         if self.format_field or self.encoding_field:
@@ -740,7 +740,7 @@ class FileField(StringField):
             w(tags.input(name=self.input_name(form, u'__detach'),
                                    type=u'checkbox'))
             w(form._cw._('detach attached file'))
-        return data.getvalue()
+        return data
 
     def render_subfield(self, form, field, renderer):
         return (renderer.render_label(form, field)
@@ -795,7 +795,7 @@ class EditableFileField(FileField):
         'text/plain', 'text/html', 'text/rest', 'text/markdown')
 
     def render(self, form, renderer):
-        html = UStringIO()
+        html = UStringIO(tracewrites=form._cw.tracehtml)
         w = html.write
         w(super(EditableFileField, self).render(form, renderer))
         if self.format(form) in self.editable_formats:
@@ -820,7 +820,7 @@ class EditableFileField(FileField):
                     w(u'<p><b>%s</b></p>' % msg)
                     w(fw.TextArea(setdomid=False).render(form, self, renderer))
                     # XXX restore form context?
-        return html.getvalue()
+        return html
 
     def _process_form_value(self, form):
         value = form._cw.form.get(self.input_name(form))
diff --git a/web/formwidgets.py b/web/formwidgets.py
--- a/web/formwidgets.py
+++ b/web/formwidgets.py
@@ -196,6 +196,8 @@ class FieldWidget(object):
 
     def _render(self, form, field, renderer):
         """This is the method you have to implement in concrete widget classes.
+
+        This method may return a Unicode string or UStringIO object.
         """
         raise NotImplementedError()
 
diff --git a/web/test/unittest_form.py b/web/test/unittest_form.py
--- a/web/test/unittest_form.py
+++ b/web/test/unittest_form.py
@@ -26,6 +26,7 @@ from six import text_type
 from logilab.common.testlib import unittest_main
 
 from cubicweb import Binary, ValidationError
+from cubicweb.utils import UStringIO
 from cubicweb.mttransforms import HAS_TAL
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.web.formfields import (IntField, StringField, RichTextField,
@@ -110,9 +111,9 @@ class EntityFieldsFormTC(CubicWebTC):
             req.form['__linkto'] = 'in_group:%s:subject' % geid
             form = self.vreg['forms'].select('edition', req, entity=e)
             form.content_type = 'text/html'
-            data = []
-            form.render(w=data.append)
-            pageinfo = self._check_html(u'\n'.join(data), form, template=None)
+            data = UStringIO()
+            form.render(w=data.write)
+            pageinfo = self._check_html(data.getvalue(), form, template=None)
             inputs = pageinfo.find_tag('select', False)
             ok = False
             for selectnode in pageinfo.matching_nodes('select', name='from_in_group-subject:A'):
@@ -141,9 +142,9 @@ class EntityFieldsFormTC(CubicWebTC):
             form = self.vreg['forms'].select('edition', req, entity=e)
             ts_after = time.time()
 
-            data = []
-            form.render(action='edit', w=data.append)
-            html_form = html.fromstring(''.join(data)).forms[0]
+            data = UStringIO()
+            form.render(action='edit', w=data.write)
+            html_form = html.fromstring(data.getvalue()).forms[0]
             fields = dict(html_form.form_values())
             self.assertIn(expected_field_name, fields)
             ts = float(fields[expected_field_name])
@@ -178,7 +179,9 @@ class EntityFieldsFormTC(CubicWebTC):
     def _render_entity_field(self, req, name, form):
         form.build_context({})
         renderer = FormRenderer(req)
-        return form.field_by_name(name, 'subject').render(form, renderer)
+        html = UStringIO()
+        html.write(form.field_by_name(name, 'subject').render(form, renderer))
+        return html.getvalue()
 
     def _test_richtextfield(self, req, expected):
         class RTFForm(EntityFieldsForm):