1import inspect
2import os.path
3import sys
4
5from _pydev_bundle._pydev_tipper_common import do_find
6from _pydevd_bundle.pydevd_constants import IS_PY2
7from _pydevd_bundle.pydevd_resolver import suppress_warnings
8
9if IS_PY2:
10    from inspect import getargspec as _originalgetargspec
11    def getargspec(*args, **kwargs):
12        ret = list(_originalgetargspec(*args, **kwargs))
13        ret.append([])
14        ret.append({})
15        return ret
16
17else:
18    from inspect import getfullargspec
19
20    def getargspec(*args, **kwargs):
21        arg_spec = getfullargspec(*args, **kwargs)
22        return arg_spec.args, arg_spec.varargs, arg_spec.varkw, arg_spec.defaults, arg_spec.kwonlyargs or [], arg_spec.kwonlydefaults or {}
23
24try:
25    xrange
26except:
27    xrange = range
28
29#completion types.
30TYPE_IMPORT = '0'
31TYPE_CLASS = '1'
32TYPE_FUNCTION = '2'
33TYPE_ATTR = '3'
34TYPE_BUILTIN = '4'
35TYPE_PARAM = '5'
36
37# completion types for IPython console
38TYPE_IPYTHON = '11'
39TYPE_IPYTHON_MAGIC = '12'
40
41def _imp(name, log=None):
42    try:
43        return __import__(name)
44    except:
45        if '.' in name:
46            sub = name[0:name.rfind('.')]
47
48            if log is not None:
49                log.add_content('Unable to import', name, 'trying with', sub)
50                log.add_exception()
51
52            return _imp(sub, log)
53        else:
54            s = 'Unable to import module: %s - sys.path: %s' % (str(name), sys.path)
55            if log is not None:
56                log.add_content(s)
57                log.add_exception()
58
59            raise ImportError(s)
60
61
62IS_IPY = False
63if sys.platform == 'cli':
64    IS_IPY = True
65    _old_imp = _imp
66    def _imp(name, log=None):
67        #We must add a reference in clr for .Net
68        import clr #@UnresolvedImport
69        initial_name = name
70        while '.' in name:
71            try:
72                clr.AddReference(name)
73                break #If it worked, that's OK.
74            except:
75                name = name[0:name.rfind('.')]
76        else:
77            try:
78                clr.AddReference(name)
79            except:
80                pass #That's OK (not dot net module).
81
82        return _old_imp(initial_name, log)
83
84
85
86def get_file(mod):
87    f = None
88    try:
89        f = inspect.getsourcefile(mod) or inspect.getfile(mod)
90    except:
91        if hasattr(mod, '__file__'):
92            f = mod.__file__
93            if f.lower(f[-4:]) in ['.pyc', '.pyo']:
94                filename = f[:-4] + '.py'
95                if os.path.exists(filename):
96                    f = filename
97
98    return f
99
100def Find(name, log=None):
101    f = None
102
103    mod = _imp(name, log)
104    parent = mod
105    foundAs = ''
106
107    if inspect.ismodule(mod):
108        f = get_file(mod)
109
110    components = name.split('.')
111
112    old_comp = None
113    for comp in components[1:]:
114        try:
115            #this happens in the following case:
116            #we have mx.DateTime.mxDateTime.mxDateTime.pyd
117            #but after importing it, mx.DateTime.mxDateTime shadows access to mxDateTime.pyd
118            mod = getattr(mod, comp)
119        except AttributeError:
120            if old_comp != comp:
121                raise
122
123        if inspect.ismodule(mod):
124            f = get_file(mod)
125        else:
126            if len(foundAs) > 0:
127                foundAs = foundAs + '.'
128            foundAs = foundAs + comp
129
130        old_comp = comp
131
132    return f, mod, parent, foundAs
133
134def search_definition(data):
135    '''@return file, line, col
136    '''
137
138    data = data.replace('\n', '')
139    if data.endswith('.'):
140        data = data.rstrip('.')
141    f, mod, parent, foundAs = Find(data)
142    try:
143        return do_find(f, mod), foundAs
144    except:
145        return do_find(f, parent), foundAs
146
147
148def generate_tip(data, log=None):
149    data = data.replace('\n', '')
150    if data.endswith('.'):
151        data = data.rstrip('.')
152
153    f, mod, parent, foundAs = Find(data, log)
154    #print_ >> open('temp.txt', 'w'), f
155    tips = generate_imports_tip_for_module(mod)
156    return f, tips
157
158
159def check_char(c):
160    if c == '-' or c == '.':
161        return '_'
162    return c
163
164_SENTINEL = object()
165
166def generate_imports_tip_for_module(obj_to_complete, dir_comps=None, getattr=getattr, filter=lambda name:True):
167    '''
168        @param obj_to_complete: the object from where we should get the completions
169        @param dir_comps: if passed, we should not 'dir' the object and should just iterate those passed as kwonly_arg parameter
170        @param getattr: the way to get kwonly_arg given object from the obj_to_complete (used for the completer)
171        @param filter: kwonly_arg callable that receives the name and decides if it should be appended or not to the results
172        @return: list of tuples, so that each tuple represents kwonly_arg completion with:
173            name, doc, args, type (from the TYPE_* constants)
174    '''
175    ret = []
176
177    if dir_comps is None:
178        dir_comps = dir(obj_to_complete)
179        if hasattr(obj_to_complete, '__dict__'):
180            dir_comps.append('__dict__')
181        if hasattr(obj_to_complete, '__class__'):
182            dir_comps.append('__class__')
183        # Fix for PY-38151 - From python doc, metaclass attributes are not in the list returned by `dir`.
184        # This is why e.g. __name__ does not appear. Let's add the metaclass attributes here.
185        try:
186            if inspect.isclass(obj_to_complete):
187                dir_comps += dir(type(obj_to_complete))
188            # do not do it from instance, it might grab irrelevant items
189        except:
190            # ignore any error just in case
191            pass
192
193    get_complete_info = True
194
195    if len(dir_comps) > 1000:
196        #ok, we don't want to let our users wait forever...
197        #no complete info for you...
198
199        get_complete_info = False
200
201    dontGetDocsOn = (float, int, str, tuple, list)
202    for d in dir_comps:
203
204        if d is None:
205            continue
206
207        if not filter(d):
208            continue
209
210        args = ''
211
212        try:
213            with suppress_warnings():
214                try:
215                    obj = getattr(obj_to_complete.__class__, d)
216                except:
217                    obj = getattr(obj_to_complete, d)
218        except: #just ignore and get it without additional info
219            ret.append((d, '', args, TYPE_BUILTIN))
220        else:
221
222            if get_complete_info:
223                try:
224                    retType = TYPE_BUILTIN
225
226                    #check if we have to get docs
227                    getDoc = True
228                    for class_ in dontGetDocsOn:
229
230                        if isinstance(obj, class_):
231                            getDoc = False
232                            break
233
234                    doc = ''
235                    if getDoc:
236                        #no need to get this info... too many constants are defined and
237                        #makes things much slower (passing all that through sockets takes quite some time)
238                        try:
239                            doc = inspect.getdoc(obj)
240                            if doc is None:
241                                doc = ''
242                        except: #may happen on jython when checking java classes (so, just ignore it)
243                            doc = ''
244
245                    if inspect.ismethoddescriptor(obj) or inspect.isdatadescriptor(obj) \
246                            or inspect.isgetsetdescriptor(obj) or inspect.ismemberdescriptor(obj):
247                        # Fix for PY-38151: `obj` is a descriptor definition, not a called descriptor
248                        # (`obj_to_complete` is the class defining it).
249                        retType = TYPE_ATTR
250
251                    elif inspect.ismethod(obj) or inspect.isbuiltin(obj) or inspect.isfunction(obj) or inspect.isroutine(obj):
252                        try:
253                            args, vargs, kwargs, defaults, kwonly_args, kwonly_defaults = getargspec(obj)
254
255                            args = args[:]
256
257                            for kwonly_arg in kwonly_args:
258                                default = kwonly_defaults.get(kwonly_arg, _SENTINEL)
259                                if default is not _SENTINEL:
260                                    args.append('%s=%s' % (kwonly_arg, default))
261                                else:
262                                    args.append(str(kwonly_arg))
263
264                            args = '(%s)' % (', '.join(args))
265                        except TypeError:
266                            #ok, let's see if we can get the arguments from the doc
267                            args, doc = signature_from_docstring(doc, getattr(obj, '__name__', None))
268
269                        retType = TYPE_FUNCTION
270
271                    elif inspect.isclass(obj):
272                        retType = TYPE_CLASS
273
274                    elif inspect.ismodule(obj):
275                        retType = TYPE_IMPORT
276
277                    else:
278                        retType = TYPE_ATTR
279
280
281                    #add token and doc to return - assure only strings.
282                    ret.append((d, doc, args, retType))
283
284                except: #just ignore and get it without aditional info
285                    ret.append((d, '', args, TYPE_BUILTIN))
286
287            else: #get_complete_info == False
288                if inspect.ismethod(obj) or inspect.isbuiltin(obj) or inspect.isfunction(obj) or inspect.isroutine(obj):
289                    retType = TYPE_FUNCTION
290
291                elif inspect.isclass(obj):
292                    retType = TYPE_CLASS
293
294                elif inspect.ismodule(obj):
295                    retType = TYPE_IMPORT
296
297                else:
298                    retType = TYPE_ATTR
299                #ok, no complete info, let's try to do this as fast and clean as possible
300                #so, no docs for this kind of information, only the signatures
301                ret.append((d, '', str(args), retType))
302
303    return ret
304
305
306def signature_from_docstring(doc, obj_name):
307    args = '()'
308    try:
309        found = False
310        if len(doc) > 0:
311            if IS_IPY:
312                # Handle case where we have the situation below
313                # sort(self, object cmp, object key)
314                # sort(self, object cmp, object key, bool reverse)
315                # sort(self)
316                # sort(self, object cmp)
317
318                # Or: sort(self: list, cmp: object, key: object)
319                # sort(self: list, cmp: object, key: object, reverse: bool)
320                # sort(self: list)
321                # sort(self: list, cmp: object)
322                if obj_name:
323                    name = obj_name + '('
324
325                    # Fix issue where it was appearing sort(aa)sort(bb)sort(cc) in the same line.
326                    lines = doc.splitlines()
327                    if len(lines) == 1:
328                        c = doc.count(name)
329                        if c > 1:
330                            doc = ('\n' + name).join(doc.split(name))
331
332                    major = ''
333                    for line in doc.splitlines():
334                        if line.startswith(name) and line.endswith(')'):
335                            if len(line) > len(major):
336                                major = line
337                    if major:
338                        args = major[major.index('('):]
339                        found = True
340
341            if not found:
342                i = doc.find('->')
343                if i < 0:
344                    i = doc.find('--')
345                    if i < 0:
346                        i = doc.find('\n')
347                        if i < 0:
348                            i = doc.find('\r')
349
350                if i > 0:
351                    s = doc[0:i]
352                    s = s.strip()
353
354                    # let's see if we have a docstring in the first line
355                    if s[-1] == ')':
356                        start = s.find('(')
357                        if start >= 0:
358                            end = s.find('[')
359                            if end <= 0:
360                                end = s.find(')')
361                                if end <= 0:
362                                    end = len(s)
363
364                            args = s[start:end]
365                            if not args[-1] == ')':
366                                args = args + ')'
367
368                            # now, get rid of unwanted chars
369                            l = len(args) - 1
370                            r = []
371                            for i in xrange(len(args)):
372                                if i == 0 or i == l:
373                                    r.append(args[i])
374                                else:
375                                    r.append(check_char(args[i]))
376
377                            args = ''.join(r)
378
379            if IS_IPY:
380                if args.startswith('(self:'):
381                    i = args.find(',')
382                    if i >= 0:
383                        args = '(self' + args[i:]
384                    else:
385                        args = '(self)'
386                i = args.find(')')
387                if i > 0:
388                    args = args[:i + 1]
389
390    except:
391        pass
392    return args, doc
393