# 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): # also used as marker for detection of complete code emission in tests 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 # 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_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: # 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.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; } """) # 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'''\
Generated by Cython {watermark}{more_info}
Yellow lines hint at Python interaction.
Click on a line that starts with a "+
" to see the C code that Cython generated for it.
Raw output: %s
\n' % (c_file, c_file)) return outlist def _save_annotation_footer(self): return (u'\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, language): try: from pygments import highlight from pygments.lexers import CythonLexer, CppLexer from pygments.formatters import HtmlFormatter except ImportError: # no Pygments, just escape the code return html_escape(code) if language == "cython": lexer = CythonLexer(stripnl=False, stripall=False) elif language == "c/cpp": lexer = CppLexer(stripnl=False, stripall=False) else: # unknown language, use fallback 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''
# generate line number with expand symbol in front,
# and the right number of digit
u'{expandsymbol}{line:0{lineno_width}d}: {code}
\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"{code}".format( score=score, covered=covered, code=c_code)) outlist.append(u"
+ {title}\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"
{code}".format(code=complete_code_as_html)) outlist.append(u"