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