pep8 edits and avoid naming conflicts with python builtins
[blender.git] / release / scripts / modules / console / complete_calltip.py
1 # Copyright (c) 2009 www.stani.be (GPL license)
2
3 # ##### BEGIN GPL LICENSE BLOCK #####
4 #
5 #  This program is free software; you can redistribute it and/or
6 #  modify it under the terms of the GNU General Public License
7 #  as published by the Free Software Foundation; either version 2
8 #  of the License, or (at your option) any later version.
9 #
10 #  This program is distributed in the hope that it will be useful,
11 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #  GNU General Public License for more details.
14 #
15 #  You should have received a copy of the GNU General Public License
16 #  along with this program; if not, write to the Free Software Foundation,
17 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 #
19 # ##### END GPL LICENSE BLOCK #####
20
21 # <pep8-80 compliant>
22
23 import inspect
24 import re
25
26
27 # regular expression constants
28 DEF_DOC = '%s\s*(\(.*?\))'
29 DEF_SOURCE = 'def\s+%s\s*(\(.*?\)):'
30 RE_EMPTY_LINE = re.compile('^\s*\n')
31 RE_FLAG = re.MULTILINE | re.DOTALL
32 RE_NEWLINE = re.compile('\n+')
33 RE_SPACE = re.compile('\s+')
34 RE_DEF_COMPLETE = re.compile(
35     # don't start with a quote
36     '''(?:^|[^"'a-zA-Z0-9_])'''
37     # start with a \w = [a-zA-Z0-9_]
38     '''((\w+'''
39     # allow also dots and closed bracket pairs []
40     '''(?:\w|[.]|\[.+?\])*'''
41     # allow empty string
42     '''|)'''
43     # allow opening bracket(s)
44     '''(?:\(|\s)*)$''')
45
46
47 def reduce_newlines(text):
48     """Reduces multiple newlines to a single newline.
49
50     :param text: text with multiple newlines
51     :type text: str
52     :returns: text with single newlines
53     :rtype: str
54
55     >>> reduce_newlines('hello\\n\\nworld')
56     'hello\\nworld'
57     """
58     return RE_NEWLINE.sub('\n', text)
59
60
61 def reduce_spaces(text):
62     """Reduces multiple whitespaces to a single space.
63
64     :param text: text with multiple spaces
65     :type text: str
66     :returns: text with single spaces
67     :rtype: str
68
69     >>> reduce_spaces('hello    \\nworld')
70     'hello world'
71     """
72     return RE_SPACE.sub(' ', text)
73
74
75 def get_doc(obj):
76     """Get the doc string or comments for an object.
77
78     :param object: object
79     :returns: doc string
80     :rtype: str
81
82     >>> get_doc(abs)
83     'abs(number) -> number\\n\\nReturn the absolute value of the argument.'
84     """
85     result = inspect.getdoc(obj) or inspect.getcomments(obj)
86     return result and RE_EMPTY_LINE.sub('', result.rstrip()) or ''
87
88
89 def get_argspec(func, strip_self=True, doc=None, source=None):
90     """Get argument specifications.
91
92     :param strip_self: strip `self` from argspec
93     :type strip_self: bool
94     :param doc: doc string of func (optional)
95     :type doc: str
96     :param source: source code of func (optional)
97     :type source: str
98     :returns: argument specification
99     :rtype: str
100
101     >>> get_argspec(inspect.getclasstree)
102     '(classes, unique=0)'
103     >>> get_argspec(abs)
104     '(number)'
105     """
106     # get the function object of the class
107     try:
108         func = func.__func__
109     except AttributeError:
110         try:
111             # py 2.X
112             func = func.im_func
113         except AttributeError:
114             pass
115     # is callable?
116     if not hasattr(func, '__call__'):
117         return ''
118     # func should have a name
119     try:
120         func_name = func.__name__
121     except AttributeError:
122         return ''
123     # from docstring
124     if doc is None:
125         doc = get_doc(func)
126     match = re.search(DEF_DOC % func_name, doc, RE_FLAG)
127     # from source code
128     if not match:
129         if source is None:
130             try:
131                 source = inspect.getsource(func)
132             except (TypeError, IOError):
133                 source = ''
134         if source:
135             match = re.search(DEF_SOURCE % func_name, source, RE_FLAG)
136     if match:
137         argspec = reduce_spaces(match.group(1))
138     else:
139         # try with the inspect.getarg* functions
140         try:
141             argspec = inspect.formatargspec(*inspect.getfullargspec(func))
142         except:
143             try:
144                 # py 2.X
145                 argspec = inspect.formatargspec(*inspect.getargspec(func))
146             except:
147                 try:
148                     argspec = inspect.formatargvalues(
149                         *inspect.getargvalues(func))
150                 except:
151                     argspec = ''
152         if strip_self:
153             argspec = argspec.replace('self, ', '')
154     return argspec
155
156
157 def complete(line, cursor, namespace):
158     """Complete callable with calltip.
159
160     :param line: incomplete text line
161     :type line: str
162     :param cursor: current character position
163     :type cursor: int
164     :param namespace: namespace
165     :type namespace: dict
166     :returns: (matches, world, scrollback)
167     :rtype: (list of str, str, str)
168
169     >>> import os
170     >>> complete('os.path.isdir(', 14, {'os': os})[-1]
171     'isdir(s)\\nReturn true if the pathname refers to an existing directory.'
172     >>> complete('abs(', 4, {})[-1]
173     'abs(number) -> number\\nReturn the absolute value of the argument.'
174     """
175     matches = []
176     word = ''
177     scrollback = ''
178     match = RE_DEF_COMPLETE.search(line[:cursor])
179
180     if match:
181         word = match.group(1)
182         func_word = match.group(2)
183         try:
184             func = eval(func_word, namespace)
185         except Exception:
186             func = None
187
188         if func:
189             doc = get_doc(func)
190             argspec = get_argspec(func, doc=doc)
191             scrollback = func_word.split('.')[-1] + (argspec or '()')
192             if doc.startswith(scrollback):
193                 scrollback = doc
194             elif doc:
195                 scrollback += '\n' + doc
196             scrollback = reduce_newlines(scrollback)
197
198     return matches, word, scrollback