678 lines
23 KiB
Python
678 lines
23 KiB
Python
from abc import abstractmethod
|
|
from inspect import Parameter
|
|
from typing import Optional, Tuple
|
|
|
|
from parso.tree import search_ancestor
|
|
|
|
from jedi.parser_utils import find_statement_documentation, clean_scope_docstring
|
|
from jedi.inference.utils import unite
|
|
from jedi.inference.base_value import ValueSet, NO_VALUES
|
|
from jedi.inference.cache import inference_state_method_cache
|
|
from jedi.inference import docstrings
|
|
from jedi.cache import memoize_method
|
|
from jedi.inference.helpers import deep_ast_copy, infer_call_of_leaf
|
|
from jedi.plugins import plugin_manager
|
|
|
|
|
|
def _merge_name_docs(names):
|
|
doc = ''
|
|
for name in names:
|
|
if doc:
|
|
# In case we have multiple values, just return all of them
|
|
# separated by a few dashes.
|
|
doc += '\n' + '-' * 30 + '\n'
|
|
doc += name.py__doc__()
|
|
return doc
|
|
|
|
|
|
class AbstractNameDefinition:
|
|
start_pos: Optional[Tuple[int, int]] = None
|
|
string_name: str
|
|
parent_context = None
|
|
tree_name = None
|
|
is_value_name = True
|
|
"""
|
|
Used for the Jedi API to know if it's a keyword or an actual name.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def infer(self):
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def goto(self):
|
|
# Typically names are already definitions and therefore a goto on that
|
|
# name will always result on itself.
|
|
return {self}
|
|
|
|
def get_qualified_names(self, include_module_names=False):
|
|
qualified_names = self._get_qualified_names()
|
|
if qualified_names is None or not include_module_names:
|
|
return qualified_names
|
|
|
|
module_names = self.get_root_context().string_names
|
|
if module_names is None:
|
|
return None
|
|
return module_names + qualified_names
|
|
|
|
def _get_qualified_names(self):
|
|
# By default, a name has no qualified names.
|
|
return None
|
|
|
|
def get_root_context(self):
|
|
return self.parent_context.get_root_context()
|
|
|
|
def get_public_name(self):
|
|
return self.string_name
|
|
|
|
def __repr__(self):
|
|
if self.start_pos is None:
|
|
return '<%s: string_name=%s>' % (self.__class__.__name__, self.string_name)
|
|
return '<%s: string_name=%s start_pos=%s>' % (self.__class__.__name__,
|
|
self.string_name, self.start_pos)
|
|
|
|
def is_import(self):
|
|
return False
|
|
|
|
def py__doc__(self):
|
|
return ''
|
|
|
|
@property
|
|
def api_type(self):
|
|
return self.parent_context.api_type
|
|
|
|
def get_defining_qualified_value(self):
|
|
"""
|
|
Returns either None or the value that is public and qualified. Won't
|
|
return a function, because a name in a function is never public.
|
|
"""
|
|
return None
|
|
|
|
|
|
class AbstractArbitraryName(AbstractNameDefinition):
|
|
"""
|
|
When you e.g. want to complete dicts keys, you probably want to complete
|
|
string literals, which is not really a name, but for Jedi we use this
|
|
concept of Name for completions as well.
|
|
"""
|
|
is_value_name = False
|
|
|
|
def __init__(self, inference_state, string):
|
|
self.inference_state = inference_state
|
|
self.string_name = string
|
|
self.parent_context = inference_state.builtins_module
|
|
|
|
def infer(self):
|
|
return NO_VALUES
|
|
|
|
|
|
class AbstractTreeName(AbstractNameDefinition):
|
|
def __init__(self, parent_context, tree_name):
|
|
self.parent_context = parent_context
|
|
self.tree_name = tree_name
|
|
|
|
def get_qualified_names(self, include_module_names=False):
|
|
import_node = search_ancestor(self.tree_name, 'import_name', 'import_from')
|
|
# For import nodes we cannot just have names, because it's very unclear
|
|
# how they would look like. For now we just ignore them in most cases.
|
|
# In case of level == 1, it works always, because it's like a submodule
|
|
# lookup.
|
|
if import_node is not None and not (import_node.level == 1
|
|
and self.get_root_context().get_value().is_package()):
|
|
# TODO improve the situation for when level is present.
|
|
if include_module_names and not import_node.level:
|
|
return tuple(n.value for n in import_node.get_path_for_name(self.tree_name))
|
|
else:
|
|
return None
|
|
|
|
return super().get_qualified_names(include_module_names)
|
|
|
|
def _get_qualified_names(self):
|
|
parent_names = self.parent_context.get_qualified_names()
|
|
if parent_names is None:
|
|
return None
|
|
return parent_names + (self.tree_name.value,)
|
|
|
|
def get_defining_qualified_value(self):
|
|
if self.is_import():
|
|
raise NotImplementedError("Shouldn't really happen, please report")
|
|
elif self.parent_context:
|
|
return self.parent_context.get_value() # Might be None
|
|
return None
|
|
|
|
def goto(self):
|
|
context = self.parent_context
|
|
name = self.tree_name
|
|
definition = name.get_definition(import_name_always=True)
|
|
if definition is not None:
|
|
type_ = definition.type
|
|
if type_ == 'expr_stmt':
|
|
# Only take the parent, because if it's more complicated than just
|
|
# a name it's something you can "goto" again.
|
|
is_simple_name = name.parent.type not in ('power', 'trailer')
|
|
if is_simple_name:
|
|
return [self]
|
|
elif type_ in ('import_from', 'import_name'):
|
|
from jedi.inference.imports import goto_import
|
|
module_names = goto_import(context, name)
|
|
return module_names
|
|
else:
|
|
return [self]
|
|
else:
|
|
from jedi.inference.imports import follow_error_node_imports_if_possible
|
|
values = follow_error_node_imports_if_possible(context, name)
|
|
if values is not None:
|
|
return [value.name for value in values]
|
|
|
|
par = name.parent
|
|
node_type = par.type
|
|
if node_type == 'argument' and par.children[1] == '=' and par.children[0] == name:
|
|
# Named param goto.
|
|
trailer = par.parent
|
|
if trailer.type == 'arglist':
|
|
trailer = trailer.parent
|
|
if trailer.type != 'classdef':
|
|
if trailer.type == 'decorator':
|
|
value_set = context.infer_node(trailer.children[1])
|
|
else:
|
|
i = trailer.parent.children.index(trailer)
|
|
to_infer = trailer.parent.children[:i]
|
|
if to_infer[0] == 'await':
|
|
to_infer.pop(0)
|
|
value_set = context.infer_node(to_infer[0])
|
|
from jedi.inference.syntax_tree import infer_trailer
|
|
for trailer in to_infer[1:]:
|
|
value_set = infer_trailer(context, value_set, trailer)
|
|
param_names = []
|
|
for value in value_set:
|
|
for signature in value.get_signatures():
|
|
for param_name in signature.get_param_names():
|
|
if param_name.string_name == name.value:
|
|
param_names.append(param_name)
|
|
return param_names
|
|
elif node_type == 'dotted_name': # Is a decorator.
|
|
index = par.children.index(name)
|
|
if index > 0:
|
|
new_dotted = deep_ast_copy(par)
|
|
new_dotted.children[index - 1:] = []
|
|
values = context.infer_node(new_dotted)
|
|
return unite(
|
|
value.goto(name, name_context=context)
|
|
for value in values
|
|
)
|
|
|
|
if node_type == 'trailer' and par.children[0] == '.':
|
|
values = infer_call_of_leaf(context, name, cut_own_trailer=True)
|
|
return values.goto(name, name_context=context)
|
|
else:
|
|
stmt = search_ancestor(
|
|
name, 'expr_stmt', 'lambdef'
|
|
) or name
|
|
if stmt.type == 'lambdef':
|
|
stmt = name
|
|
return context.goto(name, position=stmt.start_pos)
|
|
|
|
def is_import(self):
|
|
imp = search_ancestor(self.tree_name, 'import_from', 'import_name')
|
|
return imp is not None
|
|
|
|
@property
|
|
def string_name(self):
|
|
return self.tree_name.value
|
|
|
|
@property
|
|
def start_pos(self):
|
|
return self.tree_name.start_pos
|
|
|
|
|
|
class ValueNameMixin:
|
|
def infer(self):
|
|
return ValueSet([self._value])
|
|
|
|
def py__doc__(self):
|
|
doc = self._value.py__doc__()
|
|
if not doc and self._value.is_stub():
|
|
from jedi.inference.gradual.conversion import convert_names
|
|
names = convert_names([self], prefer_stub_to_compiled=False)
|
|
if self not in names:
|
|
return _merge_name_docs(names)
|
|
return doc
|
|
|
|
def _get_qualified_names(self):
|
|
return self._value.get_qualified_names()
|
|
|
|
def get_root_context(self):
|
|
if self.parent_context is None: # A module
|
|
return self._value.as_context()
|
|
return super().get_root_context()
|
|
|
|
def get_defining_qualified_value(self):
|
|
context = self.parent_context
|
|
if context is not None and (context.is_module() or context.is_class()):
|
|
return self.parent_context.get_value() # Might be None
|
|
return None
|
|
|
|
@property
|
|
def api_type(self):
|
|
return self._value.api_type
|
|
|
|
|
|
class ValueName(ValueNameMixin, AbstractTreeName):
|
|
def __init__(self, value, tree_name):
|
|
super().__init__(value.parent_context, tree_name)
|
|
self._value = value
|
|
|
|
def goto(self):
|
|
return ValueSet([self._value.name])
|
|
|
|
|
|
class TreeNameDefinition(AbstractTreeName):
|
|
_API_TYPES = dict(
|
|
import_name='module',
|
|
import_from='module',
|
|
funcdef='function',
|
|
param='param',
|
|
classdef='class',
|
|
)
|
|
|
|
def infer(self):
|
|
# Refactor this, should probably be here.
|
|
from jedi.inference.syntax_tree import tree_name_to_values
|
|
return tree_name_to_values(
|
|
self.parent_context.inference_state,
|
|
self.parent_context,
|
|
self.tree_name
|
|
)
|
|
|
|
@property
|
|
def api_type(self):
|
|
definition = self.tree_name.get_definition(import_name_always=True)
|
|
if definition is None:
|
|
return 'statement'
|
|
return self._API_TYPES.get(definition.type, 'statement')
|
|
|
|
def assignment_indexes(self):
|
|
"""
|
|
Returns an array of tuple(int, node) of the indexes that are used in
|
|
tuple assignments.
|
|
|
|
For example if the name is ``y`` in the following code::
|
|
|
|
x, (y, z) = 2, ''
|
|
|
|
would result in ``[(1, xyz_node), (0, yz_node)]``.
|
|
|
|
When searching for b in the case ``a, *b, c = [...]`` it will return::
|
|
|
|
[(slice(1, -1), abc_node)]
|
|
"""
|
|
indexes = []
|
|
is_star_expr = False
|
|
node = self.tree_name.parent
|
|
compare = self.tree_name
|
|
while node is not None:
|
|
if node.type in ('testlist', 'testlist_comp', 'testlist_star_expr', 'exprlist'):
|
|
for i, child in enumerate(node.children):
|
|
if child == compare:
|
|
index = int(i / 2)
|
|
if is_star_expr:
|
|
from_end = int((len(node.children) - i) / 2)
|
|
index = slice(index, -from_end)
|
|
indexes.insert(0, (index, node))
|
|
break
|
|
else:
|
|
raise LookupError("Couldn't find the assignment.")
|
|
is_star_expr = False
|
|
elif node.type == 'star_expr':
|
|
is_star_expr = True
|
|
elif node.type in ('expr_stmt', 'sync_comp_for'):
|
|
break
|
|
|
|
compare = node
|
|
node = node.parent
|
|
return indexes
|
|
|
|
@property
|
|
def inference_state(self):
|
|
# Used by the cache function below
|
|
return self.parent_context.inference_state
|
|
|
|
@inference_state_method_cache(default='')
|
|
def py__doc__(self):
|
|
api_type = self.api_type
|
|
if api_type in ('function', 'class', 'property'):
|
|
if self.parent_context.get_root_context().is_stub():
|
|
from jedi.inference.gradual.conversion import convert_names
|
|
names = convert_names([self], prefer_stub_to_compiled=False)
|
|
if self not in names:
|
|
return _merge_name_docs(names)
|
|
|
|
# Make sure the names are not TreeNameDefinitions anymore.
|
|
return clean_scope_docstring(self.tree_name.get_definition())
|
|
|
|
if api_type == 'module':
|
|
names = self.goto()
|
|
if self not in names:
|
|
return _merge_name_docs(names)
|
|
|
|
if api_type == 'statement' and self.tree_name.is_definition():
|
|
return find_statement_documentation(self.tree_name.get_definition())
|
|
return ''
|
|
|
|
|
|
class _ParamMixin:
|
|
def maybe_positional_argument(self, include_star=True):
|
|
options = [Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD]
|
|
if include_star:
|
|
options.append(Parameter.VAR_POSITIONAL)
|
|
return self.get_kind() in options
|
|
|
|
def maybe_keyword_argument(self, include_stars=True):
|
|
options = [Parameter.KEYWORD_ONLY, Parameter.POSITIONAL_OR_KEYWORD]
|
|
if include_stars:
|
|
options.append(Parameter.VAR_KEYWORD)
|
|
return self.get_kind() in options
|
|
|
|
def _kind_string(self):
|
|
kind = self.get_kind()
|
|
if kind == Parameter.VAR_POSITIONAL: # *args
|
|
return '*'
|
|
if kind == Parameter.VAR_KEYWORD: # **kwargs
|
|
return '**'
|
|
return ''
|
|
|
|
def get_qualified_names(self, include_module_names=False):
|
|
return None
|
|
|
|
|
|
class ParamNameInterface(_ParamMixin):
|
|
api_type = 'param'
|
|
|
|
def get_kind(self):
|
|
raise NotImplementedError
|
|
|
|
def to_string(self):
|
|
raise NotImplementedError
|
|
|
|
def get_executed_param_name(self):
|
|
"""
|
|
For dealing with type inference and working around the graph, we
|
|
sometimes want to have the param name of the execution. This feels a
|
|
bit strange and we might have to refactor at some point.
|
|
|
|
For now however it exists to avoid infering params when we don't really
|
|
need them (e.g. when we can just instead use annotations.
|
|
"""
|
|
return None
|
|
|
|
@property
|
|
def star_count(self):
|
|
kind = self.get_kind()
|
|
if kind == Parameter.VAR_POSITIONAL:
|
|
return 1
|
|
if kind == Parameter.VAR_KEYWORD:
|
|
return 2
|
|
return 0
|
|
|
|
def infer_default(self):
|
|
return NO_VALUES
|
|
|
|
|
|
class BaseTreeParamName(ParamNameInterface, AbstractTreeName):
|
|
annotation_node = None
|
|
default_node = None
|
|
|
|
def to_string(self):
|
|
output = self._kind_string() + self.get_public_name()
|
|
annotation = self.annotation_node
|
|
default = self.default_node
|
|
if annotation is not None:
|
|
output += ': ' + annotation.get_code(include_prefix=False)
|
|
if default is not None:
|
|
output += '=' + default.get_code(include_prefix=False)
|
|
return output
|
|
|
|
def get_public_name(self):
|
|
name = self.string_name
|
|
if name.startswith('__'):
|
|
# Params starting with __ are an equivalent to positional only
|
|
# variables in typeshed.
|
|
name = name[2:]
|
|
return name
|
|
|
|
def goto(self, **kwargs):
|
|
return [self]
|
|
|
|
|
|
class _ActualTreeParamName(BaseTreeParamName):
|
|
def __init__(self, function_value, tree_name):
|
|
super().__init__(
|
|
function_value.get_default_param_context(), tree_name)
|
|
self.function_value = function_value
|
|
|
|
def _get_param_node(self):
|
|
return search_ancestor(self.tree_name, 'param')
|
|
|
|
@property
|
|
def annotation_node(self):
|
|
return self._get_param_node().annotation
|
|
|
|
def infer_annotation(self, execute_annotation=True, ignore_stars=False):
|
|
from jedi.inference.gradual.annotation import infer_param
|
|
values = infer_param(
|
|
self.function_value, self._get_param_node(),
|
|
ignore_stars=ignore_stars)
|
|
if execute_annotation:
|
|
values = values.execute_annotation()
|
|
return values
|
|
|
|
def infer_default(self):
|
|
node = self.default_node
|
|
if node is None:
|
|
return NO_VALUES
|
|
return self.parent_context.infer_node(node)
|
|
|
|
@property
|
|
def default_node(self):
|
|
return self._get_param_node().default
|
|
|
|
def get_kind(self):
|
|
tree_param = self._get_param_node()
|
|
if tree_param.star_count == 1: # *args
|
|
return Parameter.VAR_POSITIONAL
|
|
if tree_param.star_count == 2: # **kwargs
|
|
return Parameter.VAR_KEYWORD
|
|
|
|
# Params starting with __ are an equivalent to positional only
|
|
# variables in typeshed.
|
|
if tree_param.name.value.startswith('__'):
|
|
return Parameter.POSITIONAL_ONLY
|
|
|
|
parent = tree_param.parent
|
|
param_appeared = False
|
|
for p in parent.children:
|
|
if param_appeared:
|
|
if p == '/':
|
|
return Parameter.POSITIONAL_ONLY
|
|
else:
|
|
if p == '*':
|
|
return Parameter.KEYWORD_ONLY
|
|
if p.type == 'param':
|
|
if p.star_count:
|
|
return Parameter.KEYWORD_ONLY
|
|
if p == tree_param:
|
|
param_appeared = True
|
|
return Parameter.POSITIONAL_OR_KEYWORD
|
|
|
|
def infer(self):
|
|
values = self.infer_annotation()
|
|
if values:
|
|
return values
|
|
|
|
doc_params = docstrings.infer_param(self.function_value, self._get_param_node())
|
|
return doc_params
|
|
|
|
|
|
class AnonymousParamName(_ActualTreeParamName):
|
|
@plugin_manager.decorate(name='goto_anonymous_param')
|
|
def goto(self):
|
|
return super().goto()
|
|
|
|
@plugin_manager.decorate(name='infer_anonymous_param')
|
|
def infer(self):
|
|
values = super().infer()
|
|
if values:
|
|
return values
|
|
from jedi.inference.dynamic_params import dynamic_param_lookup
|
|
param = self._get_param_node()
|
|
values = dynamic_param_lookup(self.function_value, param.position_index)
|
|
if values:
|
|
return values
|
|
|
|
if param.star_count == 1:
|
|
from jedi.inference.value.iterable import FakeTuple
|
|
value = FakeTuple(self.function_value.inference_state, [])
|
|
elif param.star_count == 2:
|
|
from jedi.inference.value.iterable import FakeDict
|
|
value = FakeDict(self.function_value.inference_state, {})
|
|
elif param.default is None:
|
|
return NO_VALUES
|
|
else:
|
|
return self.function_value.parent_context.infer_node(param.default)
|
|
return ValueSet({value})
|
|
|
|
|
|
class ParamName(_ActualTreeParamName):
|
|
def __init__(self, function_value, tree_name, arguments):
|
|
super().__init__(function_value, tree_name)
|
|
self.arguments = arguments
|
|
|
|
def infer(self):
|
|
values = super().infer()
|
|
if values:
|
|
return values
|
|
|
|
return self.get_executed_param_name().infer()
|
|
|
|
def get_executed_param_name(self):
|
|
from jedi.inference.param import get_executed_param_names
|
|
params_names = get_executed_param_names(self.function_value, self.arguments)
|
|
return params_names[self._get_param_node().position_index]
|
|
|
|
|
|
class ParamNameWrapper(_ParamMixin):
|
|
def __init__(self, param_name):
|
|
self._wrapped_param_name = param_name
|
|
|
|
def __getattr__(self, name):
|
|
return getattr(self._wrapped_param_name, name)
|
|
|
|
def __repr__(self):
|
|
return '<%s: %s>' % (self.__class__.__name__, self._wrapped_param_name)
|
|
|
|
|
|
class ImportName(AbstractNameDefinition):
|
|
start_pos = (1, 0)
|
|
_level = 0
|
|
|
|
def __init__(self, parent_context, string_name):
|
|
self._from_module_context = parent_context
|
|
self.string_name = string_name
|
|
|
|
def get_qualified_names(self, include_module_names=False):
|
|
if include_module_names:
|
|
if self._level:
|
|
assert self._level == 1, "Everything else is not supported for now"
|
|
module_names = self._from_module_context.string_names
|
|
if module_names is None:
|
|
return module_names
|
|
return module_names + (self.string_name,)
|
|
return (self.string_name,)
|
|
return ()
|
|
|
|
@property
|
|
def parent_context(self):
|
|
m = self._from_module_context
|
|
import_values = self.infer()
|
|
if not import_values:
|
|
return m
|
|
# It's almost always possible to find the import or to not find it. The
|
|
# importing returns only one value, pretty much always.
|
|
return next(iter(import_values)).as_context()
|
|
|
|
@memoize_method
|
|
def infer(self):
|
|
from jedi.inference.imports import Importer
|
|
m = self._from_module_context
|
|
return Importer(m.inference_state, [self.string_name], m, level=self._level).follow()
|
|
|
|
def goto(self):
|
|
return [m.name for m in self.infer()]
|
|
|
|
@property
|
|
def api_type(self):
|
|
return 'module'
|
|
|
|
def py__doc__(self):
|
|
return _merge_name_docs(self.goto())
|
|
|
|
|
|
class SubModuleName(ImportName):
|
|
_level = 1
|
|
|
|
|
|
class NameWrapper:
|
|
def __init__(self, wrapped_name):
|
|
self._wrapped_name = wrapped_name
|
|
|
|
def __getattr__(self, name):
|
|
return getattr(self._wrapped_name, name)
|
|
|
|
def __repr__(self):
|
|
return '%s(%s)' % (self.__class__.__name__, self._wrapped_name)
|
|
|
|
|
|
class StubNameMixin:
|
|
def py__doc__(self):
|
|
from jedi.inference.gradual.conversion import convert_names
|
|
# Stubs are not complicated and we can just follow simple statements
|
|
# that have an equals in them, because they typically make something
|
|
# else public. See e.g. stubs for `requests`.
|
|
names = [self]
|
|
if self.api_type == 'statement' and '=' in self.tree_name.get_definition().children:
|
|
names = [v.name for v in self.infer()]
|
|
|
|
names = convert_names(names, prefer_stub_to_compiled=False)
|
|
if self in names:
|
|
return super().py__doc__()
|
|
else:
|
|
# We have signatures ourselves in stubs, so don't use signatures
|
|
# from the implementation.
|
|
return _merge_name_docs(names)
|
|
|
|
|
|
# From here on down we make looking up the sys.version_info fast.
|
|
class StubName(StubNameMixin, TreeNameDefinition):
|
|
def infer(self):
|
|
inferred = super().infer()
|
|
if self.string_name == 'version_info' and self.get_root_context().py__name__() == 'sys':
|
|
from jedi.inference.gradual.stub_value import VersionInfo
|
|
return ValueSet(VersionInfo(c) for c in inferred)
|
|
return inferred
|
|
|
|
|
|
class ModuleName(ValueNameMixin, AbstractNameDefinition):
|
|
start_pos = 1, 0
|
|
|
|
def __init__(self, value, name):
|
|
self._value = value
|
|
self._name = name
|
|
|
|
@property
|
|
def string_name(self):
|
|
return self._name
|
|
|
|
|
|
class StubModuleName(StubNameMixin, ModuleName):
|
|
pass
|