Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d90a527

Browse files
committed
Implement auto-import and evaluation policy overrides
1 parent e95f1a6 commit d90a527

5 files changed

Lines changed: 112 additions & 16 deletions

File tree

IPython/core/completer.py

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -203,15 +203,11 @@
203203
from typing import (
204204
Iterable,
205205
Iterator,
206-
List,
207-
Tuple,
208206
Union,
209207
Any,
210208
Sequence,
211-
Dict,
212209
Optional,
213210
TYPE_CHECKING,
214-
Set,
215211
Sized,
216212
TypeVar,
217213
Literal,
@@ -236,10 +232,12 @@
236232
List as ListTrait,
237233
Unicode,
238234
Dict as DictTrait,
235+
DottedObjectName,
239236
Union as UnionTrait,
240237
observe,
241238
)
242239
from traitlets.config.configurable import Configurable
240+
from traitlets.utils.importstring import import_item
243241

244242
import __main__
245243

@@ -987,15 +985,17 @@ class Completer(Configurable):
987985
988986
- ``forbidden``: no evaluation of code is permitted,
989987
- ``minimal``: evaluation of literals and access to built-in namespace;
990-
no item/attribute evaluationm no access to locals/globals,
988+
no item/attribute evaluation, no access to locals/globals,
991989
no evaluation of any operations or comparisons.
992990
- ``limited``: access to all namespaces, evaluation of hard-coded methods
993991
(for example: :any:`dict.keys`, :any:`object.__getattr__`,
994992
:any:`object.__getitem__`) on allow-listed objects (for example:
995993
:any:`dict`, :any:`list`, :any:`tuple`, ``pandas.Series``),
996994
- ``unsafe``: evaluation of all methods and function calls but not of
997995
syntax with side-effects like `del x`,
998-
- ``dangerous``: completely arbitrary evaluation.
996+
- ``dangerous``: completely arbitrary evaluation; does not support auto-import.
997+
998+
To override specific elements of the policy, you can use ``policy_overrides`` trait.
999999
""",
10001000
).tag(config=True)
10011001

@@ -1031,6 +1031,35 @@ class Completer(Configurable):
10311031
""",
10321032
).tag(config=True)
10331033

1034+
policy_overrides = DictTrait(
1035+
default_value={},
1036+
key_trait=Unicode(),
1037+
help="""Overrides for policy evaluation.
1038+
1039+
For example, to enable auto-import on completion specify:
1040+
1041+
.. code-block::
1042+
1043+
ipython --Completer.policy_overrides='{"allow_auto_import": True}' --Completer.use_jedi=False
1044+
1045+
""",
1046+
).tag(config=True)
1047+
1048+
auto_import_method = DottedObjectName(
1049+
default_value="importlib.import_module",
1050+
allow_none=True,
1051+
help="""\
1052+
Provisional:
1053+
This is a provisional API in IPython 9.3, it may change without warnings.
1054+
1055+
A fully qualified path to an auto-import method for use by completer.
1056+
The function should take a single string and return `ModuleType` and
1057+
can raise `ImportError` exception if module is not found.
1058+
1059+
The default auto-import implementation does not populate the user namespace with the imported module.
1060+
""",
1061+
).tag(config=True)
1062+
10341063
def __init__(self, namespace=None, global_namespace=None, **kwargs):
10351064
"""Create a new completer for the command line.
10361065
@@ -1279,6 +1308,8 @@ def _evaluate_expr(self, expr):
12791308
globals=self.global_namespace,
12801309
locals=self.namespace,
12811310
evaluation=self.evaluation,
1311+
auto_import=self._auto_import,
1312+
policy_overrides=self.policy_overrides,
12821313
),
12831314
)
12841315
done = True
@@ -1292,6 +1323,14 @@ def _evaluate_expr(self, expr):
12921323
expr = self._trim_expr(expr)
12931324
return obj
12941325

1326+
@property
1327+
def _auto_import(self):
1328+
if self.auto_import_method is None:
1329+
return None
1330+
if not hasattr(self, "_auto_import_func"):
1331+
self._auto_import_func = import_item(self.auto_import_method)
1332+
return self._auto_import_func
1333+
12951334
def get__all__entries(obj):
12961335
"""returns the strings in the __all__ attribute"""
12971336
try:
@@ -2832,6 +2871,8 @@ def dict_key_matches(self, text: str) -> list[str]:
28322871
locals=self.namespace,
28332872
evaluation=self.evaluation, # type: ignore
28342873
in_subscript=True,
2874+
auto_import=self._auto_import,
2875+
policy_overrides=self.policy_overrides,
28352876
),
28362877
)
28372878

IPython/core/guarded_eval.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
1+
from copy import copy
12
from inspect import isclass, signature, Signature
2-
from pygments.formatters.terminal256 import Terminal256Formatter
33
from typing import (
44
Annotated,
55
AnyStr,
66
Callable,
7-
Dict,
87
Literal,
98
NamedTuple,
109
NewType,
1110
Optional,
1211
Protocol,
13-
Set,
1412
Sequence,
15-
Tuple,
16-
Type,
1713
TypeGuard,
1814
Union,
1915
get_args,
@@ -97,6 +93,7 @@ class EvaluationPolicy:
9793
allow_builtins_access: bool = False
9894
allow_all_operations: bool = False
9995
allow_any_calls: bool = False
96+
allow_auto_import: bool = False
10097
allowed_calls: set[Callable] = field(default_factory=set)
10198

10299
def can_get_item(self, value, item):
@@ -330,6 +327,10 @@ class EvaluationContext(NamedTuple):
330327
#: Whether the evaluation of code takes place inside of a subscript.
331328
#: Useful for evaluating ``:-1, 'col'`` in ``df[:-1, 'col']``.
332329
in_subscript: bool = False
330+
#: Auto import method
331+
auto_import: Callable[list[str], ModuleType] | None = None
332+
#: Overrides for evaluation policy
333+
policy_overrides: dict = {}
333334

334335

335336
class _IdentitySubscript:
@@ -463,6 +464,17 @@ def _find_dunder(node_op, dunders) -> Union[tuple[str, ...], None]:
463464
return dunder
464465

465466

467+
def get_policy(context: EvaluationContext) -> EvaluationPolicy:
468+
policy = copy(EVALUATION_POLICIES[context.evaluation])
469+
470+
for key, value in context.policy_overrides.items():
471+
if hasattr(policy, key):
472+
setattr(policy, key, value)
473+
else:
474+
print(f"Incorrect policy override key: {key}")
475+
return policy
476+
477+
466478
def eval_node(node: Union[ast.AST, None], context: EvaluationContext):
467479
"""Evaluate AST node in provided context.
468480
@@ -490,7 +502,8 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext):
490502
The purpose of this function is to guard against unwanted side-effects;
491503
it does not give guarantees on protection from malicious code execution.
492504
"""
493-
policy = EVALUATION_POLICIES[context.evaluation]
505+
policy = get_policy(context)
506+
494507
if node is None:
495508
return None
496509
if isinstance(node, ast.Expression):
@@ -711,14 +724,16 @@ def _resolve_annotation(
711724

712725

713726
def _eval_node_name(node_id: str, context: EvaluationContext):
714-
policy = EVALUATION_POLICIES[context.evaluation]
727+
policy = get_policy(context)
715728
if policy.allow_locals_access and node_id in context.locals:
716729
return context.locals[node_id]
717730
if policy.allow_globals_access and node_id in context.globals:
718731
return context.globals[node_id]
719732
if policy.allow_builtins_access and hasattr(builtins, node_id):
720733
# note: do not use __builtins__, it is implementation detail of cPython
721734
return getattr(builtins, node_id)
735+
if policy.allow_auto_import and context.auto_import:
736+
return context.auto_import(node_id)
722737
if not policy.allow_globals_access and not policy.allow_locals_access:
723738
raise GuardRejection(
724739
f"Namespace access not allowed in {context.evaluation} mode"
@@ -728,7 +743,7 @@ def _eval_node_name(node_id: str, context: EvaluationContext):
728743

729744

730745
def _eval_or_create_duck(duck_type, node: ast.Call, context: EvaluationContext):
731-
policy = EVALUATION_POLICIES[context.evaluation]
746+
policy = get_policy(context)
732747
# if allow-listed builtin is on type annotation, instantiate it
733748
if policy.can_call(duck_type) and not node.keywords:
734749
args = [eval_node(arg, context) for arg in node.args]

IPython/terminal/interactiveshell.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ def _llm_prefix_from_history_changed(self, change):
475475
allow_none=True,
476476
help="""\
477477
Provisional:
478-
This is a provisinal API in IPython 8.32, before stabilisation
478+
This is a provisional API in IPython 8.32, before stabilisation
479479
in 9.0, it may change without warnings.
480480
481481
class to use for the `NavigableAutoSuggestFromHistory` to request

tests/test_completer.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,17 @@ def greedy_completion():
141141

142142

143143
@contextmanager
144-
def evaluation_policy(evaluation: str):
144+
def evaluation_policy(evaluation: str, **overrides):
145145
ip = get_ipython()
146146
evaluation_original = ip.Completer.evaluation
147+
overrides_original = ip.Completer.policy_overrides
147148
try:
148149
ip.Completer.evaluation = evaluation
150+
ip.Completer.policy_overrides = overrides
149151
yield
150152
finally:
151153
ip.Completer.evaluation = evaluation_original
154+
ip.Completer.policy_overrides = overrides_original
152155

153156

154157
@contextmanager
@@ -1380,6 +1383,26 @@ def assert_completion(**kwargs):
13801383
assert_completion(line_buffer="get()['ab")
13811384
assert_completion(line_buffer="get()['abc")
13821385

1386+
def test_completion_autoimport(self):
1387+
ip = get_ipython()
1388+
complete = ip.Completer.complete
1389+
with (
1390+
evaluation_policy("limited", allow_auto_import=True),
1391+
jedi_status(False),
1392+
):
1393+
_, matches = complete(line_buffer="math.")
1394+
self.assertIn(".pi", matches)
1395+
1396+
def test_completion_no_autoimport(self):
1397+
ip = get_ipython()
1398+
complete = ip.Completer.complete
1399+
with (
1400+
evaluation_policy("limited", allow_auto_import=False),
1401+
jedi_status(False),
1402+
):
1403+
_, matches = complete(line_buffer="math.")
1404+
self.assertNotIn(".pi", matches)
1405+
13831406
def test_dict_key_completion_bytes(self):
13841407
"""Test handling of bytes in dict key completion"""
13851408
ip = get_ipython()

tests/test_guarded_eval.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import sys
22
from contextlib import contextmanager
3+
from importlib import import_module
34
from typing import (
45
Annotated,
56
AnyStr,
@@ -780,3 +781,19 @@ def test_module_access():
780781
context = minimal(numpy=numpy)
781782
with pytest.raises(GuardRejection):
782783
guarded_eval("np.linalg.norm", context)
784+
785+
786+
def test_autoimport_module():
787+
context = EvaluationContext(
788+
locals={}, globals={}, evaluation="limited", auto_import=import_module
789+
)
790+
pi = guarded_eval("math.pi", context)
791+
assert round(pi, 2) == 3.14
792+
793+
794+
def test_autoimport_deep_module():
795+
context = EvaluationContext(
796+
locals={}, globals={}, evaluation="limited", auto_import=import_module
797+
)
798+
ElementTree = guarded_eval("xml.etree.ElementTree", context)
799+
assert hasattr(ElementTree, "ElementTree")

0 commit comments

Comments
 (0)