Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support completion based on type annotations of calls #14185

Merged
merged 5 commits into from
Oct 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 27 additions & 0 deletions IPython/core/guarded_eval.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from inspect import signature, Signature
from typing import (
Any,
Callable,
Expand Down Expand Up @@ -335,6 +336,7 @@ def __getitem__(self, key):

IDENTITY_SUBSCRIPT = _IdentitySubscript()
SUBSCRIPT_MARKER = "__SUBSCRIPT_SENTINEL__"
UNKNOWN_SIGNATURE = Signature()


class GuardRejection(Exception):
Expand Down Expand Up @@ -415,6 +417,10 @@ def guarded_eval(code: str, context: EvaluationContext):
}


class Duck:
"""A dummy class used to create objects of other classes without calling their ``__init__``"""


def _find_dunder(node_op, dunders) -> Union[Tuple[str, ...], None]:
dunder = None
for op, candidate_dunder in dunders.items():
Expand Down Expand Up @@ -584,6 +590,27 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext):
if policy.can_call(func) and not node.keywords:
args = [eval_node(arg, context) for arg in node.args]
return func(*args)
try:
sig = signature(func)
except ValueError:
sig = UNKNOWN_SIGNATURE
# if annotation was not stringized, or it was stringized
# but resolved by signature call we know the return type
not_empty = sig.return_annotation is not Signature.empty
not_stringized = not isinstance(sig.return_annotation, str)
if not_empty and not_stringized:
duck = Duck()
# if allow-listed builtin is on type annotation, instantiate it
if policy.can_call(sig.return_annotation) and not node.keywords:
args = [eval_node(arg, context) for arg in node.args]
return sig.return_annotation(*args)
try:
# if custom class is in type annotation, mock it;
# this only works for heap types, not builtins
duck.__class__ = sig.return_annotation
return duck
except TypeError:
pass
raise GuardRejection(
"Call for",
func, # not joined to avoid calling `repr`
Expand Down
32 changes: 26 additions & 6 deletions IPython/core/tests/test_guarded_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,16 +253,36 @@ def test_method_descriptor():
assert guarded_eval("list.copy.__name__", context) == "copy"


class HeapType:
pass


class CallCreatesHeapType:
def __call__(self) -> HeapType:
return HeapType()


class CallCreatesBuiltin:
def __call__(self) -> frozenset:
return frozenset()


@pytest.mark.parametrize(
"data,good,bad,expected",
"data,good,bad,expected, equality",
[
[[1, 2, 3], "data.index(2)", "data.append(4)", 1],
[{"a": 1}, "data.keys().isdisjoint({})", "data.update()", True],
[[1, 2, 3], "data.index(2)", "data.append(4)", 1, True],
[{"a": 1}, "data.keys().isdisjoint({})", "data.update()", True, True],
[CallCreatesHeapType(), "data()", "data.__class__()", HeapType, False],
[CallCreatesBuiltin(), "data()", "data.__class__()", frozenset, False],
],
)
def test_evaluates_calls(data, good, bad, expected):
def test_evaluates_calls(data, good, bad, expected, equality):
context = limited(data=data)
assert guarded_eval(good, context) == expected
value = guarded_eval(good, context)
if equality:
assert value == expected
else:
assert isinstance(value, expected)

with pytest.raises(GuardRejection):
guarded_eval(bad, context)
Expand Down Expand Up @@ -534,7 +554,7 @@ def index(self, k):
def test_assumption_instance_attr_do_not_matter():
"""This is semi-specified in Python documentation.

However, since the specification says 'not guaranted
However, since the specification says 'not guaranteed
to work' rather than 'is forbidden to work', future
versions could invalidate this assumptions. This test
is meant to catch such a change if it ever comes true.
Expand Down