diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index f3ebd35d..92e8c5b2 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -471,13 +471,12 @@ def collections_namedtuple(value, arguments, callback): return ValueSet([ClassValue(inference_state, parent_context, generated_class)]) -class PartialObject(object): - def __init__(self, actual_value, arguments): +class PartialObject(ValueWrapper): + def __init__(self, actual_value, arguments, instance=None): + super(PartialObject, 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)) @@ -493,6 +492,8 @@ class PartialObject(object): return [] arg_count = 0 + if self._instance is not None: + arg_count = 1 keys = set() for key, _ in unpacked_arguments: if key is None: @@ -501,13 +502,16 @@ class PartialObject(object): keys.add(key) return [PartialSignature(s, arg_count, keys) for s in func.get_signatures()] + def py__get__(self, instance, class_value): + return ValueSet([PartialObject(self._actual_value, self._arguments, self._instance)]) + 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) + MergedPartialArguments(self._arguments, arguments, self._instance) ) @@ -523,15 +527,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): @@ -545,6 +552,15 @@ def functools_partial(value, arguments, callback): ) +def functools_partialmethod(value, arguments, callback): + return ValueSet( + # 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) + ) + + @argument_clinic('first, /') def _return_first_param(firsts): return firsts @@ -742,6 +758,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..9de3f839 100644 --- a/test/completion/stdlib.py +++ b/test/completion/stdlib.py @@ -158,6 +158,46 @@ 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] + +#? int() +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(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 186b5770..8fe485d4 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,13 @@ 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), + (partialmethod_code + 'X.c(', 'func(b)'), + (partialmethod_code + 'X.d(', None), ] ) def test_tree_signature(Script, environment, code, expected):