You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

373 lines
12 KiB
Python

import inspect
import os.path
import sys
from _pydev_bundle._pydev_tipper_common import do_find
from _pydevd_bundle.pydevd_utils import hasattr_checked, dir_checked
from inspect import getfullargspec
def getargspec(*args, **kwargs):
arg_spec = getfullargspec(*args, **kwargs)
return arg_spec.args, arg_spec.varargs, arg_spec.varkw, arg_spec.defaults, arg_spec.kwonlyargs or [], arg_spec.kwonlydefaults or {}
# completion types.
TYPE_IMPORT = "0"
TYPE_CLASS = "1"
TYPE_FUNCTION = "2"
TYPE_ATTR = "3"
TYPE_BUILTIN = "4"
TYPE_PARAM = "5"
def _imp(name, log=None):
try:
return __import__(name)
except:
if "." in name:
sub = name[0 : name.rfind(".")]
if log is not None:
log.add_content("Unable to import", name, "trying with", sub)
log.add_exception()
return _imp(sub, log)
else:
s = "Unable to import module: %s - sys.path: %s" % (str(name), sys.path)
if log is not None:
log.add_content(s)
log.add_exception()
raise ImportError(s)
IS_IPY = False
if sys.platform == "cli":
IS_IPY = True
_old_imp = _imp
def _imp(name, log=None):
# We must add a reference in clr for .Net
import clr # @UnresolvedImport
initial_name = name
while "." in name:
try:
clr.AddReference(name)
break # If it worked, that's OK.
except:
name = name[0 : name.rfind(".")]
else:
try:
clr.AddReference(name)
except:
pass # That's OK (not dot net module).
return _old_imp(initial_name, log)
def get_file(mod):
f = None
try:
f = inspect.getsourcefile(mod) or inspect.getfile(mod)
except:
try:
f = getattr(mod, "__file__", None)
except:
f = None
if f and f.lower(f[-4:]) in [".pyc", ".pyo"]:
filename = f[:-4] + ".py"
if os.path.exists(filename):
f = filename
return f
def Find(name, log=None):
f = None
mod = _imp(name, log)
parent = mod
foundAs = ""
if inspect.ismodule(mod):
f = get_file(mod)
components = name.split(".")
old_comp = None
for comp in components[1:]:
try:
# this happens in the following case:
# we have mx.DateTime.mxDateTime.mxDateTime.pyd
# but after importing it, mx.DateTime.mxDateTime shadows access to mxDateTime.pyd
mod = getattr(mod, comp)
except AttributeError:
if old_comp != comp:
raise
if inspect.ismodule(mod):
f = get_file(mod)
else:
if len(foundAs) > 0:
foundAs = foundAs + "."
foundAs = foundAs + comp
old_comp = comp
return f, mod, parent, foundAs
def search_definition(data):
"""@return file, line, col"""
data = data.replace("\n", "")
if data.endswith("."):
data = data.rstrip(".")
f, mod, parent, foundAs = Find(data)
try:
return do_find(f, mod), foundAs
except:
return do_find(f, parent), foundAs
def generate_tip(data, log=None):
data = data.replace("\n", "")
if data.endswith("."):
data = data.rstrip(".")
f, mod, parent, foundAs = Find(data, log)
# print_ >> open('temp.txt', 'w'), f
tips = generate_imports_tip_for_module(mod)
return f, tips
def check_char(c):
if c == "-" or c == ".":
return "_"
return c
_SENTINEL = object()
def generate_imports_tip_for_module(obj_to_complete, dir_comps=None, getattr=getattr, filter=lambda name: True):
"""
@param obj_to_complete: the object from where we should get the completions
@param dir_comps: if passed, we should not 'dir' the object and should just iterate those passed as kwonly_arg parameter
@param getattr: the way to get kwonly_arg given object from the obj_to_complete (used for the completer)
@param filter: kwonly_arg callable that receives the name and decides if it should be appended or not to the results
@return: list of tuples, so that each tuple represents kwonly_arg completion with:
name, doc, args, type (from the TYPE_* constants)
"""
ret = []
if dir_comps is None:
dir_comps = dir_checked(obj_to_complete)
if hasattr_checked(obj_to_complete, "__dict__"):
dir_comps.append("__dict__")
if hasattr_checked(obj_to_complete, "__class__"):
dir_comps.append("__class__")
get_complete_info = True
if len(dir_comps) > 1000:
# ok, we don't want to let our users wait forever...
# no complete info for you...
get_complete_info = False
dontGetDocsOn = (float, int, str, tuple, list, dict)
dontGetattrOn = (dict, list, set, tuple)
for d in dir_comps:
if d is None:
continue
if not filter(d):
continue
args = ""
try:
try:
if isinstance(obj_to_complete, dontGetattrOn):
raise Exception(
'Since python 3.9, e.g. "dict[str]" will return'
" a dict that's only supposed to take strings. "
'Interestingly, e.g. dict["val"] is also valid '
"and presumably represents a dict that only takes "
'keys that are "val". This breaks our check for '
"class attributes."
)
obj = getattr(obj_to_complete.__class__, d)
except:
obj = getattr(obj_to_complete, d)
except: # just ignore and get it without additional info
ret.append((d, "", args, TYPE_BUILTIN))
else:
if get_complete_info:
try:
retType = TYPE_BUILTIN
# check if we have to get docs
getDoc = True
for class_ in dontGetDocsOn:
if isinstance(obj, class_):
getDoc = False
break
doc = ""
if getDoc:
# no need to get this info... too many constants are defined and
# makes things much slower (passing all that through sockets takes quite some time)
try:
doc = inspect.getdoc(obj)
if doc is None:
doc = ""
except: # may happen on jython when checking java classes (so, just ignore it)
doc = ""
if inspect.ismethod(obj) or inspect.isbuiltin(obj) or inspect.isfunction(obj) or inspect.isroutine(obj):
try:
args, vargs, kwargs, defaults, kwonly_args, kwonly_defaults = getargspec(obj)
args = args[:]
for kwonly_arg in kwonly_args:
default = kwonly_defaults.get(kwonly_arg, _SENTINEL)
if default is not _SENTINEL:
args.append("%s=%s" % (kwonly_arg, default))
else:
args.append(str(kwonly_arg))
args = "(%s)" % (", ".join(args))
except TypeError:
# ok, let's see if we can get the arguments from the doc
args, doc = signature_from_docstring(doc, getattr(obj, "__name__", None))
retType = TYPE_FUNCTION
elif inspect.isclass(obj):
retType = TYPE_CLASS
elif inspect.ismodule(obj):
retType = TYPE_IMPORT
else:
retType = TYPE_ATTR
# add token and doc to return - assure only strings.
ret.append((d, doc, args, retType))
except: # just ignore and get it without aditional info
ret.append((d, "", args, TYPE_BUILTIN))
else: # get_complete_info == False
if inspect.ismethod(obj) or inspect.isbuiltin(obj) or inspect.isfunction(obj) or inspect.isroutine(obj):
retType = TYPE_FUNCTION
elif inspect.isclass(obj):
retType = TYPE_CLASS
elif inspect.ismodule(obj):
retType = TYPE_IMPORT
else:
retType = TYPE_ATTR
# ok, no complete info, let's try to do this as fast and clean as possible
# so, no docs for this kind of information, only the signatures
ret.append((d, "", str(args), retType))
return ret
def signature_from_docstring(doc, obj_name):
args = "()"
try:
found = False
if len(doc) > 0:
if IS_IPY:
# Handle case where we have the situation below
# sort(self, object cmp, object key)
# sort(self, object cmp, object key, bool reverse)
# sort(self)
# sort(self, object cmp)
# Or: sort(self: list, cmp: object, key: object)
# sort(self: list, cmp: object, key: object, reverse: bool)
# sort(self: list)
# sort(self: list, cmp: object)
if obj_name:
name = obj_name + "("
# Fix issue where it was appearing sort(aa)sort(bb)sort(cc) in the same line.
lines = doc.splitlines()
if len(lines) == 1:
c = doc.count(name)
if c > 1:
doc = ("\n" + name).join(doc.split(name))
major = ""
for line in doc.splitlines():
if line.startswith(name) and line.endswith(")"):
if len(line) > len(major):
major = line
if major:
args = major[major.index("(") :]
found = True
if not found:
i = doc.find("->")
if i < 0:
i = doc.find("--")
if i < 0:
i = doc.find("\n")
if i < 0:
i = doc.find("\r")
if i > 0:
s = doc[0:i]
s = s.strip()
# let's see if we have a docstring in the first line
if s[-1] == ")":
start = s.find("(")
if start >= 0:
end = s.find("[")
if end <= 0:
end = s.find(")")
if end <= 0:
end = len(s)
args = s[start:end]
if not args[-1] == ")":
args = args + ")"
# now, get rid of unwanted chars
l = len(args) - 1
r = []
for i in range(len(args)):
if i == 0 or i == l:
r.append(args[i])
else:
r.append(check_char(args[i]))
args = "".join(r)
if IS_IPY:
if args.startswith("(self:"):
i = args.find(",")
if i >= 0:
args = "(self" + args[i:]
else:
args = "(self)"
i = args.find(")")
if i > 0:
args = args[: i + 1]
except:
pass
return args, doc