[debug] add in each html snippet from where it has been generated in the code

Closes #17219704

authorLaurent Peuch <cortex@worlddomination.be>
changeset8caa109dfe94
branchdefault
phasepublic
hiddenno
parent revision#44147dab9d27 Use python3 shebang for scripts
child revision#fda5efaf6937 Add missing dbmako/mako files in python sdist, #d2accd9e379b [pkginfo] Remove unused parameter
files modified by this revision
cubicweb/pyramid/pyramidctl.py
cubicweb/view.py
cubicweb/web/test/unittest_views_debug_html.py
# HG changeset patch
# User Laurent Peuch <cortex@worlddomination.be>
# Date 1563977482 -7200
# Wed Jul 24 16:11:22 2019 +0200
# Node ID 8caa109dfe945592a34904bb56971cc57d1c6084
# Parent 44147dab9d27907208119b3f198f58c601f30cd5
[debug] add in each html snippet from where it has been generated in the code

Closes #17219704

diff --git a/cubicweb/pyramid/pyramidctl.py b/cubicweb/pyramid/pyramidctl.py
@@ -35,10 +35,11 @@
1  from logilab.common.configuration import merge_options
2 
3  from cubicweb.cwctl import CWCTL, InstanceCommand, init_cmdline_log_threshold
4  from cubicweb.pyramid import wsgi_application_from_cwconfig
5  from cubicweb.pyramid.config import get_random_secret_key
6 +from cubicweb.view import inject_html_generating_call_on_w
7  from cubicweb.server import serverctl
8  from cubicweb.web.webctl import WebCreateHandler
9  from cubicweb.toolsutils import fill_templated_file
10 
11  import waitress
@@ -269,10 +270,15 @@
12                        "'pip install pyramid_debugtoolbar'.\nYou can find more information on the "
13                        "official documentation: "
14                        "https://docs.pylonsproject.org/projects/pyramid_debugtoolbar/en/latest/")
15                  sys.exit(1)
16 
17 +        if self['debug']:
18 +            # this is for injecting those into generated html:
19 +            # > cubicweb-generated-by="module.Class" cubicweb-from-source="/path/to/file.py:42"
20 +            inject_html_generating_call_on_w()
21 +
22          app = wsgi_application_from_cwconfig(
23              cwconfig, profile=self['profile'],
24              profile_output=self['profile-output'],
25              profile_dump_every=self['profile-dump-every'],
26              debugtoolbar=self['toolbar']
diff --git a/cubicweb/view.py b/cubicweb/view.py
@@ -18,13 +18,15 @@
27  """abstract views and templates classes for CubicWeb web client"""
28 
29 
30  from cubicweb import _
31 
32 +import re
33  from io import BytesIO
34  from warnings import warn
35  from functools import partial
36 +from inspect import getframeinfo, stack
37 
38  from logilab.common.registry import yes
39  from logilab.mtconverter import xml_escape
40 
41  from rql import nodes
@@ -44,10 +46,15 @@
42  TRANSITIONAL_DOCTYPE = TRANSITIONAL_DOCTYPE_NOEXT # bw compat
43 
44  STRICT_DOCTYPE_NOEXT = u'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'
45  STRICT_DOCTYPE = STRICT_DOCTYPE_NOEXT # bw compat
46 
47 +
48 +def inject_html_generating_call_on_w():
49 +    View.debug_html_rendering = True
50 +
51 +
52  # base view object ############################################################
53 
54  class View(AppObject):
55      """This class is an abstraction of a view class, used as a base class for
56      every renderable object such as views, templates and other user interface
@@ -84,31 +91,64 @@
57      # content_type = 'application/xhtml+xml' # text/xhtml'
58      binary = False
59      add_to_breadcrumbs = True
60      category = 'view'
61      paginable = True
62 +    debug_html_rendering = False
63 
64      def __init__(self, req=None, rset=None, **kwargs):
65          super(View, self).__init__(req, rset=rset, **kwargs)
66 -        self.w = None
67 +        self._w = None
68 
69      @property
70      def content_type(self):
71          return self._cw.html_content_type()
72 
73 +    def w(self, text):
74 +        if self._w is None:
75 +            raise Exception('Error: call to %s.w before it has been set' % self)
76 +
77 +        if self.debug_html_rendering:
78 +            caller = getframeinfo(stack()[1][0])
79 +
80 +            if isinstance(text, str) and re.match(r"^ *<[a-zA-Z0-9]+( .*>|>)", text):
81 +                to_add = 'cubicweb-generated-by="%s.%s" cubicweb-from-source="%s:%s"' % (
82 +                    self.__module__, self.__class__.__name__,
83 +                    caller.filename, caller.lineno,
84 +                )
85 +
86 +                before_space, beginning_of_html = text.split("<", 1)
87 +
88 +                # when it's a tag without attribues like "<b>"
89 +                if re.match(r"^ *<[a-zA-Z0-9]+>", text):
90 +                    tag_name, rest = beginning_of_html.split(">", 1)
91 +                    rest = ">" + rest
92 +                else:
93 +                    tag_name, rest = beginning_of_html.split(" ", 1)
94 +                    rest = " " + rest
95 +
96 +                text = "%(before_space)s<%(tag_name)s %(to_add)s%(rest)s" % {
97 +                    "before_space": before_space,
98 +                    "tag_name": tag_name,
99 +                    "to_add": to_add,
100 +                    "rest": rest,
101 +                }
102 +
103 +        return self._w(text)
104 +
105      def set_stream(self, w=None):
106 -        if self.w is not None:
107 +        if self._w is not None:
108              return
109          if w is None:
110              if self.binary:
111                  self._stream = stream = BytesIO()
112              else:
113                  self._stream = stream = UStringIO()
114              w = stream.write
115          else:
116              stream = None
117 -        self.w = w
118 +        self._w = w
119          return stream
120 
121      # main view interface #####################################################
122 
123      def render(self, w=None, **context):
@@ -236,11 +276,11 @@
124      # view utilities ##########################################################
125 
126      def wview(self, __vid, rset=None, __fallback_vid=None, **kwargs):
127          """shortcut to self.view method automatically passing self.w as argument
128          """
129 -        self._cw.view(__vid, rset, __fallback_vid, w=self.w, **kwargs)
130 +        self._cw.view(__vid, rset, __fallback_vid, w=self._w, **kwargs)
131 
132      def whead(self, data):
133          self._cw.html_headers.write(data)
134 
135      def wdata(self, data):
@@ -464,21 +504,21 @@
136      """
137 
138      doctype = '<!DOCTYPE html>'
139 
140      def set_stream(self, w=None):
141 -        if self.w is not None:
142 +        if self._w is not None:
143              return
144          if w is None:
145              if self.binary:
146                  self._stream = stream = BytesIO()
147              else:
148                  self._stream = stream = HTMLStream(self._cw)
149              w = stream.write
150          else:
151              stream = None
152 -        self.w = w
153 +        self._w = w
154          return stream
155 
156      def write_doctype(self, xmldecl=True):
157          assert isinstance(self._stream, HTMLStream)
158          self._stream.doctype = self.doctype
diff --git a/cubicweb/web/test/unittest_views_debug_html.py b/cubicweb/web/test/unittest_views_debug_html.py
@@ -0,0 +1,49 @@
159 +# copyright 2019 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
160 +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
161 +#
162 +# This file is part of CubicWeb.
163 +#
164 +# CubicWeb is free software: you can redistribute it and/or modify it under the
165 +# terms of the GNU Lesser General Public License as published by the Free
166 +# Software Foundation, either version 2.1 of the License, or (at your option)
167 +# any later version.
168 +#
169 +# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
170 +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
171 +# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
172 +# details.
173 +#
174 +# You should have received a copy of the GNU Lesser General Public License along
175 +# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
176 +
177 +import inspect
178 +
179 +from cubicweb.view import View
180 +from cubicweb.devtools.testlib import CubicWebTC
181 +
182 +
183 +class DebugHtmlRenderingTC(CubicWebTC):
184 +    def setUp(self):
185 +        super().setUp()
186 +        View.debug_html_rendering = True
187 +
188 +    def tearDown(self):
189 +        super().tearDown()
190 +        View.debug_html_rendering = False
191 +
192 +    def test_debug_html_rendering_inject_tags(self):
193 +        with self.admin_access.web_request() as req:
194 +            view = self.vreg['views'].select("index", req)
195 +            view_class = view.__class__
196 +            page = view.render()
197 +
198 +            self.assertIn('cubicweb-generated-by="%s.%s"' % (view_class.__module__,
199 +                                                             view_class.__name__),
200 +                          page)
201 +            source_file = inspect.getsourcefile(view_class)
202 +            self.assertIn('cubicweb-from-source="%s' % (source_file), page)
203 +
204 +
205 +if __name__ == '__main__':
206 +    from logilab.common.testlib import unittest_main
207 +    unittest_main()