Annotate.py 7.63 KB
Newer Older
1 2
# Note: Work in progress

Robert Bradshaw's avatar
Robert Bradshaw committed
3
import os
4
import re
5
import codecs
Stefan Behnel's avatar
Stefan Behnel committed
6
from xml.sax.saxutils import escape as html_escape
7 8
from StringIO import StringIO

Robert Bradshaw's avatar
Robert Bradshaw committed
9
import Version
10
from Code import CCodeWriter
11
from Cython import Utils
12

13 14
from contextlib import closing

Robert Bradshaw's avatar
Robert Bradshaw committed
15
# need one-characters subsitutions (for now) so offsets aren't off
16 17 18 19 20
special_chars = [
    (u'&', u'\xF2', u'&'),
    (u'<', u'\xF0', u'&lt;'),
    (u'>', u'\xF1', u'&gt;'),
]
21

22 23 24

class AnnotationCCodeWriter(CCodeWriter):

Robert Bradshaw's avatar
Robert Bradshaw committed
25 26
    def __init__(self, create_from=None, buffer=None, copy_formatting=True):
        CCodeWriter.__init__(self, create_from, buffer, copy_formatting=True)
27
        if create_from is None:
28
            self.annotation_buffer = StringIO()
29 30 31 32
            self.annotations = []
            self.last_pos = None
            self.code = {}
        else:
33
            # When creating an insertion point, keep references to the same database
34 35 36
            self.annotation_buffer = create_from.annotation_buffer
            self.annotations = create_from.annotations
            self.code = create_from.code
37
            self.last_pos = create_from.last_pos
38

39 40
    def create_new(self, create_from, buffer, copy_formatting):
        return AnnotationCCodeWriter(create_from, buffer, copy_formatting)
41

42
    def write(self, s):
43 44
        CCodeWriter.write(self, s)
        self.annotation_buffer.write(s)
45

46
    def mark_pos(self, pos):
47 48
        if pos is not None:
            CCodeWriter.mark_pos(self, pos)
49
        if self.last_pos:
50
            pos_code = self.code.setdefault(self.last_pos[0].filename,{})
51 52
            code = pos_code.get(self.last_pos[1], "")
            pos_code[self.last_pos[1]] = code + self.annotation_buffer.getvalue()
53
        self.annotation_buffer = StringIO()
54 55 56 57
        self.last_pos = pos

    def annotate(self, pos, item):
        self.annotations.append((pos, item))
58

59 60
    def _css(self):
        """css template will later allow to choose a colormap"""
Matthias BUSSONNIER's avatar
Matthias BUSSONNIER committed
61
        return self._css_template
62

63 64 65 66 67 68 69
    _js = """
function toggleDiv(id) {
    theDiv = document.getElementById(id);
    if (theDiv.style.display != 'block') theDiv.style.display = 'block';
    else theDiv.style.display = 'none';
}
    """
70

71

72
    _css_template = """
73 74 75

body { font-family: courier; font-size: 12; }

76 77 78 79 80 81
.cython .code  { font-size: 9; color: #444444; display: none; margin-left: 20px; }
.cython .py_c_api  { color: red; }
.cython .py_macro_api  { color: #FF7000; }
.cython .pyx_c_api  { color: #FF3000; }
.cython .pyx_macro_api  { color: #FF7000; }
.cython .refnanny  { color: #FFA000; }
Robert Bradshaw's avatar
Robert Bradshaw committed
82

83
.cython .error_goto  { color: #FFA000; }
84

85
.cython .tag  {  }
86

87
.cython .coerce  { color: #008000; border: 1px dotted #008000 }
88

89 90
.cython .py_attr { color: #FF0000; font-weight: bold; }
.cython .c_attr  { color: #0000FF; }
91

92 93
.cython .py_call { color: #FF0000; font-weight: bold; }
.cython .c_call  { color: #0000FF; }
94

95
.cython .line { margin: 0em }
Robert Bradshaw's avatar
Robert Bradshaw committed
96

97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
    """

    def save_annotation(self, source_filename, target_filename):
        with closing(Utils.open_source_file(source_filename)) as f:
            lines = f.readlines()
        code_source_file = 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(lines, code_source_file , c_file))

    def _save_annotation_header(self, c_file):
        outlist = []
        outlist.append(u'<!DOCTYPE html>\n')
        outlist.append(u'<!-- Generated by Cython %s -->\n' % Version.watermark)
        outlist.append(u'<html>\n')
        outlist.append(u"""
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <style type="text/css">
        {css}
    </style>
    <script>
        {js}
    </script>
122
</head>
123
        """.format(css=self._css(), js=self._js))
124
        outlist.append(u'<body>\n')
125
        outlist.append(u'<p>Generated by Cython %s</p>\n' % Version.watermark)
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
        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, lines, code_source_file , c_file=None):
        """
        lines : original cython source code split by lines
        code_source_file : 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

        """

        outlist = []
        outlist.extend(self._save_annotation_header(c_file))

        outlist.extend(self._save_annotation_body(lines, code_source_file , c_file=None))
        outlist.extend(self._save_annotation_footer())
        return ''.join(outlist)


    def _save_annotation_body(self, lines, code_source_file , c_file=None):
        outlist=[]
        self.mark_pos(None)
        for k, line in enumerate(lines):
            for c, cc, html in special_chars:
                line = line.replace(c, cc)
            lines[k] = line
160

161 162
        zero_calls = dict((name, 0) for name in
                          'refnanny py_macro_api py_c_api pyx_macro_api pyx_c_api error_goto'.split())
163 164 165 166

        def annotate(match):
            group_name = match.lastgroup
            calls[group_name] += 1
167
            return ur"<span class='cython %s'>%s</span>" % (
168
                group_name, match.group(group_name))
169

170
        pos_comment_marker = u'/* \N{HORIZONTAL ELLIPSIS} */\n'
171
        k = 0
172

173 174 175
        for line in lines:
            k += 1
            try:
176
                code = code_source_file[k]
177 178
            except KeyError:
                code = ''
179
            else:
180 181 182 183
                code = _replace_pos_comment(pos_comment_marker, code)
                if code.startswith(pos_comment_marker):
                    code = code[len(pos_comment_marker):]
                code = html_escape(code)
184

185
            calls = zero_calls.copy()
186 187
            code = _parse_code(annotate, code)
            score = (5 * calls['py_c_api'] + 2 * calls['pyx_c_api'] +
188
                     calls['py_macro_api'] + calls['pyx_macro_api'])
Robert Bradshaw's avatar
Robert Bradshaw committed
189
            color = u"FFFF%02x" % int(255/(1+score/10.0))
190
            outlist.append(u"<pre class='cython line' style='background-color: #%s' onclick='toggleDiv(\"line%s\")'>" % (color, k))
191

192
            outlist.append(u" %d: " % k)
Robert Bradshaw's avatar
Robert Bradshaw committed
193
            for c, cc, html in special_chars:
194
                line = line.replace(cc, html)
195
            outlist.append(line.rstrip())
196

197
            outlist.append(u'</pre>\n')
198
            outlist.append(u"<pre id='line%s' class='cython code' style='background-color: #%s'>%s</pre>" % (k, color, code))
199
        return outlist
200

201 202 203 204
_parse_code = re.compile(
    ur'(?P<refnanny>__Pyx_X?(?:GOT|GIVE)REF|__Pyx_RefNanny[A-Za-z]+)|'
    ur'(?:'
    ur'(?P<pyx_macro_api>__Pyx_[A-Z][A-Z_]+)|'
Stefan Behnel's avatar
Stefan Behnel committed
205
    ur'(?P<pyx_c_api>__Pyx_[A-Z][a-z_][A-Za-z_]+)|'
206
    ur'(?P<py_macro_api>Py[A-Z][a-z]+_[A-Z][A-Z_]+)|'
Stefan Behnel's avatar
Stefan Behnel committed
207
    ur'(?P<py_c_api>Py[A-Z][a-z]+_[A-Z][a-z][A-Za-z_]+)'
208 209 210 211 212
    ur')(?=\()|'       # look-ahead to exclude subsequent '(' from replacement
    ur'(?P<error_goto>(?:(?<=;) *if .* +)?\{__pyx_filename = .*goto __pyx_L\w+;\})'
).sub


213 214 215 216 217 218 219
_replace_pos_comment = re.compile(
    # this matches what Cython generates as code line marker comment
    ur'^\s*/\*(?:(?:[^*]|\*[^/])*\n)+\s*\*/\s*\n',
    re.M
).sub


220
class AnnotationItem(object):
221

222 223 224 225 226
    def __init__(self, style, text, tag="", size=0):
        self.style = style
        self.text = text
        self.tag = tag
        self.size = size
227

228
    def start(self):
229
        return u"<span class='cython tag %s' title='%s'>%s" % (self.style, self.text, self.tag)
230

231
    def end(self):
232
        return self.size, u"</span>"