| ''' | |
| Copyright (c) 2018 Martin Kloesch | |
| All rights reserved. This program and the accompanying materials | |
| are made available under the terms of the Eclipse Public License v2.0 | |
| which accompanies this distribution, and is available at | |
| https://www.eclipse.org/legal/epl-2.0/ | |
| Contributors: | |
| * Martin Kloesch - initial API and implementation | |
| ''' | |
| # Python std library imports | |
| import ast as _pyease_ast | |
| import bdb as _pyease_bdb | |
| import functools as _pyease_functools | |
| import re as _pyease_re | |
| import sys as _pyease_sys | |
| import threading as _pyease_threading | |
| # : Regular expression if we are dealing with internal module. | |
| _pyease_INTERNAL_CHECKER = _pyease_re.compile(r'^<.+>$') | |
| class _pyease_PyFrame: | |
| ''' | |
| Python implementation of IPyFrame used for exchanging frame data | |
| with eclipse. | |
| Simply wraps standard python frame to more easily usable format. | |
| Python frame documentation is available here: | |
| https://docs.python.org/3/reference/datamodel.html#types | |
| ''' | |
| def __init__(self, frame): | |
| ''' | |
| Constructor only stores frame to member. | |
| :param frame: Python frame to be converted. | |
| ''' | |
| self._frame = frame | |
| def _valid_frame(self): | |
| ''' | |
| Utility method checking if the current frame is valid | |
| A frame is valid if all information is present and | |
| it's file is not ignored. | |
| :returns: ``True`` if frame is valid. | |
| ''' | |
| return self._frame and not _pyease_ignore_frame(self._frame) | |
| def getFilename(self): | |
| ''' | |
| Returns the filename of the frame. | |
| If no frame was set, a dummy value will be returned. | |
| :returns: Filename of frame or dummy value. | |
| ''' | |
| if self._valid_frame(): | |
| return self._frame.f_code.co_filename | |
| else: | |
| return '__no_frame__' | |
| def getLineNumber(self): | |
| ''' | |
| Returns the linenumber of the frame. | |
| If no frame was set, -1 is returned. | |
| :returns: Line number of frame or -1. | |
| ''' | |
| if self._valid_frame(): | |
| return self._frame.f_lineno | |
| else: | |
| return -1 | |
| def getParent(self): | |
| ''' | |
| Returns the parent of the frame. | |
| If no frame was set or the frame does not have a parent | |
| `None` is returned. | |
| :returns: Parent frame in stack or `None` | |
| ''' | |
| if self._valid_frame(): | |
| return _pyease_PyFrame(self._frame.f_back) | |
| else: | |
| return None | |
| def getVariable(self, name): | |
| ''' | |
| Get a variable from the current frame. | |
| :param: name: variable name to look up | |
| :return: variable or <code>null</code> | |
| ''' | |
| if self._valid_frame(): | |
| if name in self._frame.f_locals: | |
| return convert_value(self._frame.f_locals[name]) | |
| if name in self._frame.f_globals: | |
| return convert_value(self._frame.f_globals[name]) | |
| return None | |
| def getVariables(self): | |
| ''' | |
| Get variables visible from current frame. | |
| :return: variableName -> variableContent | |
| ''' | |
| variables = {} | |
| if self._valid_frame(): | |
| # TODO: Differentiate between global and local variables | |
| variables.update(self._frame.f_globals) | |
| variables.update(self._frame.f_locals) | |
| return convert_value(variables) | |
| def setVariable(self, name, value): | |
| ''' | |
| Sets new value for a variable only if it exists. | |
| :param name: Name of variable to be updated. | |
| :param value: New value for variable. | |
| ''' | |
| if self._valid_frame(): | |
| if name in self._frame.f_locals: | |
| self._frame.f_locals[name] = value | |
| elif name in self._frame.f_globals: | |
| self._frame.f_globals[name] = value | |
| def toString(self): | |
| ''' | |
| Required override to avoid problems in py4j bridge. | |
| ''' | |
| return '{}: #{:d}'.format(self.getFilename(), self.getLineNumber()) | |
| class Java: | |
| implements = ['org.eclipse.ease.lang.python.debugger.IPyFrame'] | |
| def _pyease_ignore_frame(frame, first=True): | |
| ''' | |
| Utility to check if a frame should be ignored. | |
| Current reasons to ignore a frame: | |
| * It is the top entry of the stack and it is a standard module. | |
| e.g. <string> | |
| * It is part of the py4j library or has py4j in its call chain. | |
| Recursively checks trace until at bottom of stack. | |
| :param frame: Frame to check if it should be ignored. | |
| :param first: Flag to signalize if it is the first call. | |
| :returns: `True` if the frame should be ignored. | |
| ''' | |
| # End of frame | |
| if not frame: | |
| return False | |
| # ignore frames that originate from the self.run() method | |
| if first: | |
| if frame.f_code.co_filename == '(none)': | |
| return True | |
| # Check if we are in the standard modules | |
| if _pyease_INTERNAL_CHECKER.match(frame.f_code.co_filename): | |
| # Only ignore standard module if its the top of the stack | |
| return first | |
| # TODO: Think of better way to identify py4j library | |
| if 'py4j' in frame.f_code.co_filename.lower(): | |
| return True | |
| return _pyease_ignore_frame(frame.f_back, False) | |
| class _pyease_CodeTracer(_pyease_bdb.Bdb): | |
| ''' | |
| Eclipse Debugger class. | |
| ''' | |
| def __init__(self, *args, **kwargs): | |
| ''' | |
| Constructor initializes instance variables. | |
| ''' | |
| _pyease_bdb.Bdb.__init__(self, *args, **kwargs) | |
| # Debugger for communication with Java world | |
| self._debugger = None | |
| # Caches for currently executed code parts | |
| self._current_frame = None | |
| self._current_file = None | |
| # Async continuation handling | |
| self._continue_event = _pyease_threading.Event() | |
| self._continue_func = lambda: None | |
| # TODO: Think about a better way to handle step return | |
| self._return_hack = False | |
| def setDebugger(self, debugger): | |
| ''' | |
| Setter method for self._debugger. | |
| :param org.eclipse.ease.lang.python.debugger.PythonDebugger debugger: | |
| PythonDebugger object to handling communication with Eclipse. | |
| ''' | |
| self._debugger = debugger | |
| def trace_dispatch(self, frame, event, arg): | |
| ''' | |
| Called for every executed line of source code. | |
| Checks if line is of interest and dispatches the call for | |
| further investigation. | |
| :param frame: Current stack frame. | |
| :param event: Type of dispatch event that occurred. | |
| :param arg: Optional argument for dispatching. | |
| ''' | |
| # Pre-filter to avoid issues with library internals | |
| if not _pyease_ignore_frame(frame): | |
| _pyease_bdb.Bdb.trace_dispatch(self, frame, event, arg) | |
| return self.trace_dispatch | |
| def user_line(self, frame): | |
| ''' | |
| Called when debugger thinks line is of interest. | |
| :param frame: Current stack frame with line. | |
| ''' | |
| self._continue_func = self.set_continue | |
| self.dispatch(frame, 'line') | |
| def user_call(self, frame, argument_list): | |
| ''' | |
| Called when debugger thinks call is of interest. | |
| :param frame: Current stack frame with call information. | |
| :param argument_list: ignored | |
| ''' | |
| self.dispatch(frame, 'call') | |
| def user_return(self, frame, return_value): | |
| ''' | |
| Called when debugger thinks returning from function is of interest. | |
| :param frame: Current stack frame with return information. | |
| :param return_value: ignored | |
| ''' | |
| self._continue_func = self.set_step | |
| self.dispatch(frame, 'return') | |
| def user_exception(self, frame, exc_info): | |
| ''' | |
| Called when debugger thinks thrown exception is of interest. | |
| :param frame: Current stack frame with exception information. | |
| :param exc_info: ignored | |
| ''' | |
| # Cache exception | |
| if self._debugger: | |
| self._debugger.setExceptionStackTrace(_pyease_PyFrame(frame)) | |
| self._continue_func = self.set_continue | |
| self.dispatch(frame, 'exception') | |
| def dispatch(self, frame, dispatch_type): | |
| ''' | |
| Dispatches the given frame to the interested debugger. | |
| :param frame: Current stack frame to be dispatched. | |
| :param dispatch_type: Type of dispatch event (call, line, ...) | |
| ''' | |
| if not self._debugger: | |
| return | |
| # Set up caches for asynchronous continuation | |
| self._current_frame = frame | |
| self._continue_event.set() | |
| # Check if we need to fake the current frame for Eclipse | |
| if self._return_hack: | |
| frame = frame.f_back | |
| self._return_hack = False | |
| # Dispatch call to Java | |
| self._debugger.traceDispatch(_pyease_PyFrame(frame), dispatch_type) | |
| # Make asynchronous call synchronous again | |
| self._continue_event.wait() | |
| self._continue_func() | |
| def suspend(self): | |
| ''' | |
| Suspend the execution to wait for asynchronous callbacks | |
| from Java. | |
| ''' | |
| self._continue_event.clear() | |
| def resume(self, resume_type): | |
| ''' | |
| Resume execution by calling the given resume function. | |
| :param resume_type: Desired resume function identifier. | |
| ''' | |
| frame = self._current_frame | |
| # Step into | |
| if resume_type == 1: | |
| self._continue_func = self.set_step | |
| # Step over | |
| elif resume_type == 2: | |
| self._continue_func = _pyease_functools.partial(self.set_next, frame) | |
| # Step return | |
| elif resume_type == 4: | |
| self._return_hack = True | |
| self._continue_func = _pyease_functools.partial(self.set_return, frame) | |
| # Continue | |
| else: | |
| self._continue_func = self.set_continue | |
| self._continue_event.set() | |
| def set_continue(self): | |
| ''' | |
| FIXME: Override of bdb.Bdb.set_continue | |
| Original stops debugging when no more breakpoints in file. | |
| This means we cannot add breakpoints later on. | |
| ''' | |
| self._set_stopinfo(self.botframe, None, -1) | |
| def setBreakpoint(self, breakpoint): | |
| ''' | |
| Interface method for Java to have simpler method of setting | |
| a breakpoint. | |
| FIXME: This is a copy of bdb.Bdb.set_break | |
| set_break performs OS level checks to see if file exists. | |
| :param breakpoint: Breakpoint to be set. | |
| ''' | |
| filename = self.canonic(breakpoint.getFilename()) | |
| lineno = breakpoint.getLineno() | |
| if filename not in self.breaks: | |
| self.breaks[filename] = [] | |
| linenos = self.breaks[filename] | |
| if lineno not in linenos: | |
| linenos .append(lineno) | |
| _pyease_bdb.Breakpoint(filename, lineno, 0, None, None) | |
| def removeBreakpoint(self, breakpoint): | |
| ''' | |
| Interface method for Java to have simpler method of removing | |
| a breakpoint. | |
| :param breakpoint: Breakpoint to be removed. | |
| ''' | |
| filename = self.canonic(breakpoint.getFilename()) | |
| lineno = breakpoint.getLineno() | |
| self.clear_break(filename, lineno) | |
| def stop_here(self, frame): | |
| ''' | |
| Hijacks frame check to see if currently executed file has changed. | |
| If file changed new breakpoints will be loaded. | |
| :param frame: Current stack frame to check for file change. | |
| ''' | |
| if self._debugger: | |
| file = frame.f_code.co_filename | |
| if file != self._current_file: | |
| self._current_file = file | |
| self.clear_all_file_breaks(file) | |
| breakpoints = self._debugger.getBreakpoints(file) | |
| for breakpoint in breakpoints: | |
| self.setBreakpoint(breakpoint) | |
| # If we have breakpoints we want to let the debugger know | |
| if breakpoints: | |
| return True | |
| return _pyease_bdb.Bdb.stop_here(self, frame) | |
| def run(self, script, filename): | |
| ''' | |
| Executes the script given using the bdb.Bdb.run method. | |
| :param script: Script to be executed. | |
| :param filename: Filename for easier identification of dynamic code. | |
| ''' | |
| # Compile code for better inspection | |
| code = '{}\n'.format(script.getCode()) | |
| ast_ = _pyease_ast.parse(code, filename, 'exec') | |
| final_expr = None | |
| for field_ in _pyease_ast.iter_fields(ast_): | |
| if 'body' != field_[0]: | |
| continue | |
| # Check if last item is an expression | |
| if len(field_[1]) > 0: | |
| if isinstance(field_[1][-1], _pyease_ast.Expr): | |
| final_expr = _pyease_ast.Expression() | |
| popped = field_[1].pop(-1) | |
| final_expr.body = popped.value | |
| # Run code up to last expression | |
| return_value = None | |
| compiled = compile(ast_, filename, 'exec') | |
| _pyease_bdb.Bdb.run(self, compiled) | |
| self._keep_debugging() | |
| # Check if last expression still needs to be run | |
| if final_expr: | |
| compiled = compile(final_expr, filename, 'eval') | |
| return_value = self.runeval(compiled) | |
| self._keep_debugging() | |
| return return_value | |
| def _keep_debugging(self): | |
| ''' | |
| Resets debugger state to be able to continue debugging after | |
| script injection. | |
| ''' | |
| _pyease_sys.settrace(self.trace_dispatch) | |
| self.quitting = False | |
| def toString(self): | |
| ''' | |
| Required override to avoid problems in py4j bridge. | |
| ''' | |
| return str(self) | |
| def equals(self, other): | |
| ''' | |
| Required override to avoid problems in py4j bridge. | |
| ''' | |
| return self is other | |
| class Java: | |
| implements = ['org.eclipse.ease.lang.python.py4j.internal.ICodeTraceFilter'] | |
| # Set up connection between eclipse and python | |
| _pyease_CodeTracer() |