import keyword
import sys
import warnings
import rope.base.codeanalyze
import rope.base.evaluate
from rope.base import pyobjects, pyobjectsdef, pynames, builtins, exceptions, worder
from rope.base.codeanalyze import SourceLinesAdapter
from rope.contrib import fixsyntax
from rope.refactor import functionutils
def code_assist(project, source_code, offset, resource=None,
templates=None, maxfixes=1, later_locals=True):
"""Return python code completions as a list of `CodeAssistProposal`\s
`resource` is a `rope.base.resources.Resource` object. If
provided, relative imports are handled.
`maxfixes` is the maximum number of errors to fix if the code has
errors in it.
If `later_locals` is `False` names defined in this scope and after
this line is ignored.
"""
if templates is not None:
warnings.warn('Codeassist no longer supports templates',
DeprecationWarning, stacklevel=2)
assist = _PythonCodeAssist(
project, source_code, offset, resource=resource,
maxfixes=maxfixes, later_locals=later_locals)
return assist()
def starting_offset(source_code, offset):
"""Return the offset in which the completion should be inserted
Usually code assist proposals should be inserted like::
completion = proposal.name
result = (source_code[:starting_offset] +
completion + source_code[offset:])
Where starting_offset is the offset returned by this function.
"""
word_finder = worder.Worder(source_code, True)
expression, starting, starting_offset = \
word_finder.get_splitted_primary_before(offset)
return starting_offset
def get_doc(project, source_code, offset, resource=None, maxfixes=1):
"""Get the pydoc"""
fixer = fixsyntax.FixSyntax(project.pycore, source_code,
resource, maxfixes)
pymodule = fixer.get_pymodule()
pyname = fixer.pyname_at(offset)
if pyname is None:
return None
pyobject = pyname.get_object()
return PyDocExtractor().get_doc(pyobject)
def get_calltip(project, source_code, offset, resource=None,
maxfixes=1, ignore_unknown=False, remove_self=False):
"""Get the calltip of a function
The format of the returned string is
``module_name.holding_scope_names.function_name(arguments)``. For
classes `__init__()` and for normal objects `__call__()` function
is used.
Note that the offset is on the function itself *not* after the its
open parenthesis. (Actually it used to be the other way but it
was easily confused when string literals were involved. So I
decided it is better for it not to try to be too clever when it
cannot be clever enough). You can use a simple search like::
offset = source_code.rindex('(', 0, offset) - 1
to handle simple situations.
If `ignore_unknown` is `True`, `None` is returned for functions
without source-code like builtins and extensions.
If `remove_self` is `True`, the first parameter whose name is self
will be removed for methods.
"""
fixer = fixsyntax.FixSyntax(project.pycore, source_code,
resource, maxfixes)
pymodule = fixer.get_pymodule()
pyname = fixer.pyname_at(offset)
if pyname is None:
return None
pyobject = pyname.get_object()
return PyDocExtractor().get_calltip(pyobject, ignore_unknown, remove_self)
def get_definition_location(project, source_code, offset,
resource=None, maxfixes=1):
"""Return the definition location of the python name at `offset`
Return a (`rope.base.resources.Resource`, lineno) tuple. If no
`resource` is given and the definition is inside the same module,
the first element of the returned tuple would be `None`. If the
location cannot be determined ``(None, None)`` is returned.
"""
fixer = fixsyntax.FixSyntax(project.pycore, source_code,
resource, maxfixes)
pymodule = fixer.get_pymodule()
pyname = fixer.pyname_at(offset)
if pyname is not None:
module, lineno = pyname.get_definition_location()
if module is not None:
return module.get_module().get_resource(), lineno
return (None, None)
def find_occurrences(*args, **kwds):
import rope.contrib.findit
warnings.warn('Use `rope.contrib.findit.find_occurrences()` instead',
DeprecationWarning, stacklevel=2)
return rope.contrib.findit.find_occurrences(*args, **kwds)
class CompletionProposal(object):
"""A completion proposal
The `scope` instance variable shows where proposed name came from
and can be 'global', 'local', 'builtin', 'attribute', 'keyword',
'imported', 'parameter_keyword'.
The `type` instance variable shows the approximate type of the
proposed object and can be 'instance', 'class', 'function', 'module',
and `None`.
All possible relations between proposal's `scope` and `type` are shown
in the table below (different scopes in rows and types in columns):
| instance | class | function | module | None
local | + | + | + | + |
global | + | + | + | + |
builtin | + | + | + | |
attribute | + | + | + | + |
imported | + | + | + | + |
keyword | | | | | +
parameter_keyword | | | | | +
"""
def __init__(self, name, scope, pyname=None):
self.name = name
self.pyname = pyname
self.scope = self._get_scope(scope)
def __str__(self):
return '%s (%s, %s)' % (self.name, self.scope, self.type)
def __repr__(self):
return str(self)
@property
def parameters(self):
"""The names of the parameters the function takes.
Returns None if this completion is not a function.
"""
pyname = self.pyname
if isinstance(pyname, pynames.ImportedName):
pyname = pyname._get_imported_pyname()
if isinstance(pyname, pynames.DefinedName):
pyobject = pyname.get_object()
if isinstance(pyobject, pyobjects.AbstractFunction):
return pyobject.get_param_names()
@property
def type(self):
pyname = self.pyname
if isinstance(pyname, builtins.BuiltinName):
pyobject = pyname.get_object()
if isinstance(pyobject, builtins.BuiltinFunction):
return 'function'
elif isinstance(pyobject, builtins.BuiltinClass):
clsobj = pyobject.builtin
return 'class'
elif isinstance(pyobject, builtins.BuiltinObject) or \
isinstance(pyobject, builtins.BuiltinName):
return 'instance'
elif isinstance(pyname, pynames.ImportedModule):
return 'module'
elif isinstance(pyname, pynames.ImportedName) or \
isinstance(pyname, pynames.DefinedName):
pyobject = pyname.get_object()
if isinstance(pyobject, pyobjects.AbstractFunction):
return 'function'
if isinstance(pyobject, pyobjects.AbstractClass):
return 'class'
return 'instance'
def _get_scope(self, scope):
if isinstance(self.pyname, builtins.BuiltinName):
return 'builtin'
if isinstance(self.pyname, pynames.ImportedModule) or \
isinstance(self.pyname, pynames.ImportedName):
return 'imported'
return scope
def get_doc(self):
"""Get the proposed object's docstring.
Returns None if it can not be get.
"""
if not self.pyname:
return None
pyobject = self.pyname.get_object()
if not hasattr(pyobject, 'get_doc'):
return None
return self.pyname.get_object().get_doc()
@property
def kind(self):
warnings.warn("the proposal's `kind` property is deprecated, " \
"use `scope` instead")
return self.scope
# leaved for backward compatibility
CodeAssistProposal = CompletionProposal
class NamedParamProposal(CompletionProposal):
"""A parameter keyword completion proposal
Holds reference to ``_function`` -- the function which
parameter ``name`` belongs to. This allows to determine
default value for this parameter.
"""
def __init__(self, name, function):
self.argname = name
name = '%s=' % name
super(NamedParamProposal, self).__init__(name, 'parameter_keyword')
self._function = function
def get_default(self):
"""Get a string representation of a param's default value.
Returns None if there is no default value for this param.
"""
definfo = functionutils.DefinitionInfo.read(self._function)
for arg, default in definfo.args_with_defaults:
if self.argname == arg:
return default
return None
def sorted_proposals(proposals, scopepref=None, typepref=None):
"""Sort a list of proposals
Return a sorted list of the given `CodeAssistProposal`\s.
`scopepref` can be a list of proposal scopes. Defaults to
``['parameter_keyword', 'local', 'global', 'imported',
'attribute', 'builtin', 'keyword']``.
`typepref` can be a list of proposal types. Defaults to
``['class', 'function', 'instance', 'module', None]``.
(`None` stands for completions with no type like keywords.)
"""
sorter = _ProposalSorter(proposals, scopepref, typepref)
return sorter.get_sorted_proposal_list()
def starting_expression(source_code, offset):
"""Return the expression to complete"""
word_finder = worder.Worder(source_code, True)
expression, starting, starting_offset = \
word_finder.get_splitted_primary_before(offset)
if expression:
return expression + '.' + starting
return starting
def default_templates():
warnings.warn('default_templates() is deprecated.',
DeprecationWarning, stacklevel=2)
return {}
class _PythonCodeAssist(object):
def __init__(self, project, source_code, offset, resource=None,
maxfixes=1, later_locals=True):
self.project = project
self.pycore = self.project.pycore
self.code = source_code
self.resource = resource
self.maxfixes = maxfixes
self.later_locals = later_locals
self.word_finder = worder.Worder(source_code, True)
self.expression, self.starting, self.offset = \
self.word_finder.get_splitted_primary_before(offset)
keywords = keyword.kwlist
def _find_starting_offset(self, source_code, offset):
current_offset = offset - 1
while current_offset >= 0 and (source_code[current_offset].isalnum() or
source_code[current_offset] in '_'):
current_offset -= 1;
return current_offset + 1
def _matching_keywords(self, starting):
result = []
for kw in self.keywords:
if kw.startswith(starting):
result.append(CompletionProposal(kw, 'keyword'))
return result
def __call__(self):
if self.offset > len(self.code):
return []
completions = list(self._code_completions().values())
if self.expression.strip() == '' and self.starting.strip() != '':
completions.extend(self._matching_keywords(self.starting))
return completions
def _dotted_completions(self, module_scope, holding_scope):
result = {}
found_pyname = rope.base.evaluate.eval_str(holding_scope,
self.expression)
if found_pyname is not None:
element = found_pyname.get_object()
compl_scope = 'attribute'
if isinstance(element, (pyobjectsdef.PyModule,
pyobjectsdef.PyPackage)):
compl_scope = 'imported'
for name, pyname in element.get_attributes().items():
if name.startswith(self.starting):
result[name] = CompletionProposal(name, compl_scope, pyname)
return result
def _undotted_completions(self, scope, result, lineno=None):
if scope.parent != None:
self._undotted_completions(scope.parent, result)
if lineno is None:
names = scope.get_propagated_names()
else:
names = scope.get_names()
for name, pyname in names.items():
if name.startswith(self.starting):
compl_scope = 'local'
if scope.get_kind() == 'Module':
compl_scope = 'global'
if lineno is None or self.later_locals or \
not self._is_defined_after(scope, pyname, lineno):
result[name] = CompletionProposal(name, compl_scope,
pyname)
def _from_import_completions(self, pymodule):
module_name = self.word_finder.get_from_module(self.offset)
if module_name is None:
return {}
pymodule = self._find_module(pymodule, module_name)
result = {}
for name in pymodule:
if name.startswith(self.starting):
result[name] = CompletionProposal(name, scope='global',
pyname=pymodule[name])
return result
def _find_module(self, pymodule, module_name):
dots = 0
while module_name[dots] == '.':
dots += 1
pyname = pynames.ImportedModule(pymodule,
module_name[dots:], dots)
return pyname.get_object()
def _is_defined_after(self, scope, pyname, lineno):
location = pyname.get_definition_location()
if location is not None and location[1] is not None:
if location[0] == scope.pyobject.get_module() and \
lineno <= location[1] <= scope.get_end():
return True
def _code_completions(self):
lineno = self.code.count('\n', 0, self.offset) + 1
fixer = fixsyntax.FixSyntax(self.pycore, self.code,
self.resource, self.maxfixes)
pymodule = fixer.get_pymodule()
module_scope = pymodule.get_scope()
code = pymodule.source_code
lines = code.split('\n')
result = {}
start = fixsyntax._logical_start(lines, lineno)
indents = fixsyntax._get_line_indents(lines[start - 1])
inner_scope = module_scope.get_inner_scope_for_line(start, indents)
if self.word_finder.is_a_name_after_from_import(self.offset):
return self._from_import_completions(pymodule)
if self.expression.strip() != '':
result.update(self._dotted_completions(module_scope, inner_scope))
else:
result.update(self._keyword_parameters(module_scope.pyobject,
inner_scope))
self._undotted_completions(inner_scope, result, lineno=lineno)
return result
def _keyword_parameters(self, pymodule, scope):
offset = self.offset
if offset == 0:
return {}
word_finder = worder.Worder(self.code, True)
lines = SourceLinesAdapter(self.code)
lineno = lines.get_line_number(offset)
if word_finder.is_on_function_call_keyword(offset - 1):
name_finder = rope.base.evaluate.ScopeNameFinder(pymodule)
function_parens = word_finder.\
find_parens_start_from_inside(offset - 1)
primary = word_finder.get_primary_at(function_parens - 1)
try:
function_pyname = rope.base.evaluate.\
eval_str(scope, primary)
except exceptions.BadIdentifierError, e:
return {}
if function_pyname is not None:
pyobject = function_pyname.get_object()
if isinstance(pyobject, pyobjects.AbstractFunction):
pass
elif isinstance(pyobject, pyobjects.AbstractClass) and \
'__init__' in pyobject:
pyobject = pyobject['__init__'].get_object()
elif '__call__' in pyobject:
pyobject = pyobject['__call__'].get_object()
if isinstance(pyobject, pyobjects.AbstractFunction):
param_names = []
param_names.extend(
pyobject.get_param_names(special_args=False))
result = {}
for name in param_names:
if name.startswith(self.starting):
result[name + '='] = NamedParamProposal(
name, pyobject
)
return result
return {}
class _ProposalSorter(object):
"""Sort a list of code assist proposals"""
def __init__(self, code_assist_proposals, scopepref=None, typepref=None):
self.proposals = code_assist_proposals
if scopepref is None:
scopepref = ['parameter_keyword', 'local', 'global', 'imported',
'attribute', 'builtin', 'keyword']
self.scopepref = scopepref
if typepref is None:
typepref = ['class', 'function', 'instance', 'module', None]
self.typerank = dict((type, index)
for index, type in enumerate(typepref))
def get_sorted_proposal_list(self):
"""Return a list of `CodeAssistProposal`"""
proposals = {}
for proposal in self.proposals:
proposals.setdefault(proposal.scope, []).append(proposal)
result = []
for scope in self.scopepref:
scope_proposals = proposals.get(scope, [])
scope_proposals = [proposal for proposal in scope_proposals
if proposal.type in self.typerank]
scope_proposals.sort(self._proposal_cmp)
result.extend(scope_proposals)
return result
def _proposal_cmp(self, proposal1, proposal2):
if proposal1.type != proposal2.type:
return cmp(self.typerank.get(proposal1.type, 100),
self.typerank.get(proposal2.type, 100))
return self._compare_underlined_names(proposal1.name,
proposal2.name)
def _compare_underlined_names(self, name1, name2):
def underline_count(name):
result = 0
while result < len(name) and name[result] == '_':
result += 1
return result
underline_count1 = underline_count(name1)
underline_count2 = underline_count(name2)
if underline_count1 != underline_count2:
return cmp(underline_count1, underline_count2)
return cmp(name1, name2)
class PyDocExtractor(object):
def get_doc(self, pyobject):
if isinstance(pyobject, pyobjects.AbstractFunction):
return self._get_function_docstring(pyobject)
elif isinstance(pyobject, pyobjects.AbstractClass):
return self._get_class_docstring(pyobject)
elif isinstance(pyobject, pyobjects.AbstractModule):
return self._trim_docstring(pyobject.get_doc())
return None
def get_calltip(self, pyobject, ignore_unknown=False, remove_self=False):
try:
if isinstance(pyobject, pyobjects.AbstractClass):
pyobject = pyobject['__init__'].get_object()
if not isinstance(pyobject, pyobjects.AbstractFunction):
pyobject = pyobject['__call__'].get_object()
except exceptions.AttributeNotFoundError:
return None
if ignore_unknown and not isinstance(pyobject, pyobjects.PyFunction):
return
if isinstance(pyobject, pyobjects.AbstractFunction):
result = self._get_function_signature(pyobject, add_module=True)
if remove_self and self._is_method(pyobject):
return result.replace('(self)', '()').replace('(self, ', '(')
return result
def _get_class_docstring(self, pyclass):
contents = self._trim_docstring(pyclass.get_doc(), 2)
supers = [super.get_name() for super in pyclass.get_superclasses()]
doc = 'class %s(%s):\n\n' % (pyclass.get_name(), ', '.join(supers)) + contents
if '__init__' in pyclass:
init = pyclass['__init__'].get_object()
if isinstance(init, pyobjects.AbstractFunction):
doc += '\n\n' + self._get_single_function_docstring(init)
return doc
def _get_function_docstring(self, pyfunction):
functions = [pyfunction]
if self._is_method(pyfunction):
functions.extend(self._get_super_methods(pyfunction.parent,
pyfunction.get_name()))
return '\n\n'.join([self._get_single_function_docstring(function)
for function in functions])
def _is_method(self, pyfunction):
return isinstance(pyfunction, pyobjects.PyFunction) and \
isinstance(pyfunction.parent, pyobjects.PyClass)
def _get_single_function_docstring(self, pyfunction):
signature = self._get_function_signature(pyfunction)
docs = self._trim_docstring(pyfunction.get_doc(), indents=2)
return signature + ':\n\n' + docs
def _get_super_methods(self, pyclass, name):
result = []
for super_class in pyclass.get_superclasses():
if name in super_class:
function = super_class[name].get_object()
if isinstance(function, pyobjects.AbstractFunction):
result.append(function)
result.extend(self._get_super_methods(super_class, name))
return result
def _get_function_signature(self, pyfunction, add_module=False):
location = self._location(pyfunction, add_module)
if isinstance(pyfunction, pyobjects.PyFunction):
info = functionutils.DefinitionInfo.read(pyfunction)
return location + info.to_string()
else:
return '%s(%s)' % (location + pyfunction.get_name(),
', '.join(pyfunction.get_param_names()))
def _location(self, pyobject, add_module=False):
location = []
parent = pyobject.parent
while parent and not isinstance(parent, pyobjects.AbstractModule):
location.append(parent.get_name())
location.append('.')
parent = parent.parent
if add_module:
if isinstance(pyobject, pyobjects.PyFunction):
module = pyobject.get_module()
location.insert(0, self._get_module(pyobject))
if isinstance(parent, builtins.BuiltinModule):
location.insert(0, parent.get_name() + '.')
return ''.join(location)
def _get_module(self, pyfunction):
module = pyfunction.get_module()
if module is not None:
resource = module.get_resource()
if resource is not None:
return pyfunction.pycore.modname(resource) + '.'
return ''
def _trim_docstring(self, docstring, indents=0):
"""The sample code from :PEP:`257`"""
if not docstring:
return ''
# Convert tabs to spaces (following normal Python rules)
# and split into a list of lines:
lines = docstring.expandtabs().splitlines()
# Determine minimum indentation (first line doesn't count):
indent = sys.maxint
for line in lines[1:]:
stripped = line.lstrip()
if stripped:
indent = min(indent, len(line) - len(stripped))
# Remove indentation (first line is special):
trimmed = [lines[0].strip()]
if indent < sys.maxint:
for line in lines[1:]:
trimmed.append(line[indent:].rstrip())
# Strip off trailing and leading blank lines:
while trimmed and not trimmed[-1]:
trimmed.pop()
while trimmed and not trimmed[0]:
trimmed.pop(0)
# Return a single string:
return '\n'.join((' ' * indents + line for line in trimmed))
# Deprecated classes
class TemplateProposal(CodeAssistProposal):
def __init__(self, name, template):
warnings.warn('TemplateProposal is deprecated.',
DeprecationWarning, stacklevel=2)
super(TemplateProposal, self).__init__(name, 'template')
self.template = template
class Template(object):
def __init__(self, template):
self.template = template
warnings.warn('Template is deprecated.',
DeprecationWarning, stacklevel=2)
def variables(self):
return []
def substitute(self, mapping):
return self.template
def get_cursor_location(self, mapping):
return len(self.template)