__init__.py 3.21 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
"""
PythonTutor port to Basthon.

For privacy concern, it avoid sending user code to PythonTutor
(iframe embeding). Instead, it generates PythonTutor's traces with Basthon's
interpreter then create a PythonTutor vizualiser.
"""

import js
import pkg_resources
from urllib.parse import quote
import json
from .generate_trace import generate_trace
from basthon import kernel

__author__ = "Romain Casati"
__license__ = "GNU GPL v3"
__email__ = "romain.casati@basthon.fr"


__all__ = ['tutor']


# get iframe.html
with open(pkg_resources.resource_filename('tutor', 'iframe.html')) as f:
    iframe_template = f.read()


29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
_iframe_count = -1


def _new_iframe_id():
    global _iframe_count
    _iframe_count += 1
    return f"basthon-pythontutor-iframe-{_iframe_count}"


# to dynamically set the iframe height we nee this communication
# system between iframe and parent
def _iframe_resize(event, *args):
    data = event.data
    if data is None:
        return

    type = data.type
    if type != 'pytutor-iframe-resize':
        return

    target = data.target
    height = data.height
    if target is None or height is None:
        return
    js.document.getElementById(target).style.height = f"{int(height) + 20}px"


js.window.addEventListener('message', _iframe_resize)


59
60
61
62
def tutor(**kwargs):
    """
    Tutor the current executed script with PythonTutor.

63
    See PythonTutor at http://pythontutor.com for available options.
64
65
66
67
68
69
70
71
72

    The code is not executed by PythonTutor itself (server side)
    but by Basthon. The solution is actually entirely executed
    in frontend.
    """
    code = kernel.locals()['__eval_data__']['code']
    lines = code.split('\n')

    # listing tutor import lines then remove
Romain Casati's avatar
Romain Casati committed
73
    # this is hacky... and should be implemented with inspect
74
75
76
77
78
79
80
81
    import_lines = [i for i, line in enumerate(lines)
                    if line in ('import tutor',
                                'from tutor import tutor',
                                'from tutor import *')]
    for index in import_lines[::-1]:
        del lines[index]

    # listing tutor() call lines
Romain Casati's avatar
Romain Casati committed
82
    # this is hacky... and should be implemented with inspect
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
    tutor_lines = [i for i, line in enumerate(lines)
                   if line.startswith('tutor.tutor(')
                   or line.startswith('tutor(')]
    # actually it could be everywhere but not nested
    if not tutor_lines:
        raise RuntimeError("tutor.tutor() should be called on first or last line.")
    # removing tutor() call lines
    for index in tutor_lines[::-1]:
        del lines[index]

    # rebuild code
    code = '\n'.join(lines).strip('\n')

    # generate PythonTutor trace
    trace = json.dumps(generate_trace(code, **kwargs))

99
100
101
    # get new iframe id
    id = _new_iframe_id()

102
103
104
    # building pytutor visualizer
    iframe = iframe_template.format(
        json_trace=trace,
Romain Casati's avatar
Romain Casati committed
105
        extern_tutor_url=f"{js.Basthon.basthonModulesRoot()}/extern",
106
        iframe_id=id)
107
    iframe = f'<iframe id="{id}" style="width: 100%; height: 400px;" frameborder="0" src="data:text/html;charset=utf-8,{quote(iframe)}"></iframe>'
108
109

    # sending node to Basthon for rendering
110
    kernel.display_event({"display_type": "tutor",
111
112
                          "content": iframe,
                          "iframe-id": id})