Path: blob/master/elisp/emacs-for-python/rope-dist/rope/contrib/codeassist.py
1421 views
import keyword1import sys2import warnings34import rope.base.codeanalyze5import rope.base.evaluate6from rope.base import pyobjects, pyobjectsdef, pynames, builtins, exceptions, worder7from rope.base.codeanalyze import SourceLinesAdapter8from rope.contrib import fixsyntax9from rope.refactor import functionutils101112def code_assist(project, source_code, offset, resource=None,13templates=None, maxfixes=1, later_locals=True):14"""Return python code completions as a list of `CodeAssistProposal`\s1516`resource` is a `rope.base.resources.Resource` object. If17provided, relative imports are handled.1819`maxfixes` is the maximum number of errors to fix if the code has20errors in it.2122If `later_locals` is `False` names defined in this scope and after23this line is ignored.2425"""26if templates is not None:27warnings.warn('Codeassist no longer supports templates',28DeprecationWarning, stacklevel=2)29assist = _PythonCodeAssist(30project, source_code, offset, resource=resource,31maxfixes=maxfixes, later_locals=later_locals)32return assist()333435def starting_offset(source_code, offset):36"""Return the offset in which the completion should be inserted3738Usually code assist proposals should be inserted like::3940completion = proposal.name41result = (source_code[:starting_offset] +42completion + source_code[offset:])4344Where starting_offset is the offset returned by this function.4546"""47word_finder = worder.Worder(source_code, True)48expression, starting, starting_offset = \49word_finder.get_splitted_primary_before(offset)50return starting_offset515253def get_doc(project, source_code, offset, resource=None, maxfixes=1):54"""Get the pydoc"""55fixer = fixsyntax.FixSyntax(project.pycore, source_code,56resource, maxfixes)57pymodule = fixer.get_pymodule()58pyname = fixer.pyname_at(offset)59if pyname is None:60return None61pyobject = pyname.get_object()62return PyDocExtractor().get_doc(pyobject)636465def get_calltip(project, source_code, offset, resource=None,66maxfixes=1, ignore_unknown=False, remove_self=False):67"""Get the calltip of a function6869The format of the returned string is70``module_name.holding_scope_names.function_name(arguments)``. For71classes `__init__()` and for normal objects `__call__()` function72is used.7374Note that the offset is on the function itself *not* after the its75open parenthesis. (Actually it used to be the other way but it76was easily confused when string literals were involved. So I77decided it is better for it not to try to be too clever when it78cannot be clever enough). You can use a simple search like::7980offset = source_code.rindex('(', 0, offset) - 18182to handle simple situations.8384If `ignore_unknown` is `True`, `None` is returned for functions85without source-code like builtins and extensions.8687If `remove_self` is `True`, the first parameter whose name is self88will be removed for methods.89"""90fixer = fixsyntax.FixSyntax(project.pycore, source_code,91resource, maxfixes)92pymodule = fixer.get_pymodule()93pyname = fixer.pyname_at(offset)94if pyname is None:95return None96pyobject = pyname.get_object()97return PyDocExtractor().get_calltip(pyobject, ignore_unknown, remove_self)9899100def get_definition_location(project, source_code, offset,101resource=None, maxfixes=1):102"""Return the definition location of the python name at `offset`103104Return a (`rope.base.resources.Resource`, lineno) tuple. If no105`resource` is given and the definition is inside the same module,106the first element of the returned tuple would be `None`. If the107location cannot be determined ``(None, None)`` is returned.108109"""110fixer = fixsyntax.FixSyntax(project.pycore, source_code,111resource, maxfixes)112pymodule = fixer.get_pymodule()113pyname = fixer.pyname_at(offset)114if pyname is not None:115module, lineno = pyname.get_definition_location()116if module is not None:117return module.get_module().get_resource(), lineno118return (None, None)119120121def find_occurrences(*args, **kwds):122import rope.contrib.findit123warnings.warn('Use `rope.contrib.findit.find_occurrences()` instead',124DeprecationWarning, stacklevel=2)125return rope.contrib.findit.find_occurrences(*args, **kwds)126127128class CompletionProposal(object):129"""A completion proposal130131The `scope` instance variable shows where proposed name came from132and can be 'global', 'local', 'builtin', 'attribute', 'keyword',133'imported', 'parameter_keyword'.134135The `type` instance variable shows the approximate type of the136proposed object and can be 'instance', 'class', 'function', 'module',137and `None`.138139All possible relations between proposal's `scope` and `type` are shown140in the table below (different scopes in rows and types in columns):141142| instance | class | function | module | None143local | + | + | + | + |144global | + | + | + | + |145builtin | + | + | + | |146attribute | + | + | + | + |147imported | + | + | + | + |148keyword | | | | | +149parameter_keyword | | | | | +150151"""152153def __init__(self, name, scope, pyname=None):154self.name = name155self.scope = scope156self.pyname = pyname157if pyname is not None:158self.type = self._get_type()159else:160self.type = None161162def __str__(self):163return '%s (%s, %s)' % (self.name, self.scope, self.type)164165def __repr__(self):166return str(self)167168@property169def parameters(self):170"""The names of the parameters the function takes.171172Returns None if this completion is not a function.173"""174pyname = self.pyname175if isinstance(pyname, pynames.ImportedName):176pyname = pyname._get_imported_pyname()177if isinstance(pyname, pynames.DefinedName):178pyobject = pyname.get_object()179if isinstance(pyobject, pyobjects.AbstractFunction):180return pyobject.get_param_names()181182def _get_type(self):183pyname = self.pyname184if isinstance(pyname, builtins.BuiltinName):185self.scope = 'builtin'186pyobject = pyname.get_object()187if isinstance(pyobject, builtins.BuiltinFunction):188return 'function'189elif isinstance(pyobject, builtins.BuiltinClass):190clsobj = pyobject.builtin191return 'class'192elif isinstance(pyobject, builtins.BuiltinObject) or \193isinstance(pyobject, builtins.BuiltinName):194return 'instance'195elif isinstance(pyname, pynames.ImportedModule):196self.scope = 'imported'197return 'module'198elif isinstance(pyname, pynames.ImportedName) or \199isinstance(pyname, pynames.DefinedName):200if isinstance(pyname, pynames.ImportedName):201self.scope = 'imported'202pyobject = pyname.get_object()203if isinstance(pyobject, pyobjects.AbstractFunction):204return 'function'205if isinstance(pyobject, pyobjects.AbstractClass):206return 'class'207return 'instance'208209def get_doc(self):210"""Get the proposed object's docstring.211212Returns None if it can not be get.213"""214if not self.pyname:215return None216pyobject = self.pyname.get_object()217if not hasattr(pyobject, 'get_doc'):218return None219return self.pyname.get_object().get_doc()220221@property222def kind(self):223warnings.warn("the proposal's `kind` property is deprecated, " \224"use `scope` instead")225return self.scope226227228# leaved for backward compatibility229CodeAssistProposal = CompletionProposal230231232class NamedParamProposal(CompletionProposal):233"""A parameter keyword completion proposal234235Holds reference to ``_function`` -- the function which236parameter ``name`` belongs to. This allows to determine237default value for this parameter.238"""239def __init__(self, name, function):240self.argname = name241name = '%s=' % name242super(NamedParamProposal, self).__init__(name, 'parameter_keyword')243self._function = function244245def get_default(self):246"""Get a string representation of a param's default value.247248Returns None if there is no default value for this param.249"""250definfo = functionutils.DefinitionInfo.read(self._function)251for arg, default in definfo.args_with_defaults:252if self.argname == arg:253return default254return None255256257def sorted_proposals(proposals, scopepref=None, typepref=None):258"""Sort a list of proposals259260Return a sorted list of the given `CodeAssistProposal`\s.261262`scopepref` can be a list of proposal scopes. Defaults to263``['parameter_keyword', 'local', 'global', 'imported',264'attribute', 'builtin', 'keyword']``.265266`typepref` can be a list of proposal types. Defaults to267``['class', 'function', 'instance', 'module', None]``.268(`None` stands for completions with no type like keywords.)269"""270sorter = _ProposalSorter(proposals, scopepref, typepref)271return sorter.get_sorted_proposal_list()272273274def starting_expression(source_code, offset):275"""Return the expression to complete"""276word_finder = worder.Worder(source_code, True)277expression, starting, starting_offset = \278word_finder.get_splitted_primary_before(offset)279if expression:280return expression + '.' + starting281return starting282283284def default_templates():285warnings.warn('default_templates() is deprecated.',286DeprecationWarning, stacklevel=2)287return {}288289290class _PythonCodeAssist(object):291292def __init__(self, project, source_code, offset, resource=None,293maxfixes=1, later_locals=True):294self.project = project295self.pycore = self.project.pycore296self.code = source_code297self.resource = resource298self.maxfixes = maxfixes299self.later_locals = later_locals300self.word_finder = worder.Worder(source_code, True)301self.expression, self.starting, self.offset = \302self.word_finder.get_splitted_primary_before(offset)303304keywords = keyword.kwlist305306def _find_starting_offset(self, source_code, offset):307current_offset = offset - 1308while current_offset >= 0 and (source_code[current_offset].isalnum() or309source_code[current_offset] in '_'):310current_offset -= 1;311return current_offset + 1312313def _matching_keywords(self, starting):314result = []315for kw in self.keywords:316if kw.startswith(starting):317result.append(CompletionProposal(kw, 'keyword'))318return result319320def __call__(self):321if self.offset > len(self.code):322return []323completions = list(self._code_completions().values())324if self.expression.strip() == '' and self.starting.strip() != '':325completions.extend(self._matching_keywords(self.starting))326return completions327328def _dotted_completions(self, module_scope, holding_scope):329result = {}330found_pyname = rope.base.evaluate.eval_str(holding_scope,331self.expression)332if found_pyname is not None:333element = found_pyname.get_object()334compl_scope = 'attribute'335if isinstance(element, (pyobjectsdef.PyModule,336pyobjectsdef.PyPackage)):337compl_scope = 'imported'338for name, pyname in element.get_attributes().items():339if name.startswith(self.starting):340result[name] = CompletionProposal(name, compl_scope, pyname)341return result342343def _undotted_completions(self, scope, result, lineno=None):344if scope.parent != None:345self._undotted_completions(scope.parent, result)346if lineno is None:347names = scope.get_propagated_names()348else:349names = scope.get_names()350for name, pyname in names.items():351if name.startswith(self.starting):352compl_scope = 'local'353if scope.get_kind() == 'Module':354compl_scope = 'global'355if lineno is None or self.later_locals or \356not self._is_defined_after(scope, pyname, lineno):357result[name] = CompletionProposal(name, compl_scope,358pyname)359360def _from_import_completions(self, pymodule):361module_name = self.word_finder.get_from_module(self.offset)362if module_name is None:363return {}364pymodule = self._find_module(pymodule, module_name)365result = {}366for name in pymodule:367if name.startswith(self.starting):368result[name] = CompletionProposal(name, scope='global',369pyname=pymodule[name])370return result371372def _find_module(self, pymodule, module_name):373dots = 0374while module_name[dots] == '.':375dots += 1376pyname = pynames.ImportedModule(pymodule,377module_name[dots:], dots)378return pyname.get_object()379380def _is_defined_after(self, scope, pyname, lineno):381location = pyname.get_definition_location()382if location is not None and location[1] is not None:383if location[0] == scope.pyobject.get_module() and \384lineno <= location[1] <= scope.get_end():385return True386387def _code_completions(self):388lineno = self.code.count('\n', 0, self.offset) + 1389fixer = fixsyntax.FixSyntax(self.pycore, self.code,390self.resource, self.maxfixes)391pymodule = fixer.get_pymodule()392module_scope = pymodule.get_scope()393code = pymodule.source_code394lines = code.split('\n')395result = {}396start = fixsyntax._logical_start(lines, lineno)397indents = fixsyntax._get_line_indents(lines[start - 1])398inner_scope = module_scope.get_inner_scope_for_line(start, indents)399if self.word_finder.is_a_name_after_from_import(self.offset):400return self._from_import_completions(pymodule)401if self.expression.strip() != '':402result.update(self._dotted_completions(module_scope, inner_scope))403else:404result.update(self._keyword_parameters(module_scope.pyobject,405inner_scope))406self._undotted_completions(inner_scope, result, lineno=lineno)407return result408409def _keyword_parameters(self, pymodule, scope):410offset = self.offset411if offset == 0:412return {}413word_finder = worder.Worder(self.code, True)414lines = SourceLinesAdapter(self.code)415lineno = lines.get_line_number(offset)416if word_finder.is_on_function_call_keyword(offset - 1):417name_finder = rope.base.evaluate.ScopeNameFinder(pymodule)418function_parens = word_finder.\419find_parens_start_from_inside(offset - 1)420primary = word_finder.get_primary_at(function_parens - 1)421try:422function_pyname = rope.base.evaluate.\423eval_str(scope, primary)424except exceptions.BadIdentifierError, e:425return {}426if function_pyname is not None:427pyobject = function_pyname.get_object()428if isinstance(pyobject, pyobjects.AbstractFunction):429pass430elif isinstance(pyobject, pyobjects.AbstractClass) and \431'__init__' in pyobject:432pyobject = pyobject['__init__'].get_object()433elif '__call__' in pyobject:434pyobject = pyobject['__call__'].get_object()435if isinstance(pyobject, pyobjects.AbstractFunction):436param_names = []437param_names.extend(438pyobject.get_param_names(special_args=False))439result = {}440for name in param_names:441if name.startswith(self.starting):442result[name + '='] = NamedParamProposal(443name, pyobject444)445return result446return {}447448449class _ProposalSorter(object):450"""Sort a list of code assist proposals"""451452def __init__(self, code_assist_proposals, scopepref=None, typepref=None):453self.proposals = code_assist_proposals454if scopepref is None:455scopepref = ['parameter_keyword', 'local', 'global', 'imported',456'attribute', 'builtin', 'keyword']457self.scopepref = scopepref458if typepref is None:459typepref = ['class', 'function', 'instance', 'module', None]460self.typerank = dict((type, index)461for index, type in enumerate(typepref))462463def get_sorted_proposal_list(self):464"""Return a list of `CodeAssistProposal`"""465proposals = {}466for proposal in self.proposals:467proposals.setdefault(proposal.scope, []).append(proposal)468result = []469for scope in self.scopepref:470scope_proposals = proposals.get(scope, [])471scope_proposals = [proposal for proposal in scope_proposals472if proposal.type in self.typerank]473scope_proposals.sort(self._proposal_cmp)474result.extend(scope_proposals)475return result476477def _proposal_cmp(self, proposal1, proposal2):478if proposal1.type != proposal2.type:479return cmp(self.typerank.get(proposal1.type, 100),480self.typerank.get(proposal2.type, 100))481return self._compare_underlined_names(proposal1.name,482proposal2.name)483484def _compare_underlined_names(self, name1, name2):485def underline_count(name):486result = 0487while result < len(name) and name[result] == '_':488result += 1489return result490underline_count1 = underline_count(name1)491underline_count2 = underline_count(name2)492if underline_count1 != underline_count2:493return cmp(underline_count1, underline_count2)494return cmp(name1, name2)495496497class PyDocExtractor(object):498499def get_doc(self, pyobject):500if isinstance(pyobject, pyobjects.AbstractFunction):501return self._get_function_docstring(pyobject)502elif isinstance(pyobject, pyobjects.AbstractClass):503return self._get_class_docstring(pyobject)504elif isinstance(pyobject, pyobjects.AbstractModule):505return self._trim_docstring(pyobject.get_doc())506return None507508def get_calltip(self, pyobject, ignore_unknown=False, remove_self=False):509try:510if isinstance(pyobject, pyobjects.AbstractClass):511pyobject = pyobject['__init__'].get_object()512if not isinstance(pyobject, pyobjects.AbstractFunction):513pyobject = pyobject['__call__'].get_object()514except exceptions.AttributeNotFoundError:515return None516if ignore_unknown and not isinstance(pyobject, pyobjects.PyFunction):517return518if isinstance(pyobject, pyobjects.AbstractFunction):519result = self._get_function_signature(pyobject, add_module=True)520if remove_self and self._is_method(pyobject):521return result.replace('(self)', '()').replace('(self, ', '(')522return result523524def _get_class_docstring(self, pyclass):525contents = self._trim_docstring(pyclass.get_doc(), 2)526supers = [super.get_name() for super in pyclass.get_superclasses()]527doc = 'class %s(%s):\n\n' % (pyclass.get_name(), ', '.join(supers)) + contents528529if '__init__' in pyclass:530init = pyclass['__init__'].get_object()531if isinstance(init, pyobjects.AbstractFunction):532doc += '\n\n' + self._get_single_function_docstring(init)533return doc534535def _get_function_docstring(self, pyfunction):536functions = [pyfunction]537if self._is_method(pyfunction):538functions.extend(self._get_super_methods(pyfunction.parent,539pyfunction.get_name()))540return '\n\n'.join([self._get_single_function_docstring(function)541for function in functions])542543def _is_method(self, pyfunction):544return isinstance(pyfunction, pyobjects.PyFunction) and \545isinstance(pyfunction.parent, pyobjects.PyClass)546547def _get_single_function_docstring(self, pyfunction):548signature = self._get_function_signature(pyfunction)549docs = self._trim_docstring(pyfunction.get_doc(), indents=2)550return signature + ':\n\n' + docs551552def _get_super_methods(self, pyclass, name):553result = []554for super_class in pyclass.get_superclasses():555if name in super_class:556function = super_class[name].get_object()557if isinstance(function, pyobjects.AbstractFunction):558result.append(function)559result.extend(self._get_super_methods(super_class, name))560return result561562def _get_function_signature(self, pyfunction, add_module=False):563location = self._location(pyfunction, add_module)564if isinstance(pyfunction, pyobjects.PyFunction):565info = functionutils.DefinitionInfo.read(pyfunction)566return location + info.to_string()567else:568return '%s(%s)' % (location + pyfunction.get_name(),569', '.join(pyfunction.get_param_names()))570571def _location(self, pyobject, add_module=False):572location = []573parent = pyobject.parent574while parent and not isinstance(parent, pyobjects.AbstractModule):575location.append(parent.get_name())576location.append('.')577parent = parent.parent578if add_module:579if isinstance(pyobject, pyobjects.PyFunction):580module = pyobject.get_module()581location.insert(0, self._get_module(pyobject))582if isinstance(parent, builtins.BuiltinModule):583location.insert(0, parent.get_name() + '.')584return ''.join(location)585586def _get_module(self, pyfunction):587module = pyfunction.get_module()588if module is not None:589resource = module.get_resource()590if resource is not None:591return pyfunction.pycore.modname(resource) + '.'592return ''593594def _trim_docstring(self, docstring, indents=0):595"""The sample code from :PEP:`257`"""596if not docstring:597return ''598# Convert tabs to spaces (following normal Python rules)599# and split into a list of lines:600lines = docstring.expandtabs().splitlines()601# Determine minimum indentation (first line doesn't count):602indent = sys.maxint603for line in lines[1:]:604stripped = line.lstrip()605if stripped:606indent = min(indent, len(line) - len(stripped))607# Remove indentation (first line is special):608trimmed = [lines[0].strip()]609if indent < sys.maxint:610for line in lines[1:]:611trimmed.append(line[indent:].rstrip())612# Strip off trailing and leading blank lines:613while trimmed and not trimmed[-1]:614trimmed.pop()615while trimmed and not trimmed[0]:616trimmed.pop(0)617# Return a single string:618return '\n'.join((' ' * indents + line for line in trimmed))619620621# Deprecated classes622623class TemplateProposal(CodeAssistProposal):624def __init__(self, name, template):625warnings.warn('TemplateProposal is deprecated.',626DeprecationWarning, stacklevel=2)627super(TemplateProposal, self).__init__(name, 'template')628self.template = template629630631class Template(object):632633def __init__(self, template):634self.template = template635warnings.warn('Template is deprecated.',636DeprecationWarning, stacklevel=2)637638def variables(self):639return []640641def substitute(self, mapping):642return self.template643644def get_cursor_location(self, mapping):645return len(self.template)646647648