From 5fda4a2f8b2dfea5bac4ab3b87a07403426d732c Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Thu, 20 Sep 2018 21:14:07 +0200 Subject: [PATCH] Start putting the signature matching onto the ExecutedParam class --- jedi/evaluate/context/function.py | 42 +++++++++++++++++++-------- jedi/evaluate/context/instance.py | 48 ++++++++++++++++++++++++++----- jedi/evaluate/context/iterable.py | 33 ++------------------- jedi/evaluate/param.py | 24 +++++++++++++--- jedi/evaluate/pep0484.py | 12 +++++--- test/completion/arrays.py | 7 +++-- 6 files changed, 106 insertions(+), 60 deletions(-) diff --git a/jedi/evaluate/context/function.py b/jedi/evaluate/context/function.py index b19c6890..cfaca4ae 100644 --- a/jedi/evaluate/context/function.py +++ b/jedi/evaluate/context/function.py @@ -110,8 +110,10 @@ class FunctionContext(use_metaclass(CachedMetaClass, AbstractFunction)): ) return function - def py__call__(self, arguments): + def py__call__(self, arguments, need_param_match=False): function_execution = self.get_function_execution(arguments) + if need_param_match and function_execution.matches_signature(): + return NO_CONTEXTS return function_execution.infer() def get_function_execution(self, arguments=None): @@ -274,6 +276,19 @@ class FunctionExecutionContext(TreeContext): def get_executed_params(self): return self.var_args.get_executed_params(self) + def matches_signature(self): + matches = all(executed_param.matches_signature() + for executed_param in self.get_executed_params()) + if debug.enable_notice: + signature = parser_utils.get_call_signature(self.tree_node) + if matches: + debug.dbg("Overloading match: %s@%s", + signature, self.tree_node.start_pos[0], color='BLUE') + else: + debug.dbg("Overloading no match: %s@%s (%s)", + signature, self.tree_node.start_pos[0], self.var_args, color='BLUE') + return matches + def infer(self): """ Created to be used by inheritance. @@ -301,17 +316,17 @@ class FunctionExecutionContext(TreeContext): class OverloadedFunctionContext(ContextWrapper): def __init__(self, function, overloaded_functions): super(OverloadedFunctionContext, self).__init__(function) - self._overloaded_functions = overloaded_functions + self.overloaded_functions = overloaded_functions def py__call__(self, arguments): debug.dbg("Execute overloaded function %s", self._wrapped_context, color='BLUE') return ContextSet.from_sets( - matching_function.py__call__(arguments=arguments) - for matching_function in self.get_matching_functions(arguments) + f.py__call__(arguments=arguments, need_param_match=True) + for f in self.overloaded_functions ) def get_matching_functions(self, arguments): - for f in self._overloaded_functions: + for f in self.overloaded_functions: signature = parser_utils.get_call_signature(f.tree_node) if signature_matches(f, arguments): debug.dbg("Overloading match: %s@%s", @@ -324,15 +339,18 @@ class OverloadedFunctionContext(ContextWrapper): def signature_matches(function_context, arguments): unpacked_arguments = arguments.unpack() + key_args = {} for param_node in function_context.tree_node.get_params(): - key, argument = next(unpacked_arguments, (None, None)) + while True: + key, argument = next(unpacked_arguments, (None, None)) + if key is None or argument is None: + break + key_args[key] = argument if argument is None: - # This signature has an parameter more than arguments were given. - return bool(param_node.star_count) - if key is not None: - # TODO this is obviously wrong, we cannot just ignore keyword - # arguments, but it's easier for now. - return False + argument = key_args.pop(param_node.name.value, None) + if argument is None: + # This signature has an parameter more than arguments were given. + return bool(param_node.star_count == 1) if param_node.annotation is not None: if param_node.star_count == 2: diff --git a/jedi/evaluate/context/instance.py b/jedi/evaluate/context/instance.py index bbc538b1..6d16e270 100644 --- a/jedi/evaluate/context/instance.py +++ b/jedi/evaluate/context/instance.py @@ -68,6 +68,9 @@ class AbstractInstanceContext(Context): def is_class(self): return False + def get_annotated_class_object(self): + return self.class_context # This is the default. + @property def py__call__(self): names = self.get_function_slot_names(u'__call__') @@ -118,8 +121,9 @@ class AbstractInstanceContext(Context): def get_filters(self, search_global=None, until_position=None, origin_scope=None, include_self_names=True): + class_context = self.get_annotated_class_object() if include_self_names: - for cls in py__mro__(self.class_context): + for cls in py__mro__(class_context): if not isinstance(cls, compiled.CompiledObject) \ or cls.tree_node is not None: # In this case we're excluding compiled objects that are @@ -127,7 +131,7 @@ class AbstractInstanceContext(Context): # compiled objects to search for self variables. yield SelfAttributeFilter(self.evaluator, self, cls, origin_scope) - for cls in py__mro__(self.class_context): + for cls in py__mro__(class_context): if isinstance(cls, compiled.CompiledObject): yield CompiledInstanceClassFilter(self.evaluator, self, cls) else: @@ -170,18 +174,16 @@ class AbstractInstanceContext(Context): def name(self): pass - def _create_init_execution(self, class_context, bound_method): - return bound_method.get_function_execution(self.var_args) - def create_init_executions(self): for name in self.get_function_slot_names(u'__init__'): + # TODO is this correct? I think we need to check for functions. if isinstance(name, LazyInstanceClassName): function = FunctionContext.from_context( self.parent_context, name.tree_name.parent ) bound_method = BoundMethod(self, name.class_context, function) - yield self._create_init_execution(name.class_context, bound_method) + yield bound_method.get_function_execution(self.var_args) @evaluator_method_cache() def create_instance_context(self, class_context, node): @@ -199,7 +201,7 @@ class AbstractInstanceContext(Context): ) bound_method = BoundMethod(self, class_context, func) if scope.name.value == '__init__' and parent_context == class_context: - return self._create_init_execution(class_context, bound_method) + return bound_method.get_function_execution(self.var_args) else: return bound_method.get_function_execution() elif scope.type == 'classdef': @@ -259,6 +261,38 @@ class TreeInstance(AbstractInstanceContext): def name(self): return filters.ContextName(self, self.class_context.name.tree_name) + @evaluator_method_cache() + def get_annotated_class_object(self): + from jedi.evaluate.pep0484 import define_type_vars_for_execution + + for func in self._get_annotation_init_functions(): + # Just take the first result, it should always be one, because we + # control the typeshed code. + bound = BoundMethod(self, self.class_context, func) + execution = bound.get_function_execution(self.var_args) + if not execution.matches_signature(): + # First check if the signature even matches, if not we don't + # need to infer anything. + print('no m', bound) + continue + print(bound) + context_set = define_type_vars_for_execution( + ContextSet(self.class_context), + execution, + self.class_context.list_type_vars() + ) + if context_set: + return next(iter(context_set)) + return self.class_context + + def _get_annotation_init_functions(self): + for init in self.class_context.py__getattribute__('__init__'): + if isinstance(init, OverloadedFunctionContext): + for func in init.overloaded_functions: + yield func + elif isinstance(init, FunctionContext): + yield init + class AnonymousInstance(TreeInstance): def __init__(self, evaluator, parent_context, class_context): diff --git a/jedi/evaluate/context/iterable.py b/jedi/evaluate/context/iterable.py index f7590cc7..72a622bd 100644 --- a/jedi/evaluate/context/iterable.py +++ b/jedi/evaluate/context/iterable.py @@ -188,37 +188,8 @@ class Sequence(BuiltinOverwrite, IterableMixin): @memoize_method def get_object(self): klass = compiled.builtin_from_name(self.evaluator, self.array_type) - annotated_instance, = self._annotate_class(klass).execute_evaluated() - return annotated_instance - - def _annotate_class(self, klass): - from jedi.evaluate.pep0484 import define_type_vars_for_execution - - instance, = klass.execute(self) - - for execution_context in self._get_init_executions(instance): - # Just take the first result, it should always be one, because we - # control the typeshed code. - return define_type_vars_for_execution( - ContextSet(klass), - execution_context, - klass.list_type_vars() - ) - assert "Should never land here, probably an issue with typeshed changes" - - def _get_init_executions(self, instance): - from jedi.evaluate import arguments - from jedi.evaluate.context.instance import InstanceArguments - for init in instance.py__getattribute__('__init__'): - try: - method = init.get_matching_functions - except AttributeError: - continue - else: - base_args = arguments.ValuesArguments([ContextSet(self)]) - arguments = InstanceArguments(instance, base_args) - for func in method(arguments): - yield func.get_function_execution(base_args) + instance, = klass.execute_evaluated(self) + return instance def py__bool__(self): return None # We don't know the length, because of appends. diff --git a/jedi/evaluate/param.py b/jedi/evaluate/param.py index ba1d1715..b8514583 100644 --- a/jedi/evaluate/param.py +++ b/jedi/evaluate/param.py @@ -24,16 +24,32 @@ class ExecutedParam(object): self._lazy_context = lazy_context self.string_name = param_node.name.value + def infer_annotations(self): + from jedi.evaluate import pep0484 + return pep0484.infer_param(self._execution_context, self._param_node) + def infer(self, use_hints=True): if use_hints: - from jedi.evaluate import pep0484 - pep0484_hints = pep0484.infer_param(self._execution_context, self._param_node) doc_params = docstrings.infer_param(self._execution_context, self._param_node) - if pep0484_hints or doc_params: - return pep0484_hints | doc_params + ann = self.infer_annotations().execute_annotation() + if ann or doc_params: + return ann | doc_params return self._lazy_context.infer() + def matches_signature(self): + argument_contexts = self.infer(use_hints=False).py__class__() + if self._param_node.star_count: + return True + annotations = self.infer_annotations() + if not annotations: + # If we cannot infer annotations - or there aren't any - pretend + # that the signature matches. + return True + return any(c1.is_sub_class_of(c2) + for c1 in argument_contexts + for c2 in annotations) + @property def var_args(self): return self._execution_context.var_args diff --git a/jedi/evaluate/pep0484.py b/jedi/evaluate/pep0484.py index beb4b6a7..63cd9e60 100644 --- a/jedi/evaluate/pep0484.py +++ b/jedi/evaluate/pep0484.py @@ -58,7 +58,7 @@ def _evaluate_annotation_string(context, string, index=None): lambda context: context.array_type == u'tuple' # noqa and len(list(context.py__iter__())) >= index ).py__simple_getitem__(index) - return context_set.execute_annotation() + return context_set def _fix_forward_reference(context, node): @@ -174,7 +174,7 @@ def infer_param(execution_context, param): ) # Annotations are like default params and resolve in the same way. context = execution_context.function_context.get_default_param_context() - return _evaluate_for_annotation(context, annotation).execute_annotation() + return _evaluate_for_annotation(context, annotation) def py__annotations__(funcdef): @@ -212,7 +212,7 @@ def infer_return_types(function_execution_context): return _evaluate_annotation_string( function_execution_context.function_context.get_default_param_context(), match.group(1).strip() - ) + ).execute_annotation() if annotation is None: return NO_CONTEXTS @@ -255,6 +255,8 @@ def _infer_type_vars_for_execution(execution_context, annotation_dict): annotation_node = annotation_dict[executed_param.string_name] except KeyError: continue + if executed_param._param_node.star_count: # TODO remove this. + continue annotation_variables = find_unknown_type_vars(context, annotation_node) if annotation_variables: @@ -478,7 +480,9 @@ def _find_type_from_comment_hint(context, node, varlist, name): match = re.match(r"^#\s*type:\s*([^#]*)", comment) if match is None: return [] - return _evaluate_annotation_string(context, match.group(1).strip(), index) + return _evaluate_annotation_string( + context, match.group(1).strip(), index + ).execute_annotation() def find_unknown_type_vars(context, node): diff --git a/test/completion/arrays.py b/test/completion/arrays.py index faaa3beb..4f5c8e70 100644 --- a/test/completion/arrays.py +++ b/test/completion/arrays.py @@ -209,7 +209,6 @@ g dic2 = {'asdf': 3, 'b': 'str'} #? int() dic2['asdf'] -# TODO for now get doesn't work properly when used with a literal. #? None int() str() dic2.get('asdf') @@ -268,8 +267,12 @@ for x in {1: 3.0, '': 1j}: dict().values().__iter__ d = dict(a=3, b='') -#? int() str() +# Indexing is not supported +#? d.values()[0] +x, = d.values() +#? int() str() +x #? int() d['a'] #? int() None