|
|
|
|
| from __future__ import absolute_import
|
|
|
| import os
|
| import os.path
|
| import re
|
| import codecs
|
| import textwrap
|
| from datetime import datetime
|
| from functools import partial
|
| from collections import defaultdict
|
| from xml.sax.saxutils import escape as html_escape
|
| try:
|
| from StringIO import StringIO
|
| except ImportError:
|
| from io import StringIO
|
|
|
| from . import Version
|
| from .Code import CCodeWriter
|
| from .. import Utils
|
|
|
|
|
| class AnnotationCCodeWriter(CCodeWriter):
|
|
|
|
|
| COMPLETE_CODE_TITLE = "Complete cythonized code"
|
|
|
| def __init__(self, create_from=None, buffer=None, copy_formatting=True, show_entire_c_code=False, source_desc=None):
|
| CCodeWriter.__init__(self, create_from, buffer, copy_formatting=copy_formatting)
|
| self.show_entire_c_code = show_entire_c_code
|
| if create_from is None:
|
| self.annotation_buffer = StringIO()
|
| self.last_annotated_pos = None
|
|
|
| self.annotations = defaultdict(partial(defaultdict, list))
|
|
|
| self.code = defaultdict(partial(defaultdict, str))
|
|
|
| self.scopes = defaultdict(partial(defaultdict, set))
|
| else:
|
|
|
| self.annotation_buffer = create_from.annotation_buffer
|
| self.annotations = create_from.annotations
|
| self.code = create_from.code
|
| self.scopes = create_from.scopes
|
| self.last_annotated_pos = create_from.last_annotated_pos
|
|
|
| def create_new(self, create_from, buffer, copy_formatting):
|
| return AnnotationCCodeWriter(create_from, buffer, copy_formatting)
|
|
|
| def _write_to_buffer(self, s):
|
| self.buffer.write(s)
|
| self.annotation_buffer.write(s)
|
|
|
| def mark_pos(self, pos, trace=True):
|
| if pos is not None:
|
| CCodeWriter.mark_pos(self, pos, trace)
|
| if self.funcstate and self.funcstate.scope:
|
|
|
| self.scopes[pos[0].filename][pos[1]].add(self.funcstate.scope)
|
| if self.last_annotated_pos:
|
| source_desc, line, _ = self.last_annotated_pos
|
| pos_code = self.code[source_desc.filename]
|
| pos_code[line] += self.annotation_buffer.getvalue()
|
| self.annotation_buffer = StringIO()
|
| self.last_annotated_pos = pos
|
|
|
| def annotate(self, pos, item):
|
| self.annotations[pos[0].filename][pos[1]].append((pos[2], item))
|
|
|
| def _css(self):
|
| """css template will later allow to choose a colormap"""
|
| css = [self._css_template]
|
| for i in range(255):
|
| color = u"FFFF%02x" % int(255.0 // (1.0 + i/10.0))
|
| css.append('.cython.score-%d {background-color: #%s;}' % (i, color))
|
| try:
|
| from pygments.formatters import HtmlFormatter
|
| except ImportError:
|
| pass
|
| else:
|
| css.append(HtmlFormatter().get_style_defs('.cython'))
|
| return '\n'.join(css)
|
|
|
| _css_template = textwrap.dedent("""
|
| body.cython { font-family: courier; font-size: 12; }
|
|
|
| .cython.tag { }
|
| .cython.line { color: #000000; margin: 0em }
|
| .cython.code { font-size: 9; color: #444444; display: none; margin: 0px 0px 0px 8px; border-left: 8px none; }
|
|
|
| .cython.line .run { background-color: #B0FFB0; }
|
| .cython.line .mis { background-color: #FFB0B0; }
|
| .cython.code.run { border-left: 8px solid #B0FFB0; }
|
| .cython.code.mis { border-left: 8px solid #FFB0B0; }
|
|
|
| .cython.code .py_c_api { color: red; }
|
| .cython.code .py_macro_api { color: #FF7000; }
|
| .cython.code .pyx_c_api { color: #FF3000; }
|
| .cython.code .pyx_macro_api { color: #FF7000; }
|
| .cython.code .refnanny { color: #FFA000; }
|
| .cython.code .trace { color: #FFA000; }
|
| .cython.code .error_goto { color: #FFA000; }
|
|
|
| .cython.code .coerce { color: #008000; border: 1px dotted #008000 }
|
| .cython.code .py_attr { color: #FF0000; font-weight: bold; }
|
| .cython.code .c_attr { color: #0000FF; }
|
| .cython.code .py_call { color: #FF0000; font-weight: bold; }
|
| .cython.code .c_call { color: #0000FF; }
|
| """)
|
|
|
|
|
| _onclick_attr = ' onclick="{0}"'.format((
|
| "(function(s){"
|
| " s.display = s.display === 'block' ? 'none' : 'block'"
|
| "})(this.nextElementSibling.style)"
|
| ).replace(' ', '')
|
| )
|
|
|
| def save_annotation(self, source_filename, target_filename, coverage_xml=None):
|
| with Utils.open_source_file(source_filename) as f:
|
| code = f.read()
|
| generated_code = self.code.get(source_filename, {})
|
| c_file = Utils.decode_filename(os.path.basename(target_filename))
|
| html_filename = os.path.splitext(target_filename)[0] + ".html"
|
|
|
| with codecs.open(html_filename, "w", encoding="UTF-8") as out_buffer:
|
| out_buffer.write(self._save_annotation(code, generated_code, c_file, source_filename, coverage_xml))
|
|
|
| def _save_annotation_header(self, c_file, source_filename, coverage_timestamp=None):
|
| coverage_info = ''
|
| if coverage_timestamp:
|
| coverage_info = u' with coverage data from {timestamp}'.format(
|
| timestamp=datetime.fromtimestamp(int(coverage_timestamp) // 1000))
|
|
|
| outlist = [
|
| textwrap.dedent(u'''\
|
| <!DOCTYPE html>
|
| <!-- Generated by Cython {watermark} -->
|
| <html>
|
| <head>
|
| <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
| <title>Cython: {filename}</title>
|
| <style type="text/css">
|
| {css}
|
| </style>
|
| </head>
|
| <body class="cython">
|
| <p><span style="border-bottom: solid 1px grey;">Generated by Cython {watermark}</span>{more_info}</p>
|
| <p>
|
| <span style="background-color: #FFFF00">Yellow lines</span> hint at Python interaction.<br />
|
| Click on a line that starts with a "<code>+</code>" to see the C code that Cython generated for it.
|
| </p>
|
| ''').format(css=self._css(), watermark=Version.watermark,
|
| filename=os.path.basename(source_filename) if source_filename else '',
|
| more_info=coverage_info)
|
| ]
|
| if c_file:
|
| outlist.append(u'<p>Raw output: <a href="%s">%s</a></p>\n' % (c_file, c_file))
|
| return outlist
|
|
|
| def _save_annotation_footer(self):
|
| return (u'</body></html>\n',)
|
|
|
| def _save_annotation(self, code, generated_code, c_file=None, source_filename=None, coverage_xml=None):
|
| """
|
| lines : original cython source code split by lines
|
| generated_code : generated c code keyed by line number in original file
|
| target filename : name of the file in which to store the generated html
|
| c_file : filename in which the c_code has been written
|
| """
|
| if coverage_xml is not None and source_filename:
|
| coverage_timestamp = coverage_xml.get('timestamp', '').strip()
|
| covered_lines = self._get_line_coverage(coverage_xml, source_filename)
|
| else:
|
| coverage_timestamp = covered_lines = None
|
| annotation_items = dict(self.annotations[source_filename])
|
| scopes = dict(self.scopes[source_filename])
|
|
|
| outlist = []
|
| outlist.extend(self._save_annotation_header(c_file, source_filename, coverage_timestamp))
|
| outlist.extend(self._save_annotation_body(code, generated_code, annotation_items, scopes, covered_lines))
|
| outlist.extend(self._save_annotation_footer())
|
| return ''.join(outlist)
|
|
|
| def _get_line_coverage(self, coverage_xml, source_filename):
|
| coverage_data = None
|
| for entry in coverage_xml.iterfind('.//class'):
|
| if not entry.get('filename'):
|
| continue
|
| if (entry.get('filename') == source_filename or
|
| os.path.abspath(entry.get('filename')) == source_filename):
|
| coverage_data = entry
|
| break
|
| elif source_filename.endswith(entry.get('filename')):
|
| coverage_data = entry
|
| if coverage_data is None:
|
| return None
|
| return dict(
|
| (int(line.get('number')), int(line.get('hits')))
|
| for line in coverage_data.iterfind('lines/line')
|
| )
|
|
|
| def _htmlify_code(self, code, language):
|
| try:
|
| from pygments import highlight
|
| from pygments.lexers import CythonLexer, CppLexer
|
| from pygments.formatters import HtmlFormatter
|
| except ImportError:
|
|
|
| return html_escape(code)
|
|
|
| if language == "cython":
|
| lexer = CythonLexer(stripnl=False, stripall=False)
|
| elif language == "c/cpp":
|
| lexer = CppLexer(stripnl=False, stripall=False)
|
| else:
|
|
|
| return html_escape(code)
|
| html_code = highlight(
|
| code, lexer,
|
| HtmlFormatter(nowrap=True))
|
| return html_code
|
|
|
| def _save_annotation_body(self, cython_code, generated_code, annotation_items, scopes, covered_lines=None):
|
| outlist = [u'<div class="cython">']
|
| pos_comment_marker = u'/* \N{HORIZONTAL ELLIPSIS} */\n'
|
| new_calls_map = dict(
|
| (name, 0) for name in
|
| 'refnanny trace py_macro_api py_c_api pyx_macro_api pyx_c_api error_goto'.split()
|
| ).copy
|
|
|
| self.mark_pos(None)
|
|
|
| def annotate(match):
|
| group_name = match.lastgroup
|
| calls[group_name] += 1
|
| return u"<span class='%s'>%s</span>" % (
|
| group_name, match.group(group_name))
|
|
|
| lines = self._htmlify_code(cython_code, "cython").splitlines()
|
| lineno_width = len(str(len(lines)))
|
| if not covered_lines:
|
| covered_lines = None
|
|
|
| for k, line in enumerate(lines, 1):
|
| try:
|
| c_code = generated_code[k]
|
| except KeyError:
|
| c_code = ''
|
| else:
|
| c_code = _replace_pos_comment(pos_comment_marker, c_code)
|
| if c_code.startswith(pos_comment_marker):
|
| c_code = c_code[len(pos_comment_marker):]
|
| c_code = html_escape(c_code)
|
|
|
| calls = new_calls_map()
|
| c_code = _parse_code(annotate, c_code)
|
| score = (5 * calls['py_c_api'] + 2 * calls['pyx_c_api'] +
|
| calls['py_macro_api'] + calls['pyx_macro_api'])
|
|
|
| if c_code:
|
| onclick = self._onclick_attr
|
| expandsymbol = '+'
|
| else:
|
| onclick = ''
|
| expandsymbol = ' '
|
|
|
| covered = ''
|
| if covered_lines is not None and k in covered_lines:
|
| hits = covered_lines[k]
|
| if hits is not None:
|
| covered = 'run' if hits else 'mis'
|
|
|
| outlist.append(
|
| u'<pre class="cython line score-{score}"{onclick}>'
|
|
|
|
|
| u'{expandsymbol}<span class="{covered}">{line:0{lineno_width}d}</span>: {code}</pre>\n'.format(
|
| score=score,
|
| expandsymbol=expandsymbol,
|
| covered=covered,
|
| lineno_width=lineno_width,
|
| line=k,
|
| code=line.rstrip(),
|
| onclick=onclick,
|
| ))
|
| if c_code:
|
| outlist.append(u"<pre class='cython code score-{score} {covered}'>{code}</pre>".format(
|
| score=score, covered=covered, code=c_code))
|
| outlist.append(u"</div>")
|
|
|
|
|
| if self.show_entire_c_code:
|
| outlist.append(u'<p><div class="cython">')
|
| onclick_title = u"<pre class='cython line'{onclick}>+ {title}</pre>\n"
|
| outlist.append(onclick_title.format(
|
| onclick=self._onclick_attr,
|
| title=AnnotationCCodeWriter.COMPLETE_CODE_TITLE,
|
| ))
|
| complete_code_as_html = self._htmlify_code(self.buffer.getvalue(), "c/cpp")
|
| outlist.append(u"<pre class='cython code'>{code}</pre>".format(code=complete_code_as_html))
|
| outlist.append(u"</div></p>")
|
|
|
| return outlist
|
|
|
|
|
| _parse_code = re.compile((
|
| br'(?P<refnanny>__Pyx_X?(?:GOT|GIVE)REF|__Pyx_RefNanny[A-Za-z]+)|'
|
| br'(?P<trace>__Pyx_Trace[A-Za-z]+)|'
|
| br'(?:'
|
| br'(?P<pyx_macro_api>__Pyx_[A-Z][A-Z_]+)|'
|
| br'(?P<pyx_c_api>(?:__Pyx_[A-Z][a-z_][A-Za-z_]*)|__pyx_convert_[A-Za-z_]*)|'
|
| br'(?P<py_macro_api>Py[A-Z][a-z]+_[A-Z][A-Z_]+)|'
|
| br'(?P<py_c_api>Py[A-Z][a-z]+_[A-Z][a-z][A-Za-z_]*)'
|
| br')(?=\()|'
|
| br'(?P<error_goto>(?:(?<=;) *if [^;]* +)?__PYX_ERR\([^)]+\))'
|
| ).decode('ascii')).sub
|
|
|
|
|
| _replace_pos_comment = re.compile(
|
|
|
| br'^\s*/\*(?:(?:[^*]|\*[^/])*\n)+\s*\*/\s*\n'.decode('ascii'),
|
| re.M
|
| ).sub
|
|
|
|
|
| class AnnotationItem(object):
|
|
|
| def __init__(self, style, text, tag="", size=0):
|
| self.style = style
|
| self.text = text
|
| self.tag = tag
|
| self.size = size
|
|
|
| def start(self):
|
| return u"<span class='cython tag %s' title='%s'>%s" % (self.style, self.text, self.tag)
|
|
|
| def end(self):
|
| return self.size, u"</span>"
|
|
|