256 lines
8.6 KiB
Python
256 lines
8.6 KiB
Python
# Online Python Tutor
|
|
# https://github.com/pgbovine/OnlinePythonTutor/
|
|
#
|
|
# Copyright (C) 2010-2012 Philip J. Guo (philip@pgbovine.net)
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
# copy of this software and associated documentation files (the
|
|
# "Software"), to deal in the Software without restriction, including
|
|
# without limitation the rights to use, copy, modify, merge, publish,
|
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
# permit persons to whom the Software is furnished to do so, subject to
|
|
# the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included
|
|
# in all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
# Thanks to John DeNero for making the encoder work on both Python 2 and 3
|
|
|
|
|
|
# Given an arbitrary piece of Python data, encode it in such a manner
|
|
# that it can be later encoded into JSON.
|
|
# http://json.org/
|
|
#
|
|
# We use this function to encode run-time traces of data structures
|
|
# to send to the front-end.
|
|
#
|
|
# Format:
|
|
# Primitives:
|
|
# * None, int, long, float, str, bool - unchanged
|
|
# (json.dumps encodes these fine verbatim)
|
|
#
|
|
# Compound objects:
|
|
# * list - ['LIST', elt1, elt2, elt3, ..., eltN]
|
|
# * tuple - ['TUPLE', elt1, elt2, elt3, ..., eltN]
|
|
# * set - ['SET', elt1, elt2, elt3, ..., eltN]
|
|
# * dict - ['DICT', [key1, value1], [key2, value2], ..., [keyN, valueN]]
|
|
# * instance - ['INSTANCE', class name, [attr1, value1], [attr2, value2], ..., [attrN, valueN]]
|
|
# * class - ['CLASS', class name, [list of superclass names], [attr1, value1], [attr2, value2], ..., [attrN, valueN]]
|
|
# * function - ['FUNCTION', function name, parent frame ID (for nested functions)]
|
|
# * module - ['module', module name]
|
|
# * other - [<type name>, string representation of object]
|
|
# * compound object reference - ['REF', target object's unique_id]
|
|
#
|
|
# the unique_id is derived from id(), which allows us to capture aliasing
|
|
|
|
|
|
# number of significant digits for floats
|
|
FLOAT_PRECISION = 4
|
|
|
|
|
|
import re, types
|
|
import sys
|
|
typeRE = re.compile("<type '(.*)'>")
|
|
classRE = re.compile("<class '(.*)'>")
|
|
|
|
import inspect
|
|
|
|
is_python3 = (sys.version_info[0] == 3)
|
|
if is_python3:
|
|
long = None # Avoid NameError when evaluating "long"
|
|
|
|
|
|
def is_class(dat):
|
|
"""Return whether dat is a class."""
|
|
if is_python3:
|
|
return isinstance(dat, type)
|
|
else:
|
|
return type(dat) in (types.ClassType, types.TypeType)
|
|
|
|
|
|
def is_instance(dat):
|
|
"""Return whether dat is an instance of a class."""
|
|
if is_python3:
|
|
return isinstance(type(dat), type) and not isinstance(dat, type)
|
|
else:
|
|
# ugh, classRE match is a bit of a hack :(
|
|
return type(dat) == types.InstanceType or classRE.match(str(type(dat)))
|
|
|
|
|
|
def get_name(obj):
|
|
"""Return the name of an object."""
|
|
return obj.__name__ if hasattr(obj, '__name__') else get_name(type(obj))
|
|
|
|
|
|
# Note that this might BLOAT MEMORY CONSUMPTION since we're holding on
|
|
# to every reference ever created by the program without ever releasing
|
|
# anything!
|
|
class ObjectEncoder:
|
|
def __init__(self):
|
|
# Key: canonicalized small ID
|
|
# Value: encoded (compound) heap object
|
|
self.encoded_heap_objects = {}
|
|
|
|
self.id_to_small_IDs = {}
|
|
self.cur_small_ID = 1
|
|
|
|
|
|
def get_heap(self):
|
|
return self.encoded_heap_objects
|
|
|
|
|
|
def reset_heap(self):
|
|
# VERY IMPORTANT to reassign to an empty dict rather than just
|
|
# clearing the existing dict, since get_heap() could have been
|
|
# called earlier to return a reference to a previous heap state
|
|
self.encoded_heap_objects = {}
|
|
|
|
def set_function_parent_frame_ID(self, ref_obj, enclosing_frame_id):
|
|
assert ref_obj[0] == 'REF'
|
|
func_obj = self.encoded_heap_objects[ref_obj[1]]
|
|
assert func_obj[0] == 'FUNCTION'
|
|
func_obj[-1] = enclosing_frame_id
|
|
|
|
|
|
# return either a primitive object or an object reference;
|
|
# and as a side effect, update encoded_heap_objects
|
|
def encode(self, dat, get_parent):
|
|
"""Encode a data value DAT using the GET_PARENT function for parent ids."""
|
|
# primitive type
|
|
if type(dat) in (int, long, float, str, bool, type(None)):
|
|
if type(dat) is float:
|
|
return round(dat, FLOAT_PRECISION)
|
|
else:
|
|
return dat
|
|
|
|
# compound type - return an object reference and update encoded_heap_objects
|
|
else:
|
|
my_id = id(dat)
|
|
|
|
try:
|
|
my_small_id = self.id_to_small_IDs[my_id]
|
|
except KeyError:
|
|
my_small_id = self.cur_small_ID
|
|
self.id_to_small_IDs[my_id] = self.cur_small_ID
|
|
self.cur_small_ID += 1
|
|
|
|
del my_id # to prevent bugs later in this function
|
|
|
|
ret = ['REF', my_small_id]
|
|
|
|
# punt early if you've already encoded this object
|
|
if my_small_id in self.encoded_heap_objects:
|
|
return ret
|
|
|
|
|
|
# major side-effect!
|
|
new_obj = []
|
|
self.encoded_heap_objects[my_small_id] = new_obj
|
|
|
|
typ = type(dat)
|
|
|
|
if typ == list:
|
|
new_obj.append('LIST')
|
|
for e in dat:
|
|
new_obj.append(self.encode(e, get_parent))
|
|
elif typ == tuple:
|
|
new_obj.append('TUPLE')
|
|
for e in dat:
|
|
new_obj.append(self.encode(e, get_parent))
|
|
elif typ == set:
|
|
new_obj.append('SET')
|
|
for e in dat:
|
|
new_obj.append(self.encode(e, get_parent))
|
|
elif typ == dict:
|
|
new_obj.append('DICT')
|
|
for (k, v) in dat.items():
|
|
# don't display some built-in locals ...
|
|
if k not in ('__module__', '__return__', '__locals__'):
|
|
new_obj.append([self.encode(k, get_parent), self.encode(v, get_parent)])
|
|
elif typ in (types.FunctionType, types.MethodType):
|
|
if is_python3:
|
|
argspec = inspect.getfullargspec(dat)
|
|
else:
|
|
argspec = inspect.getargspec(dat)
|
|
|
|
printed_args = [e for e in argspec.args]
|
|
if argspec.varargs:
|
|
printed_args.append('*' + argspec.varargs)
|
|
|
|
if is_python3:
|
|
if argspec.varkw:
|
|
printed_args.append('**' + argspec.varkw)
|
|
if argspec.kwonlyargs:
|
|
printed_args.extend(argspec.kwonlyargs)
|
|
else:
|
|
if argspec.keywords:
|
|
printed_args.append('**' + argspec.keywords)
|
|
|
|
func_name = get_name(dat)
|
|
pretty_name = func_name + '(' + ', '.join(printed_args) + ')'
|
|
encoded_val = ['FUNCTION', pretty_name, None]
|
|
if get_parent:
|
|
enclosing_frame_id = get_parent(dat)
|
|
encoded_val[2] = enclosing_frame_id
|
|
new_obj.extend(encoded_val)
|
|
elif typ is types.BuiltinFunctionType:
|
|
pretty_name = get_name(dat) + '(...)'
|
|
new_obj.extend(['FUNCTION', pretty_name, None])
|
|
elif is_class(dat) or is_instance(dat):
|
|
self.encode_class_or_instance(dat, new_obj)
|
|
elif typ is types.ModuleType:
|
|
new_obj.extend(['module', dat.__name__])
|
|
else:
|
|
typeStr = str(typ)
|
|
m = typeRE.match(typeStr)
|
|
|
|
if not m:
|
|
m = classRE.match(typeStr)
|
|
|
|
assert m, typ
|
|
new_obj.extend([m.group(1), str(dat)])
|
|
|
|
return ret
|
|
|
|
|
|
def encode_class_or_instance(self, dat, new_obj):
|
|
"""Encode dat as a class or instance."""
|
|
if is_instance(dat):
|
|
if hasattr(dat, '__class__'):
|
|
# common case ...
|
|
class_name = get_name(dat.__class__)
|
|
else:
|
|
# super special case for something like
|
|
# "from datetime import datetime_CAPI" in Python 3.2,
|
|
# which is some weird 'PyCapsule' type ...
|
|
# http://docs.python.org/release/3.1.5/c-api/capsule.html
|
|
class_name = get_name(type(dat))
|
|
|
|
new_obj.extend(['INSTANCE', class_name])
|
|
# don't traverse inside modules, or else risk EXPLODING the visualization
|
|
if class_name == 'module':
|
|
return
|
|
else:
|
|
superclass_names = [e.__name__ for e in dat.__bases__ if e is not object]
|
|
new_obj.extend(['CLASS', get_name(dat), superclass_names])
|
|
|
|
# traverse inside of its __dict__ to grab attributes
|
|
# (filter out useless-seeming ones):
|
|
hidden = ('__doc__', '__module__', '__return__', '__dict__',
|
|
'__locals__', '__weakref__')
|
|
if hasattr(dat, '__dict__'):
|
|
user_attrs = sorted([e for e in dat.__dict__ if e not in hidden])
|
|
else:
|
|
user_attrs = []
|
|
|
|
for attr in user_attrs:
|
|
new_obj.append([self.encode(attr, None), self.encode(dat.__dict__[attr], None)])
|
|
|