Skip to content

Commit ee7ad52

Browse files
authored
Fallback to type annotations for attribute completions (ipython#15079)
Fixes ipython#14917 Enable IPython tab completer to use type annotations when attribute access is restricted by the evaluation policy.
2 parents d4cd689 + 4e12cce commit ee7ad52

File tree

2 files changed

+50
-1
lines changed

2 files changed

+50
-1
lines changed

IPython/core/guarded_eval.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from IPython.utils.decorators import undoc
3232

3333

34-
from typing import Self, LiteralString
34+
from typing import Self, LiteralString, get_type_hints
3535

3636
if sys.version_info < (3, 12):
3737
from typing_extensions import TypeAliasType
@@ -1036,6 +1036,22 @@ def dummy_function(*args, **kwargs):
10361036
value = eval_node(node.value, context)
10371037
if policy.can_get_attr(value, node.attr):
10381038
return getattr(value, node.attr)
1039+
try:
1040+
cls = (
1041+
value if isinstance(value, type) else getattr(value, "__class__", None)
1042+
)
1043+
if cls is not None:
1044+
resolved_hints = get_type_hints(
1045+
cls,
1046+
globalns=(context.globals or {}),
1047+
localns=(context.locals or {}),
1048+
)
1049+
if node.attr in resolved_hints:
1050+
annotated = resolved_hints[node.attr]
1051+
return _resolve_annotation(annotated, context)
1052+
except Exception:
1053+
# Fall through to the guard rejection
1054+
pass
10391055
raise GuardRejection(
10401056
"Attribute access (`__getattr__`) for",
10411057
type(value), # not joined to avoid calling `repr`

tests/test_completer.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1572,6 +1572,39 @@ def __getattr__(self, attr):
15721572
self.assertNotIn(".append", matches)
15731573
self.assertNotIn(".keys", matches)
15741574

1575+
def test_completion_fallback_to_annotation_for_attribute(self):
1576+
code = textwrap.dedent(
1577+
"""
1578+
class StringMethods:
1579+
def a():
1580+
pass
1581+
1582+
class Test:
1583+
str: StringMethods
1584+
def __init__(self):
1585+
self.str = StringMethods()
1586+
def __getattr__(self, name):
1587+
raise AttributeError(f"{name} not found")
1588+
"""
1589+
)
1590+
1591+
repro = types.ModuleType("repro")
1592+
sys.modules["repro"] = repro
1593+
exec(code, repro.__dict__)
1594+
1595+
ip = get_ipython()
1596+
ip.user_ns["repro"] = repro
1597+
exec("r = repro.Test()", ip.user_ns)
1598+
1599+
complete = ip.Completer.complete
1600+
try:
1601+
with evaluation_policy("limited"), jedi_status(False):
1602+
_, matches = complete(line_buffer="r.str.")
1603+
self.assertIn(".a", matches)
1604+
finally:
1605+
sys.modules.pop("repro", None)
1606+
ip.user_ns.pop("r", None)
1607+
15751608
def test_policy_warnings(self):
15761609
with self.assertWarns(
15771610
UserWarning,

0 commit comments

Comments
 (0)