Repository: ipython/ipython

Issue:
Do not warn repeatedly on invalid completer policy overrides
Currently when user sets a completer policy override and then changes evaluation mode to one which does not support given override, they get a warning each time they use a completer.

```python
%config IPCompleter.evaluation = 'limited'
%config IPCompleter.policy_overrides = {'allowed_getattr_external': []}
%config IPCompleter.evaluation = 'unsafe'
# see warnings from now on Tab
```

Instead, the warning should be displayed only once, ideally when changing the evaluation policy.


Discussion:

Patch:
diff --git a/IPython/core/completer.py b/IPython/core/completer.py
index 520a326ba00..5c7330f4974 100644
--- a/IPython/core/completer.py
+++ b/IPython/core/completer.py
@@ -213,7 +213,11 @@
     Literal,
 )
 
-from IPython.core.guarded_eval import guarded_eval, EvaluationContext
+from IPython.core.guarded_eval import (
+    guarded_eval,
+    EvaluationContext,
+    _validate_policy_overrides,
+)
 from IPython.core.error import TryNext
 from IPython.core.inputtransformer2 import ESC_MAGIC
 from IPython.core.latex_symbols import latex_symbols, reverse_latex_symbol
@@ -952,7 +956,6 @@ def split_line(self, line, cursor_pos=None):
         return self._delim_re.split(cut_line)[-1]
 
 
-
 class Completer(Configurable):
 
     greedy = Bool(
@@ -1044,6 +1047,18 @@ class Completer(Configurable):
         """,
     ).tag(config=True)
 
+    @observe("evaluation")
+    def _evaluation_changed(self, _change):
+        _validate_policy_overrides(
+            policy_name=self.evaluation, policy_overrides=self.policy_overrides
+        )
+
+    @observe("policy_overrides")
+    def _policy_overrides_changed(self, _change):
+        _validate_policy_overrides(
+            policy_name=self.evaluation, policy_overrides=self.policy_overrides
+        )
+
     auto_import_method = DottedObjectName(
         default_value="importlib.import_module",
         allow_none=True,
diff --git a/IPython/core/guarded_eval.py b/IPython/core/guarded_eval.py
index 3d667b769a4..3789317095d 100644
--- a/IPython/core/guarded_eval.py
+++ b/IPython/core/guarded_eval.py
@@ -21,6 +21,7 @@
 import collections
 import operator
 import sys
+import warnings
 from functools import cached_property
 from dataclasses import dataclass, field
 from types import MethodDescriptorType, ModuleType
@@ -349,15 +350,16 @@ class _DummyNamedTuple(NamedTuple):
     """Used internally to retrieve methods of named tuple instance."""
 
 
+EvaluationPolicyName = Literal["forbidden", "minimal", "limited", "unsafe", "dangerous"]
+
+
 class EvaluationContext(NamedTuple):
     #: Local namespace
     locals: dict
     #: Global namespace
     globals: dict
     #: Evaluation policy identifier
-    evaluation: Literal["forbidden", "minimal", "limited", "unsafe", "dangerous"] = (
-        "forbidden"
-    )
+    evaluation: EvaluationPolicyName = "forbidden"
     #: Whether the evaluation of code takes place inside of a subscript.
     #: Useful for evaluating ``:-1, 'col'`` in ``df[:-1, 'col']``.
     in_subscript: bool = False
@@ -504,11 +506,24 @@ def get_policy(context: EvaluationContext) -> EvaluationPolicy:
     for key, value in context.policy_overrides.items():
         if hasattr(policy, key):
             setattr(policy, key, value)
-        else:
-            print(f"Incorrect policy override key: {key}")
     return policy
 
 
+def _validate_policy_overrides(
+    policy_name: EvaluationPolicyName, policy_overrides: dict
+) -> bool:
+    policy = EVALUATION_POLICIES[policy_name]
+
+    all_good = True
+    for key, value in policy_overrides.items():
+        if not hasattr(policy, key):
+            warnings.warn(
+                f"Override {key!r} is not valid with {policy_name!r} evaluation policy"
+            )
+            all_good = False
+    return all_good
+
+
 def eval_node(node: Union[ast.AST, None], context: EvaluationContext):
     """Evaluate AST node in provided context.
 


Test patch:
diff --git a/tests/test_completer.py b/tests/test_completer.py
index 044bcc3c397..fd329efa0fe 100644
--- a/tests/test_completer.py
+++ b/tests/test_completer.py
@@ -1456,6 +1456,21 @@ def __getattr__(self, attr):
             _, matches = complete(line_buffer="unsafe_list_factory.example.")
             self.assertNotIn(".append", matches)
 
+    def test_policy_warnings(self):
+        with self.assertWarns(
+            UserWarning,
+            msg="Override 'allowed_getattr_external' is not valid with 'unsafe' evaluation policy",
+        ):
+            with evaluation_policy("unsafe", allowed_getattr_external=[]):
+                pass
+
+        with self.assertWarns(
+            UserWarning,
+            msg="Override 'test' is not valid with 'limited' evaluation policy",
+        ):
+            with evaluation_policy("limited", test=[]):
+                pass
+
     def test_dict_key_completion_bytes(self):
         """Test handling of bytes in dict key completion"""
         ip = get_ipython()