123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317 |
- # Note: Work in progress
- 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 # does not support writing 'str' in Py2
- from . import Version
- from .Code import CCodeWriter
- from .. import Utils
- class AnnotationCCodeWriter(CCodeWriter):
- def __init__(self, create_from=None, buffer=None, copy_formatting=True):
- CCodeWriter.__init__(self, create_from, buffer, copy_formatting=copy_formatting)
- if create_from is None:
- self.annotation_buffer = StringIO()
- self.last_annotated_pos = None
- # annotations[filename][line] -> [(column, AnnotationItem)*]
- self.annotations = defaultdict(partial(defaultdict, list))
- # code[filename][line] -> str
- self.code = defaultdict(partial(defaultdict, str))
- # scopes[filename][line] -> set(scopes)
- self.scopes = defaultdict(partial(defaultdict, set))
- else:
- # When creating an insertion point, keep references to the same database
- 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(self, s):
- CCodeWriter.write(self, 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:
- # lambdas and genexprs can result in multiple scopes per line => keep them in a set
- 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/(1+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 { 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; }
- """)
- # on-click toggle function to show/hide C source code
- _onclick_attr = ' onclick="{0}"'.format((
- "(function(s){"
- " s.display = s.display === 'block' ? 'none' : 'block'"
- "})(this.nextElementSibling.style)"
- ).replace(' ', '') # poor dev's JS minification
- )
- 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 # but we might still find a better match...
- 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):
- try:
- from pygments import highlight
- from pygments.lexers import CythonLexer
- from pygments.formatters import HtmlFormatter
- except ImportError:
- # no Pygments, just escape the code
- return html_escape(code)
- html_code = highlight(
- code, CythonLexer(stripnl=False, stripall=False),
- 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).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}>'
- # generate line number with expand symbol in front,
- # and the right number of digit
- 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>")
- 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')(?=\()|' # look-ahead to exclude subsequent '(' from replacement
- br'(?P<error_goto>(?:(?<=;) *if [^;]* +)?__PYX_ERR\([^)]+\))'
- ).decode('ascii')).sub
- _replace_pos_comment = re.compile(
- # this matches what Cython generates as code line marker comment
- 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>"
|