From 96c969687ae2916cf7ce91ef95f52aaa66602863 Mon Sep 17 00:00:00 2001 From: Daniel Lemm <61800298+ffe4@users.noreply.github.com> Date: Thu, 12 Mar 2020 18:40:28 +0100 Subject: [PATCH 1/3] Add partialmethod, fixes #1519 Returns correct method signature but test/completion/stdlib.py fails --- jedi/plugins/stdlib.py | 51 +++++++++++++++++++++++++++ test/completion/stdlib.py | 17 +++++++++ test/test_inference/test_signature.py | 18 ++++++++++ 3 files changed, 86 insertions(+) diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index 448f2b50..8678051e 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -513,6 +513,49 @@ class PartialObject(object): ) +class PartialMethodObject(object): + def __init__(self, actual_value, arguments): + self._actual_value = actual_value + self._arguments = arguments + + def __getattr__(self, name): + return getattr(self._actual_value, name) + + def _get_function(self, unpacked_arguments): + key, lazy_value = next(unpacked_arguments, (None, None)) + if key is not None or lazy_value is None: + debug.warning("Partial should have a proper function %s", self._arguments) + return None + return lazy_value.infer() + + def get_signatures(self): + unpacked_arguments = self._arguments.unpack() + func = self._get_function(unpacked_arguments) + if func is None: + return [] + + arg_count = 1 + keys = set() + for key, _ in unpacked_arguments: + if key is None: + arg_count += 1 + else: + keys.add(key) + return [PartialSignature(s, arg_count, keys) for s in func.get_signatures()] + + def py__get__(self, instance, class_value): + return ValueSet([self]) + + def py__call__(self, arguments): + func = self._get_function(self._arguments.unpack()) + if func is None: + return NO_VALUES + + return func.execute( + MergedPartialArguments(self._arguments, arguments) + ) + + class PartialSignature(SignatureWrapper): def __init__(self, wrapped_signature, skipped_arg_count, skipped_arg_set): super(PartialSignature, self).__init__(wrapped_signature) @@ -547,6 +590,13 @@ def functools_partial(value, arguments, callback): ) +def functools_partialmethod(value, arguments, callback): + return ValueSet( + PartialMethodObject(instance, arguments) + for instance in value.py__call__(arguments) + ) + + @argument_clinic('first, /') def _return_first_param(firsts): return firsts @@ -744,6 +794,7 @@ _implemented = { }, 'functools': { 'partial': functools_partial, + 'partialmethod': functools_partialmethod, 'wraps': _functools_wraps, }, '_weakref': { diff --git a/test/completion/stdlib.py b/test/completion/stdlib.py index 636b66fb..4fa5a238 100644 --- a/test/completion/stdlib.py +++ b/test/completion/stdlib.py @@ -158,6 +158,23 @@ tup[0] #? float() tup[1] +class X: + def function(self, a, b): + return a, b + a = functools.partialmethod(function, 0) + kw = functools.partialmethod(function, b=1.0) + +#? int() +X().a('')[0] +#? str() +X().a('')[1] + +tup = X().kw(1) +#? int() +tup[0] +#? float() +tup[1] + def my_decorator(f): @functools.wraps(f) def wrapper(*args, **kwds): diff --git a/test/test_inference/test_signature.py b/test/test_inference/test_signature.py index 186b5770..967198b3 100644 --- a/test/test_inference/test_signature.py +++ b/test/test_inference/test_signature.py @@ -65,6 +65,19 @@ d = functools.partial() ''' +partialmethod_code = ''' +import functools + +class X: + def func(self, a, b, c): + pass + a = functools.partialmethod(func) + b = functools.partialmethod(func, 1) + c = functools.partialmethod(func, 1, c=2) + d = functools.partialmethod() +''' + + @pytest.mark.parametrize( 'code, expected', [ ('def f(a, * args, x): pass\n f(', 'f(a, *args, x)'), @@ -82,6 +95,11 @@ d = functools.partial() (partial_code + 'b(', 'func(b, c)'), (partial_code + 'c(', 'func(b)'), (partial_code + 'd(', None), + + (partialmethod_code + 'X().a(', 'func(a, b, c)'), + (partialmethod_code + 'X().b(', 'func(b, c)'), + (partialmethod_code + 'X().c(', 'func(b)'), + (partialmethod_code + 'X().d(', None), ] ) def test_tree_signature(Script, environment, code, expected): From fd6540a9e529a059f7090bfb58cfbc995401d920 Mon Sep 17 00:00:00 2001 From: Daniel Lemm <61800298+ffe4@users.noreply.github.com> Date: Fri, 13 Mar 2020 21:40:58 +0100 Subject: [PATCH 2/3] Fix PartialMethodObject (WIP) Implemented feedback from PR #1522. Does not pass new tests in test/completion/stdlib.py --- jedi/plugins/stdlib.py | 24 ++++++++++++++---------- test/completion/stdlib.py | 12 ++++++++++++ test/test_inference/test_signature.py | 2 ++ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index 8678051e..eca0232a 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -513,13 +513,12 @@ class PartialObject(object): ) -class PartialMethodObject(object): - def __init__(self, actual_value, arguments): +class PartialMethodObject(ValueWrapper): + def __init__(self, actual_value, arguments, instance=None): + super(PartialMethodObject, self).__init__(actual_value) self._actual_value = actual_value self._arguments = arguments - - def __getattr__(self, name): - return getattr(self._actual_value, name) + self._instance = instance def _get_function(self, unpacked_arguments): key, lazy_value = next(unpacked_arguments, (None, None)) @@ -534,7 +533,9 @@ class PartialMethodObject(object): if func is None: return [] - arg_count = 1 + arg_count = 0 + if self._instance is not None: + arg_count = 1 keys = set() for key, _ in unpacked_arguments: if key is None: @@ -544,7 +545,7 @@ class PartialMethodObject(object): return [PartialSignature(s, arg_count, keys) for s in func.get_signatures()] def py__get__(self, instance, class_value): - return ValueSet([self]) + return ValueSet([PartialMethodObject(self._actual_value, self._arguments, self._instance)]) def py__call__(self, arguments): func = self._get_function(self._arguments.unpack()) @@ -552,7 +553,7 @@ class PartialMethodObject(object): return NO_VALUES return func.execute( - MergedPartialArguments(self._arguments, arguments) + MergedPartialArguments(self._arguments, arguments, self._instance) ) @@ -568,15 +569,18 @@ class PartialSignature(SignatureWrapper): class MergedPartialArguments(AbstractArguments): - def __init__(self, partial_arguments, call_arguments): + def __init__(self, partial_arguments, call_arguments, instance=None): self._partial_arguments = partial_arguments self._call_arguments = call_arguments + self._instance = instance def unpack(self, funcdef=None): unpacked = self._partial_arguments.unpack(funcdef) # Ignore this one, it's the function. It was checked before that it's # there. next(unpacked) + if self._instance is not None: + yield None, LazyKnownValue(self._instance) for key_lazy_value in unpacked: yield key_lazy_value for key_lazy_value in self._call_arguments.unpack(funcdef): @@ -592,7 +596,7 @@ def functools_partial(value, arguments, callback): def functools_partialmethod(value, arguments, callback): return ValueSet( - PartialMethodObject(instance, arguments) + PartialMethodObject(instance, arguments, instance) # FIXME pass correct instance as last arg for instance in value.py__call__(arguments) ) diff --git a/test/completion/stdlib.py b/test/completion/stdlib.py index 4fa5a238..de9d7c8d 100644 --- a/test/completion/stdlib.py +++ b/test/completion/stdlib.py @@ -169,12 +169,24 @@ X().a('')[0] #? str() X().a('')[1] +#? int() +X.a(X(), '')[0] +#? str() +X.a(X(), '')[1] + tup = X().kw(1) #? int() tup[0] #? float() tup[1] +tup = X.kw(X(), 1) +#? int() +tup[0] +#? float() +tup[1] + + def my_decorator(f): @functools.wraps(f) def wrapper(*args, **kwds): diff --git a/test/test_inference/test_signature.py b/test/test_inference/test_signature.py index 967198b3..8fe485d4 100644 --- a/test/test_inference/test_signature.py +++ b/test/test_inference/test_signature.py @@ -100,6 +100,8 @@ class X: (partialmethod_code + 'X().b(', 'func(b, c)'), (partialmethod_code + 'X().c(', 'func(b)'), (partialmethod_code + 'X().d(', None), + (partialmethod_code + 'X.c(', 'func(b)'), + (partialmethod_code + 'X.d(', None), ] ) def test_tree_signature(Script, environment, code, expected): From 2406e583861153335da27a5c098f4fbd7337abb2 Mon Sep 17 00:00:00 2001 From: Daniel Lemm <61800298+ffe4@users.noreply.github.com> Date: Fri, 13 Mar 2020 23:47:48 +0100 Subject: [PATCH 3/3] Refactor stdlib PartialObject Merges PartialObject and PartialMethodObject. Also adds more tests. Some parts are still WIP, see: #1522. Fixes #1519 --- jedi/plugins/stdlib.py | 50 +++++---------------------------------- test/completion/stdlib.py | 11 +++++++++ 2 files changed, 17 insertions(+), 44 deletions(-) diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index eca0232a..c6fc4054 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -473,49 +473,9 @@ def collections_namedtuple(value, arguments, callback): return ValueSet([ClassValue(inference_state, parent_context, generated_class)]) -class PartialObject(object): - def __init__(self, actual_value, arguments): - self._actual_value = actual_value - self._arguments = arguments - - def __getattr__(self, name): - return getattr(self._actual_value, name) - - def _get_function(self, unpacked_arguments): - key, lazy_value = next(unpacked_arguments, (None, None)) - if key is not None or lazy_value is None: - debug.warning("Partial should have a proper function %s", self._arguments) - return None - return lazy_value.infer() - - def get_signatures(self): - unpacked_arguments = self._arguments.unpack() - func = self._get_function(unpacked_arguments) - if func is None: - return [] - - arg_count = 0 - keys = set() - for key, _ in unpacked_arguments: - if key is None: - arg_count += 1 - else: - keys.add(key) - return [PartialSignature(s, arg_count, keys) for s in func.get_signatures()] - - def py__call__(self, arguments): - func = self._get_function(self._arguments.unpack()) - if func is None: - return NO_VALUES - - return func.execute( - MergedPartialArguments(self._arguments, arguments) - ) - - -class PartialMethodObject(ValueWrapper): +class PartialObject(ValueWrapper): def __init__(self, actual_value, arguments, instance=None): - super(PartialMethodObject, self).__init__(actual_value) + super(PartialObject, self).__init__(actual_value) self._actual_value = actual_value self._arguments = arguments self._instance = instance @@ -545,7 +505,7 @@ class PartialMethodObject(ValueWrapper): return [PartialSignature(s, arg_count, keys) for s in func.get_signatures()] def py__get__(self, instance, class_value): - return ValueSet([PartialMethodObject(self._actual_value, self._arguments, self._instance)]) + return ValueSet([PartialObject(self._actual_value, self._arguments, self._instance)]) def py__call__(self, arguments): func = self._get_function(self._arguments.unpack()) @@ -596,7 +556,9 @@ def functools_partial(value, arguments, callback): def functools_partialmethod(value, arguments, callback): return ValueSet( - PartialMethodObject(instance, arguments, instance) # FIXME pass correct instance as last arg + # XXX last argument is a placeholder. See: + # https://github.com/davidhalter/jedi/pull/1522#discussion_r392474671 + PartialObject(instance, arguments, True) for instance in value.py__call__(arguments) ) diff --git a/test/completion/stdlib.py b/test/completion/stdlib.py index de9d7c8d..9de3f839 100644 --- a/test/completion/stdlib.py +++ b/test/completion/stdlib.py @@ -169,6 +169,11 @@ X().a('')[0] #? str() X().a('')[1] +#? int() +X.a('')[0] +#? str() +X.a('')[1] + #? int() X.a(X(), '')[0] #? str() @@ -180,6 +185,12 @@ tup[0] #? float() tup[1] +tup = X.kw(1) +#? int() +tup[0] +#? float() +tup[1] + tup = X.kw(X(), 1) #? int() tup[0]