diff --git a/IPython/core/guarded_eval.py b/IPython/core/guarded_eval.py index c2d88d0..d60a5c5 100644 --- a/IPython/core/guarded_eval.py +++ b/IPython/core/guarded_eval.py @@ -60,7 +60,8 @@ MayHaveGetattr = Union[HasGetAttr, DoesNotHaveGetAttr] def _unbind_method(func: Callable) -> Union[Callable, None]: """Get unbound method for given bound method. - Returns None if cannot get unbound method.""" + Returns None if cannot get unbound method, or method is already unbound. + """ owner = getattr(func, "__self__", None) owner_class = type(owner) name = getattr(func, "__name__", None) @@ -214,7 +215,7 @@ class SelectivePolicy(EvaluationPolicy): accept = False - # Many objects do not have `__getattr__`, this is fine + # Many objects do not have `__getattr__`, this is fine. if has_original_attr is None and has_original_attribute: accept = True else: @@ -234,9 +235,10 @@ class SelectivePolicy(EvaluationPolicy): if not is_property: return True - # Properties in allowed types are ok + # Properties in allowed types are ok (although we do not include any + # properties in our default allow list currently). if type(value) in self.allowed_getattr: - return True + return True # pragma: no cover # Properties in subclasses of allowed types may be ok if not changed for module_name, *access_path in self.allowed_getattr_external: diff --git a/IPython/core/tests/test_guarded_eval.py b/IPython/core/tests/test_guarded_eval.py index 1ee93ff..905cf3a 100644 --- a/IPython/core/tests/test_guarded_eval.py +++ b/IPython/core/tests/test_guarded_eval.py @@ -22,7 +22,6 @@ unsafe = partial(create_context, "unsafe") dangerous = partial(create_context, "dangerous") LIMITED_OR_HIGHER = [limited, unsafe, dangerous] - MINIMAL_OR_HIGHER = [minimal, *LIMITED_OR_HIGHER] @@ -41,6 +40,39 @@ def module_not_installed(module: str): sys.modules[module] = to_restore +def test_external_not_installed(): + """ + Because attribute check requires checking if object is not of allowed + external type, this tests logic for absence of external module. + """ + + class Custom: + def __init__(self): + self.test = 1 + + def __getattr__(self, key): + return key + + with module_not_installed("pandas"): + context = limited(x=Custom()) + with pytest.raises(GuardRejection): + guarded_eval("x.test", context) + + +@dec.skip_without("pandas") +def test_external_changed_api(monkeypatch): + """Check that the execution rejects if external API changed paths""" + import pandas as pd + + series = pd.Series([1], index=["a"]) + + with monkeypatch.context() as m: + m.delattr(pd, "Series") + context = limited(data=series) + with pytest.raises(GuardRejection): + guarded_eval("data.iloc[0]", context) + + @dec.skip_without("pandas") def test_pandas_series_iloc(): import pandas as pd @@ -496,6 +528,7 @@ def test_unbind_method(): x = X() assert _unbind_method(x.index) is X.index assert _unbind_method([].index) is list.index + assert _unbind_method(list.index) is None def test_assumption_instance_attr_do_not_matter():