Collect return types when tracing call signatures

This commit is contained in:
Dmitry Trofimov
2016-02-21 01:20:34 +01:00
parent 8a95f405ac
commit 6a7762244c
13 changed files with 314 additions and 74 deletions
@@ -58,10 +58,9 @@ each command has a format:
* PYDB - pydevd, the python end
'''
from _pydev_imps import _pydev_threading as threading
from _pydev_bundle.pydev_imports import _queue
from _pydev_imps import _pydev_time as time, _pydev_thread
from _pydev_imps._pydev_socket import socket, AF_INET, SOCK_STREAM, SHUT_RD, SHUT_WR
from _pydev_bundle.pydev_imports import _queue
from _pydevd_bundle.pydevd_constants import * #@UnusedWildImport
try:
@@ -115,10 +114,9 @@ CMD_REMOVE_DJANGO_EXCEPTION_BREAK = 126
CMD_SET_NEXT_STATEMENT = 127
CMD_SMART_STEP_INTO = 128
CMD_EXIT = 129
CMD_SIGNATURE_CALL_TRACE = 130
CMD_SET_PY_EXCEPTION = 131
CMD_GET_FILE_CONTENTS = 132
CMD_SET_PROPERTY_TRACE = 133
@@ -142,35 +140,36 @@ CMD_RETURN = 502
CMD_ERROR = 901
ID_TO_MEANING = {
'101':'CMD_RUN',
'102':'CMD_LIST_THREADS',
'103':'CMD_THREAD_CREATE',
'104':'CMD_THREAD_KILL',
'105':'CMD_THREAD_SUSPEND',
'106':'CMD_THREAD_RUN',
'107':'CMD_STEP_INTO',
'108':'CMD_STEP_OVER',
'109':'CMD_STEP_RETURN',
'110':'CMD_GET_VARIABLE',
'111':'CMD_SET_BREAK',
'112':'CMD_REMOVE_BREAK',
'113':'CMD_EVALUATE_EXPRESSION',
'114':'CMD_GET_FRAME',
'115':'CMD_EXEC_EXPRESSION',
'116':'CMD_WRITE_TO_CONSOLE',
'117':'CMD_CHANGE_VARIABLE',
'118':'CMD_RUN_TO_LINE',
'119':'CMD_RELOAD_CODE',
'120':'CMD_GET_COMPLETIONS',
'121':'CMD_CONSOLE_EXEC',
'122':'CMD_ADD_EXCEPTION_BREAK',
'123':'CMD_REMOVE_EXCEPTION_BREAK',
'124':'CMD_LOAD_SOURCE',
'125':'CMD_ADD_DJANGO_EXCEPTION_BREAK',
'126':'CMD_REMOVE_DJANGO_EXCEPTION_BREAK',
'127':'CMD_SET_NEXT_STATEMENT',
'128':'CMD_SMART_STEP_INTO',
'101': 'CMD_RUN',
'102': 'CMD_LIST_THREADS',
'103': 'CMD_THREAD_CREATE',
'104': 'CMD_THREAD_KILL',
'105': 'CMD_THREAD_SUSPEND',
'106': 'CMD_THREAD_RUN',
'107': 'CMD_STEP_INTO',
'108': 'CMD_STEP_OVER',
'109': 'CMD_STEP_RETURN',
'110': 'CMD_GET_VARIABLE',
'111': 'CMD_SET_BREAK',
'112': 'CMD_REMOVE_BREAK',
'113': 'CMD_EVALUATE_EXPRESSION',
'114': 'CMD_GET_FRAME',
'115': 'CMD_EXEC_EXPRESSION',
'116': 'CMD_WRITE_TO_CONSOLE',
'117': 'CMD_CHANGE_VARIABLE',
'118': 'CMD_RUN_TO_LINE',
'119': 'CMD_RELOAD_CODE',
'120': 'CMD_GET_COMPLETIONS',
'121': 'CMD_CONSOLE_EXEC',
'122': 'CMD_ADD_EXCEPTION_BREAK',
'123': 'CMD_REMOVE_EXCEPTION_BREAK',
'124': 'CMD_LOAD_SOURCE',
'125': 'CMD_ADD_DJANGO_EXCEPTION_BREAK',
'126': 'CMD_REMOVE_DJANGO_EXCEPTION_BREAK',
'127': 'CMD_SET_NEXT_STATEMENT',
'128': 'CMD_SMART_STEP_INTO',
'129': 'CMD_EXIT',
'130': 'CMD_SIGNATURE_CALL_TRACE',
'131': 'CMD_SET_PY_EXCEPTION',
@@ -184,14 +183,13 @@ ID_TO_MEANING = {
'139': 'CMD_SEND_CURR_EXCEPTION_TRACE_PROCEEDED',
'140': 'CMD_IGNORE_THROWN_EXCEPTION_AT',
'141': 'CMD_ENABLE_DONT_TRACE',
'143': 'CMD_GET_ARRAY',
'144': 'CMD_STEP_INTO_MY_CODE',
'145': 'CMD_GET_CONCURRENCY_EVENT',
'501':'CMD_VERSION',
'502':'CMD_RETURN',
'901':'CMD_ERROR',
'143':'CMD_GET_ARRAY',
'144':'CMD_STEP_INTO_MY_CODE',
'145':'CMD_GET_CONCURRENCY_EVENT',
'501': 'CMD_VERSION',
'502': 'CMD_RETURN',
'901': 'CMD_ERROR',
}
MAX_IO_MSG_SIZE = 1000 #if the io is too big, we'll not send all (could make the debugger too non-responsive)
@@ -13,13 +13,14 @@ from _pydevd_bundle.pydevd_comm import CMD_STEP_CAUGHT_EXCEPTION, CMD_STEP_RETUR
from _pydevd_bundle.pydevd_constants import STATE_SUSPEND, dict_contains, get_thread_id, STATE_RUN, dict_iter_values
from _pydevd_bundle.pydevd_frame_utils import add_exception_to_frame, just_raised
from pydevd_file_utils import get_abs_path_real_path_and_base_from_frame
try:
from inspect import CO_GENERATOR
except:
CO_GENERATOR = 0
try:
from _pydevd_bundle.pydevd_signature import send_signature_call_trace
from _pydevd_bundle.pydevd_signature import send_signature_call_trace, send_signature_return_trace
except ImportError:
def send_signature_call_trace(*args, **kwargs):
pass
@@ -80,6 +81,12 @@ class PyDBFrame: # No longer cdef because object was dying when only a reference
return self.trace_exception
def trace_return(self, frame, event, arg):
if event == 'return':
main_debugger, filename, info, thread = self._args
send_signature_return_trace(main_debugger, frame, filename, arg)
return self.trace_return
# IFDEF CYTHON
# def should_stop_on_exception(self, frame, str event, arg):
# cdef PyDBAdditionalThreadInfo info;
@@ -274,12 +281,15 @@ class PyDBFrame: # No longer cdef because object was dying when only a reference
try:
# print 'frame trace_dispatch', frame.f_lineno, frame.f_code.co_name, event
info.is_tracing = True
need_trace_return = False
if main_debugger._finish_debugging_session:
return None
if event == 'call' and main_debugger.signature_factory:
send_signature_call_trace(main_debugger, frame, filename)
need_trace_return = send_signature_call_trace(main_debugger, frame, filename)
if event == 'return' and main_debugger.signature_factory:
send_signature_return_trace(main_debugger, frame, filename, arg)
plugin_manager = main_debugger.plugin
@@ -341,7 +351,10 @@ class PyDBFrame: # No longer cdef because object was dying when only a reference
if has_exception_breakpoints:
return self.trace_exception
else:
return None
if need_trace_return:
return self.trace_return
else:
return None
else:
#checks the breakpoint to see if there is a context match in some function
@@ -361,7 +374,10 @@ class PyDBFrame: # No longer cdef because object was dying when only a reference
if has_exception_breakpoints:
return self.trace_exception
else:
return None
if need_trace_return:
return self.trace_return
else:
return None
#We may have hit a breakpoint or we are already in step mode. Either way, let's check what we should do in this frame
@@ -19,6 +19,7 @@ class Signature(object):
self.name = name
self.args = []
self.args_str = []
self.return_type = None
def add_arg(self, name, type):
self.args.append((name, type))
@@ -28,32 +29,39 @@ class Signature(object):
return "%s %s(%s)"%(self.file, self.name, ", ".join(self.args_str))
def get_type_of_value(value, ignore_module_name=('__main__', '__builtin__', 'builtins')):
tp = type(value)
class_name = tp.__name__
if class_name == 'instance': # old-style classes
tp = value.__class__
class_name = tp.__name__
if hasattr(tp, '__module__') and tp.__module__ and tp.__module__ not in ignore_module_name:
class_name = "%s.%s"%(tp.__module__, class_name)
return class_name
class SignatureFactory(object):
def __init__(self):
self._caller_cache = {}
self._ignore_module_name = ('__main__', '__builtin__', 'builtins')
self.cache = CallSignatureCache()
def is_in_scope(self, filename):
return not pydevd_utils.not_in_project_roots(filename)
def create_signature(self, frame):
def create_signature(self, frame, with_args=True):
try:
code = frame.f_code
locals = frame.f_locals
filename, modulename, funcname = self.file_module_function_of(frame)
res = Signature(filename, funcname)
for i in xrange(0, code.co_argcount):
name = code.co_varnames[i]
tp = type(locals[name])
class_name = tp.__name__
if class_name == 'instance': # old-style classes
tp = locals[name].__class__
class_name = tp.__name__
if hasattr(tp, '__module__') and tp.__module__ and tp.__module__ not in self._ignore_module_name:
class_name = "%s.%s"%(tp.__module__, class_name)
res.add_arg(name, class_name)
if with_args:
for i in xrange(0, code.co_argcount):
name = code.co_varnames[i]
class_name = get_type_of_value(locals[name])
res.add_arg(name, class_name)
return res
except:
import traceback
@@ -109,6 +117,41 @@ class SignatureFactory(object):
return filename, modulename, funcname
def get_signature_info(signature):
return signature.file, signature.name, ' '.join([arg[1]for arg in signature.args])
def get_frame_info(frame):
co = frame.f_code
return co.co_name, frame.f_lineno, co.co_filename
class CallSignatureCache(object):
def __init__(self):
self.cache = {}
def add(self, signature):
filename, name, args_type = get_signature_info(signature)
if not filename in self.cache:
self.cache[filename] = {}
calls_from_file = self.cache[filename]
if not name in calls_from_file:
calls_from_file[name] = {}
name_calls = calls_from_file[name]
name_calls[args_type] = None
def is_in_cache(self, signature):
filename, name, args_type = get_signature_info(signature)
if filename in self.cache and name in self.cache[filename] and args_type in self.cache[filename][name]:
return True
return False
def create_signature_message(signature):
cmdTextList = ["<xml>"]
@@ -116,14 +159,40 @@ def create_signature_message(signature):
for arg in signature.args:
cmdTextList.append('<arg name="%s" type="%s"></arg>' % (pydevd_vars.make_valid_xml_value(arg[0]), pydevd_vars.make_valid_xml_value(arg[1])))
if signature.return_type is not None:
cmdTextList.append('<return type="%s"></return>' % (pydevd_vars.make_valid_xml_value(signature.return_type)))
cmdTextList.append("</call_signature></xml>")
cmdText = ''.join(cmdTextList)
return NetCommand(CMD_SIGNATURE_CALL_TRACE, 0, cmdText)
def send_signature_call_trace(dbg, frame, filename):
if dbg.signature_factory.is_in_scope(filename):
dbg.writer.add_command(create_signature_message(dbg.signature_factory.create_signature(frame)))
if dbg.signature_factory and dbg.signature_factory.is_in_scope(filename):
signature = dbg.signature_factory.create_signature(frame)
if dbg.signature_factory.cache is not None:
if not dbg.signature_factory.cache.is_in_cache(signature):
dbg.signature_factory.cache.add(signature)
dbg.writer.add_command(create_signature_message(signature))
return True
else:
# we don't send signature if it is cached
return False
else:
dbg.writer.add_command(create_signature_message(signature))
return True
return False
def send_signature_return_trace(dbg, frame, filename, return_value):
if dbg.signature_factory and dbg.signature_factory.is_in_scope(filename):
signature = dbg.signature_factory.create_signature(frame, with_args=False)
signature.return_type = get_type_of_value(return_value)
dbg.writer.add_command(create_signature_message(signature))
return True
return False
@@ -13,6 +13,8 @@ import java.util.List;
public class PySignature {
private final String myFile;
private final String myFunctionName;
private NamedParameter myReturnType = null;
private final List<NamedParameter> myArgs = Lists.newArrayList();
@@ -46,6 +48,22 @@ public class PySignature {
return myArgs;
}
public NamedParameter getReturnType() {
return myReturnType;
}
public PySignature addReturnType(@Nullable String returnType) {
if (StringUtil.isNotEmpty(returnType)) {
if (myReturnType != null) {
myReturnType.addType(returnType);
}
else {
myReturnType = new NamedParameter("", returnType);
}
}
return this;
}
@NotNull
public PySignature addAllArgs(@NotNull PySignature signature) {
for (NamedParameter param : signature.getArgs()) {
@@ -71,6 +89,11 @@ public class PySignature {
return null;
}
@Nullable
public String getReturnTypeQualifiedName() {
return myReturnType != null ? myReturnType.getTypeQualifiedName() : null;
}
public static class NamedParameter {
private final String myName;
@@ -30,6 +30,7 @@ public abstract class AbstractCommand<T> {
public static final int LOAD_SOURCE = 124;
public static final int SMART_STEP_INTO = 128;
public static final int EXIT = 129;
public static final int CALL_SIGNATURE_TRACE = 130;
public static final int CMD_SET_PY_EXCEPTION = 131;
@@ -45,6 +46,9 @@ public abstract class AbstractCommand<T> {
public static final int CMD_ENABLE_DONT_TRACE = 141;
public static final int SHOW_CONSOLE = 142;
public static final int GET_ARRAY = 143;
public static final int STEP_INTO_MY_CODE = 144;
public static final int LOG_CONCURRENCY_EVENT = 145;
public static final int ERROR = 901;
@@ -52,9 +56,7 @@ public abstract class AbstractCommand<T> {
public static final String NEW_LINE_CHAR = "@_@NEW_LINE_CHAR@_@";
public static final String TAB_CHAR = "@_@TAB_CHAR@_@";
public static final int GET_ARRAY = 143;
public static final int STEP_INTO_MY_CODE = 144;
public static final int LOG_CONCURRENCY_EVENT = 145;
@NotNull private final RemoteDebugger myDebugger;
private final int myCommandCode;
@@ -31,10 +31,15 @@ public class ProtocolParser {
while (reader.hasMoreChildren()) {
reader.moveDown();
if (!"arg".equals(reader.getNodeName())) {
throw new PyDebuggerException("Expected <arg>, found " + reader.getNodeName());
if ("arg".equals(reader.getNodeName())) {
signature.addArgument(readString(reader, "name", ""), readString(reader, "type", ""));
} else
if ("return".equals(reader.getNodeName())) {
signature.addReturnType(readString(reader, "type", ""));
} else {
throw new PyDebuggerException("Expected <arg> or <return>, found " + reader.getNodeName());
}
signature.addArgument(readString(reader, "name", ""), readString(reader, "type", ""));
reader.moveUp();
}
@@ -16,6 +16,7 @@
package com.jetbrains.python.debugger;
import com.intellij.openapi.util.Ref;
import com.jetbrains.python.psi.PyCallable;
import com.jetbrains.python.psi.PyFunction;
import com.jetbrains.python.psi.PyNamedParameter;
import com.jetbrains.python.psi.types.*;
@@ -25,6 +26,7 @@ import org.jetbrains.annotations.NotNull;
* @author traff
*/
public class PyCallSignatureTypeProvider extends PyTypeProviderBase {
@Override
public Ref<PyType> getParameterType(@NotNull final PyNamedParameter param,
@NotNull final PyFunction func,
@NotNull TypeEvalContext context) {
@@ -34,8 +36,25 @@ public class PyCallSignatureTypeProvider extends PyTypeProviderBase {
if (typeName != null) {
final PyType type = PyTypeParser.getTypeByName(param, typeName);
if (type != null) {
final PyType evaluatedType = PyDynamicallyEvaluatedType.create(type);
return Ref.create(evaluatedType);
return Ref.create(PyDynamicallyEvaluatedType.create(type));
}
}
}
return null;
}
@Override
public Ref<PyType> getReturnType(@NotNull final PyCallable callable, @NotNull TypeEvalContext context) {
if (callable instanceof PyFunction) {
PyFunction function = (PyFunction)callable;
PySignature signature = PySignatureCacheManager.getInstance(function.getProject()).findSignature(function);
if (signature != null) {
final String typeName = signature.getReturnType().getTypeQualifiedName();
if (typeName != null) {
final PyType type = PyTypeParser.getTypeByName(function, typeName);
if (type != null) {
return Ref.create(PyDynamicallyEvaluatedType.create(type));
}
}
}
}
@@ -104,7 +104,7 @@ public class PySignatureCacheManagerImpl extends PySignatureCacheManager {
else {
//noinspection ConstantConditions
lines[i] = signatureToString(stringToSignature(file.
getCanonicalPath(), lines[i]).addAllArgs(signature));
getCanonicalPath(), lines[i]).addAllArgs(signature).addReturnType(signature.getReturnTypeQualifiedName()));
}
}
i++;
@@ -140,7 +140,9 @@ public class PySignatureCacheManagerImpl extends PySignatureCacheManager {
}
private static String signatureToString(PySignature signature) {
return signature.getFunctionName() + "\t" + StringUtil.join(arguments(signature), "\t");
return signature.getFunctionName() + "\t" + StringUtil.join(arguments(signature), "\t") +
(signature.getReturnType() != null
? "\t" + StringUtil.join(signature.getReturnType().getTypesList(), "\t") : "");
}
private static List<String> arguments(PySignature signature) {
@@ -248,8 +250,11 @@ public class PySignatureCacheManagerImpl extends PySignatureCacheManager {
if (var.length == 2) {
signature = signature.addArgument(var[0], var[1]);
}
else if (var.length == 1) {
signature = signature.addReturnType(var[0]);
}
else {
throw new IllegalStateException("Should be <name>:<type> format. " + parts[i] + " instead.");
throw new IllegalStateException("Should be <name>:<type> format for arg or <type> for return type; '" + parts[i] + "' instead.");
}
}
return signature;
@@ -259,7 +264,7 @@ public class PySignatureCacheManagerImpl extends PySignatureCacheManager {
@Nullable
private static VirtualFile getFile(@NotNull PySignature signature) {
return LocalFileSystem.getInstance().findFileByPath(signature.getFile());
return LocalFileSystem.getInstance().refreshAndFindFileByPath(signature.getFile());
}
@Nullable
@@ -291,7 +296,7 @@ public class PySignatureCacheManagerImpl extends PySignatureCacheManager {
}
});
}
}, "Cleaning the cache of dynamically collected types", true, myProject);
}, "Cleaning the Cache of Dynamically Collected Types", true, myProject);
String message;
@@ -275,7 +275,8 @@ public class PyDocstringGenerator {
if (param.getType() == null) {
String type;
if (signature != null) {
type = StringUtil.notNullize(signature.getArgTypeQualifiedName(param.getName()));
type = StringUtil.notNullize(param.isReturnValue() ? signature.getReturnTypeQualifiedName() :
signature.getArgTypeQualifiedName(param.getName()));
}
else {
type = paramTypes.get(paramCoordinates);
@@ -15,7 +15,6 @@
*/
package com.jetbrains.python.psi.types;
import com.intellij.openapi.project.Project;
import com.jetbrains.python.PyNames;
import org.jetbrains.annotations.NotNull;
+4
View File
@@ -0,0 +1,4 @@
def foo(x):
return x
foo(1)
+11
View File
@@ -0,0 +1,11 @@
def foo(x):
"""
:param x:
:type x: int
:return:
:rtype: int
"""
return x
foo(1)
@@ -0,0 +1,88 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.env.python;
import com.google.common.collect.Lists;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.testFramework.EditorTestUtil;
import com.jetbrains.env.PyEnvTestCase;
import com.jetbrains.env.python.debug.PyDebuggerTask;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.debugger.PyDebuggerOptionsProvider;
import com.jetbrains.python.debugger.PySignatureCacheManagerImpl;
import java.io.IOException;
/**
* @author traff
*/
public class PyDynamicTypesTest extends PyEnvTestCase {
public void test1() throws Exception {
doTest(getTestName(true) + ".py");
}
private void doTest(final String scriptName) {
runPythonTest(new PyDebuggerTask("/" + "dynamicTypes", scriptName) {
@Override
public void setUp(String testName) throws Exception {
super.setUp(testName);
}
@Override
public void before() throws Exception {
PyDebuggerOptionsProvider.getInstance(myFixture.getProject()).setSaveCallSignatures(true);
}
@Override
public void after() throws Exception {
PyDebuggerOptionsProvider.getInstance(myFixture.getProject()).setSaveCallSignatures(false);
}
@Override
public void testing() throws Exception {
waitForTerminate();
edt(() -> {
myFixture.configureByFile("dynamicTypes/" + scriptName);
try {
//copy signature attributes from real file to temporary test file
PySignatureCacheManagerImpl.CALL_SIGNATURES_ATTRIBUTE.writeAttributeBytes(myFixture.getFile().getVirtualFile(),
PySignatureCacheManagerImpl.CALL_SIGNATURES_ATTRIBUTE
.readAttributeBytes(LocalFileSystem.getInstance()
.refreshAndFindFileByPath(
getTestDataPath() +
"/dynamicTypes/" +
scriptName)));
}
catch (IOException e) {
throw new RuntimeException(e);
}
EditorTestUtil.setCaretsAndSelection(myFixture.getEditor(), new EditorTestUtil.CaretAndSelectionState(
Lists.newArrayList(new EditorTestUtil.CaretInfo(new LogicalPosition(0, 6), null)), null));
final IntentionAction action = myFixture.findSingleIntention(PyBundle.message("INTN.doc.string.stub"));
myFixture.launchAction(action);
myFixture.checkResultByFile("dynamicTypes/" + getTestName(true) + "_after.py");
});
}
});
}
}