191 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			191 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # Copyright (c) 2009 www.stani.be (GPL license)
 | |
| 
 | |
| # This program is free software: you can redistribute it and/or modify
 | |
| # it under the terms of the GNU Lesser General Public License as published by
 | |
| # the Free Software Foundation, either version 3 of the License, or
 | |
| # (at your option) any later version.
 | |
| #
 | |
| # This program is distributed in the hope that it will be useful,
 | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | |
| # GNU Lesser General Public License for more details.
 | |
| #
 | |
| # You should have received a copy of the GNU Lesser General Public License
 | |
| # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | |
| 
 | |
| import inspect
 | |
| import re
 | |
| 
 | |
| 
 | |
| # regular expression constants
 | |
| DEF_DOC = '%s\s*(\(.*?\))'
 | |
| DEF_SOURCE = 'def\s+%s\s*(\(.*?\)):'
 | |
| RE_EMPTY_LINE = re.compile('^\s*\n')
 | |
| RE_FLAG = re.MULTILINE | re.DOTALL
 | |
| RE_NEWLINE = re.compile('\n+')
 | |
| RE_SPACE = re.compile('\s+')
 | |
| RE_DEF_COMPLETE = re.compile(
 | |
|     # don't start with a quote
 | |
|     '''(?:^|[^"'a-zA-Z0-9_])'''
 | |
|     # start with a \w = [a-zA-Z0-9_]
 | |
|     '''((\w+'''
 | |
|     # allow also dots and closed bracket pairs []
 | |
|     '''(?:\w|[.]|\[.+?\])*'''
 | |
|     # allow empty string
 | |
|     '''|)'''
 | |
|     # allow opening bracket(s)
 | |
|     '''(?:\(|\s)*)$''')
 | |
| 
 | |
| 
 | |
| def reduce_newlines(text):
 | |
|     """Reduces multiple newlines to a single newline.
 | |
| 
 | |
|     :param text: text with multiple newlines
 | |
|     :type text: str
 | |
|     :returns: text with single newlines
 | |
|     :rtype: str
 | |
| 
 | |
|     >>> reduce_newlines('hello\\n\\nworld')
 | |
|     'hello\\nworld'
 | |
|     """
 | |
|     return RE_NEWLINE.sub('\n', text)
 | |
| 
 | |
| 
 | |
| def reduce_spaces(text):
 | |
|     """Reduces multiple whitespaces to a single space.
 | |
| 
 | |
|     :param text: text with multiple spaces
 | |
|     :type text: str
 | |
|     :returns: text with single spaces
 | |
|     :rtype: str
 | |
| 
 | |
|     >>> reduce_spaces('hello    \\nworld')
 | |
|     'hello world'
 | |
|     """
 | |
|     return RE_SPACE.sub(' ', text)
 | |
| 
 | |
| 
 | |
| def get_doc(object):
 | |
|     """Get the doc string or comments for an object.
 | |
| 
 | |
|     :param object: object
 | |
|     :returns: doc string
 | |
|     :rtype: str
 | |
| 
 | |
|     >>> get_doc(abs)
 | |
|     'abs(number) -> number\\n\\nReturn the absolute value of the argument.'
 | |
|     """
 | |
|     result = inspect.getdoc(object) or inspect.getcomments(object)
 | |
|     return result and RE_EMPTY_LINE.sub('', result.rstrip()) or ''
 | |
| 
 | |
| 
 | |
| def get_argspec(func, strip_self=True, doc=None, source=None):
 | |
|     """Get argument specifications.
 | |
| 
 | |
|     :param strip_self: strip `self` from argspec
 | |
|     :type strip_self: bool
 | |
|     :param doc: doc string of func (optional)
 | |
|     :type doc: str
 | |
|     :param source: source code of func (optional)
 | |
|     :type source: str
 | |
|     :returns: argument specification
 | |
|     :rtype: str
 | |
| 
 | |
|     >>> get_argspec(inspect.getclasstree)
 | |
|     '(classes, unique=0)'
 | |
|     >>> get_argspec(abs)
 | |
|     '(number)'
 | |
|     """
 | |
|     # get the function object of the class
 | |
|     try:
 | |
|         func = func.__func__
 | |
|     except AttributeError:
 | |
|         try:
 | |
|             # py 2.X
 | |
|             func = func.im_func
 | |
|         except AttributeError:
 | |
|             pass
 | |
|     # is callable?
 | |
|     if not hasattr(func, '__call__'):
 | |
|         return ''
 | |
|     # func should have a name
 | |
|     try:
 | |
|         func_name = func.__name__
 | |
|     except AttributeError:
 | |
|         return ''
 | |
|     # from docstring
 | |
|     if doc is None:
 | |
|         doc = get_doc(func)
 | |
|     match = re.search(DEF_DOC % func_name, doc, RE_FLAG)
 | |
|     # from source code
 | |
|     if not match:
 | |
|         if source is None:
 | |
|             try:
 | |
|                 source = inspect.getsource(func)
 | |
|             except TypeError:
 | |
|                 source = ''
 | |
|         if source:
 | |
|             match = re.search(DEF_SOURCE % func_name, source, RE_FLAG)
 | |
|     if match:
 | |
|         argspec = reduce_spaces(match.group(1))
 | |
|     else:
 | |
|         # try with the inspect.getarg* functions
 | |
|         try:
 | |
|             argspec = inspect.formatargspec(*inspect.getfullargspec(func))
 | |
|         except:
 | |
|             try:
 | |
|                 # py 2.X
 | |
|                 argspec = inspect.formatargspec(*inspect.getargspec(func))
 | |
|             except:
 | |
|                 try:
 | |
|                     argspec = inspect.formatargvalues(
 | |
|                         *inspect.getargvalues(func))
 | |
|                 except:
 | |
|                     argspec = ''
 | |
|         if strip_self:
 | |
|             argspec = argspec.replace('self, ', '')
 | |
|     return argspec
 | |
| 
 | |
| 
 | |
| def complete(line, cursor, namespace):
 | |
|     """Complete callable with calltip.
 | |
| 
 | |
|     :param line: incomplete text line
 | |
|     :type line: str
 | |
|     :param cursor: current character position
 | |
|     :type cursor: int
 | |
|     :param namespace: namespace
 | |
|     :type namespace: dict
 | |
|     :returns: (matches, world, scrollback)
 | |
|     :rtype: (list of str, str, str)
 | |
| 
 | |
|     >>> import os
 | |
|     >>> complete('os.path.isdir(', 14, {'os': os})[-1]
 | |
|     'isdir(s)\\nReturn true if the pathname refers to an existing directory.'
 | |
|     >>> complete('abs(', 4, {})[-1]
 | |
|     'abs(number) -> number\\nReturn the absolute value of the argument.'
 | |
|     """
 | |
|     matches = []
 | |
|     match = RE_DEF_COMPLETE.search(line[:cursor])
 | |
|     if match:
 | |
|         word = match.group(1)
 | |
|         func_word = match.group(2)
 | |
|         try:
 | |
|             func = eval(func_word, namespace)
 | |
|         except Exception:
 | |
|             func = None
 | |
|             scrollback = ''
 | |
|         if func:
 | |
|             doc = get_doc(func)
 | |
|             argspec = get_argspec(func, doc=doc)
 | |
|             scrollback = func_word.split('.')[-1] + (argspec or '()')
 | |
|             if doc.startswith(scrollback):
 | |
|                 scrollback = doc
 | |
|             elif doc:
 | |
|                 scrollback += '\n' + doc
 | |
|             scrollback = reduce_newlines(scrollback)
 | |
|     else:
 | |
|         word = ''
 | |
|         scrollback = ''
 | |
|     return matches, word, scrollback
 |