test_guarded_eval.py
785 lines
| 20.5 KiB
| text/x-python
|
PythonLexer
krassowski
|
r28679 | import sys | ||
krassowski
|
r27926 | from contextlib import contextmanager | ||
krassowski
|
r28680 | from typing import ( | ||
Annotated, | ||||
AnyStr, | ||||
NamedTuple, | ||||
Literal, | ||||
NewType, | ||||
Optional, | ||||
Protocol, | ||||
TypeGuard, | ||||
Union, | ||||
TypedDict, | ||||
) | ||||
krassowski
|
r27921 | from functools import partial | ||
krassowski
|
r27913 | from IPython.core.guarded_eval import ( | ||
EvaluationContext, | ||||
GuardRejection, | ||||
guarded_eval, | ||||
krassowski
|
r27918 | _unbind_method, | ||
krassowski
|
r27913 | ) | ||
krassowski
|
r27906 | from IPython.testing import decorators as dec | ||
import pytest | ||||
krassowski
|
r28679 | if sys.version_info < (3, 11): | ||
krassowski
|
r28680 | from typing_extensions import Self, LiteralString | ||
krassowski
|
r28679 | else: | ||
krassowski
|
r28680 | from typing import Self, LiteralString | ||
krassowski
|
r28679 | |||
if sys.version_info < (3, 12): | ||||
from typing_extensions import TypeAliasType | ||||
else: | ||||
from typing import TypeAliasType | ||||
krassowski
|
r27921 | def create_context(evaluation: str, **kwargs): | ||
return EvaluationContext(locals=kwargs, globals={}, evaluation=evaluation) | ||||
krassowski
|
r27906 | |||
krassowski
|
r27921 | forbidden = partial(create_context, "forbidden") | ||
minimal = partial(create_context, "minimal") | ||||
limited = partial(create_context, "limited") | ||||
unsafe = partial(create_context, "unsafe") | ||||
dangerous = partial(create_context, "dangerous") | ||||
LIMITED_OR_HIGHER = [limited, unsafe, dangerous] | ||||
MINIMAL_OR_HIGHER = [minimal, *LIMITED_OR_HIGHER] | ||||
krassowski
|
r27913 | |||
krassowski
|
r27906 | |||
krassowski
|
r27926 | @contextmanager | ||
def module_not_installed(module: str): | ||||
import sys | ||||
try: | ||||
to_restore = sys.modules[module] | ||||
del sys.modules[module] | ||||
except KeyError: | ||||
to_restore = None | ||||
try: | ||||
yield | ||||
finally: | ||||
sys.modules[module] = to_restore | ||||
krassowski
|
r27929 | 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) | ||||
krassowski
|
r27913 | @dec.skip_without("pandas") | ||
krassowski
|
r27906 | def test_pandas_series_iloc(): | ||
import pandas as pd | ||||
krassowski
|
r27913 | |||
series = pd.Series([1], index=["a"]) | ||||
krassowski
|
r27914 | context = limited(data=series) | ||
krassowski
|
r27913 | assert guarded_eval("data.iloc[0]", context) == 1 | ||
krassowski
|
r27906 | |||
krassowski
|
r27926 | def test_rejects_custom_properties(): | ||
class BadProperty: | ||||
@property | ||||
def iloc(self): | ||||
return [None] | ||||
series = BadProperty() | ||||
context = limited(data=series) | ||||
with pytest.raises(GuardRejection): | ||||
guarded_eval("data.iloc[0]", context) | ||||
@dec.skip_without("pandas") | ||||
def test_accepts_non_overriden_properties(): | ||||
import pandas as pd | ||||
class GoodProperty(pd.Series): | ||||
pass | ||||
series = GoodProperty([1], index=["a"]) | ||||
context = limited(data=series) | ||||
assert guarded_eval("data.iloc[0]", context) == 1 | ||||
krassowski
|
r27913 | @dec.skip_without("pandas") | ||
krassowski
|
r27906 | def test_pandas_series(): | ||
import pandas as pd | ||||
krassowski
|
r27913 | |||
krassowski
|
r27914 | context = limited(data=pd.Series([1], index=["a"])) | ||
krassowski
|
r27906 | assert guarded_eval('data["a"]', context) == 1 | ||
with pytest.raises(KeyError): | ||||
guarded_eval('data["c"]', context) | ||||
krassowski
|
r27913 | @dec.skip_without("pandas") | ||
krassowski
|
r27906 | def test_pandas_bad_series(): | ||
import pandas as pd | ||||
krassowski
|
r27913 | |||
krassowski
|
r27906 | class BadItemSeries(pd.Series): | ||
def __getitem__(self, key): | ||||
krassowski
|
r27913 | return "CUSTOM_ITEM" | ||
krassowski
|
r27906 | |||
class BadAttrSeries(pd.Series): | ||||
def __getattr__(self, key): | ||||
krassowski
|
r27913 | return "CUSTOM_ATTR" | ||
krassowski
|
r27906 | |||
krassowski
|
r27913 | bad_series = BadItemSeries([1], index=["a"]) | ||
krassowski
|
r27914 | context = limited(data=bad_series) | ||
krassowski
|
r27906 | |||
with pytest.raises(GuardRejection): | ||||
guarded_eval('data["a"]', context) | ||||
with pytest.raises(GuardRejection): | ||||
guarded_eval('data["c"]', context) | ||||
# note: here result is a bit unexpected because | ||||
# pandas `__getattr__` calls `__getitem__`; | ||||
# FIXME - special case to handle it? | ||||
krassowski
|
r27913 | assert guarded_eval("data.a", context) == "CUSTOM_ITEM" | ||
krassowski
|
r27906 | |||
context = unsafe(data=bad_series) | ||||
krassowski
|
r27913 | assert guarded_eval('data["a"]', context) == "CUSTOM_ITEM" | ||
krassowski
|
r27906 | |||
krassowski
|
r27913 | bad_attr_series = BadAttrSeries([1], index=["a"]) | ||
krassowski
|
r27914 | context = limited(data=bad_attr_series) | ||
krassowski
|
r27906 | assert guarded_eval('data["a"]', context) == 1 | ||
with pytest.raises(GuardRejection): | ||||
krassowski
|
r27913 | guarded_eval("data.a", context) | ||
krassowski
|
r27906 | |||
krassowski
|
r27913 | @dec.skip_without("pandas") | ||
krassowski
|
r27906 | def test_pandas_dataframe_loc(): | ||
import pandas as pd | ||||
from pandas.testing import assert_series_equal | ||||
krassowski
|
r27913 | |||
data = pd.DataFrame([{"a": 1}]) | ||||
krassowski
|
r27914 | context = limited(data=data) | ||
krassowski
|
r27913 | assert_series_equal(guarded_eval('data.loc[:, "a"]', context), data["a"]) | ||
krassowski
|
r27906 | |||
def test_named_tuple(): | ||||
class GoodNamedTuple(NamedTuple): | ||||
a: str | ||||
pass | ||||
class BadNamedTuple(NamedTuple): | ||||
a: str | ||||
krassowski
|
r27913 | |||
krassowski
|
r27906 | def __getitem__(self, key): | ||
return None | ||||
krassowski
|
r27913 | good = GoodNamedTuple(a="x") | ||
bad = BadNamedTuple(a="x") | ||||
krassowski
|
r27906 | |||
krassowski
|
r27914 | context = limited(data=good) | ||
krassowski
|
r27913 | assert guarded_eval("data[0]", context) == "x" | ||
krassowski
|
r27906 | |||
krassowski
|
r27914 | context = limited(data=bad) | ||
krassowski
|
r27906 | with pytest.raises(GuardRejection): | ||
krassowski
|
r27913 | guarded_eval("data[0]", context) | ||
krassowski
|
r27906 | |||
def test_dict(): | ||||
krassowski
|
r27914 | context = limited(data={"a": 1, "b": {"x": 2}, ("x", "y"): 3}) | ||
krassowski
|
r27906 | assert guarded_eval('data["a"]', context) == 1 | ||
krassowski
|
r27913 | assert guarded_eval('data["b"]', context) == {"x": 2} | ||
krassowski
|
r27906 | assert guarded_eval('data["b"]["x"]', context) == 2 | ||
assert guarded_eval('data["x", "y"]', context) == 3 | ||||
krassowski
|
r27913 | assert guarded_eval("data.keys", context) | ||
krassowski
|
r27906 | |||
def test_set(): | ||||
krassowski
|
r27914 | context = limited(data={"a", "b"}) | ||
krassowski
|
r27913 | assert guarded_eval("data.difference", context) | ||
krassowski
|
r27906 | |||
def test_list(): | ||||
krassowski
|
r27914 | context = limited(data=[1, 2, 3]) | ||
krassowski
|
r27913 | assert guarded_eval("data[1]", context) == 2 | ||
assert guarded_eval("data.copy", context) | ||||
krassowski
|
r27906 | |||
def test_dict_literal(): | ||||
krassowski
|
r27914 | context = limited() | ||
krassowski
|
r27913 | assert guarded_eval("{}", context) == {} | ||
krassowski
|
r27906 | assert guarded_eval('{"a": 1}', context) == {"a": 1} | ||
def test_list_literal(): | ||||
krassowski
|
r27914 | context = limited() | ||
krassowski
|
r27913 | assert guarded_eval("[]", context) == [] | ||
krassowski
|
r27906 | assert guarded_eval('[1, "a"]', context) == [1, "a"] | ||
def test_set_literal(): | ||||
krassowski
|
r27914 | context = limited() | ||
krassowski
|
r27913 | assert guarded_eval("set()", context) == set() | ||
krassowski
|
r27906 | assert guarded_eval('{"a"}', context) == {"a"} | ||
krassowski
|
r27921 | def test_evaluates_if_expression(): | ||
krassowski
|
r27914 | context = limited() | ||
krassowski
|
r27913 | assert guarded_eval("2 if True else 3", context) == 2 | ||
assert guarded_eval("4 if False else 5", context) == 5 | ||||
krassowski
|
r27906 | |||
def test_object(): | ||||
obj = object() | ||||
krassowski
|
r27914 | context = limited(obj=obj) | ||
krassowski
|
r27913 | assert guarded_eval("obj.__dir__", context) == obj.__dir__ | ||
krassowski
|
r27906 | |||
@pytest.mark.parametrize( | ||||
"code,expected", | ||||
[ | ||||
krassowski
|
r27913 | ["int.numerator", int.numerator], | ||
["float.is_integer", float.is_integer], | ||||
["complex.real", complex.real], | ||||
], | ||||
krassowski
|
r27906 | ) | ||
def test_number_attributes(code, expected): | ||||
krassowski
|
r27914 | assert guarded_eval(code, limited()) == expected | ||
krassowski
|
r27906 | |||
def test_method_descriptor(): | ||||
krassowski
|
r27914 | context = limited() | ||
krassowski
|
r27913 | assert guarded_eval("list.copy.__name__", context) == "copy" | ||
krassowski
|
r27906 | |||
krassowski
|
r28433 | class HeapType: | ||
pass | ||||
class CallCreatesHeapType: | ||||
def __call__(self) -> HeapType: | ||||
return HeapType() | ||||
class CallCreatesBuiltin: | ||||
def __call__(self) -> frozenset: | ||||
return frozenset() | ||||
krassowski
|
r28675 | class HasStaticMethod: | ||
@staticmethod | ||||
def static_method() -> HeapType: | ||||
return HeapType() | ||||
class InitReturnsFrozenset: | ||||
def __new__(self) -> frozenset: # type:ignore[misc] | ||||
return frozenset() | ||||
krassowski
|
r28676 | class StringAnnotation: | ||
def heap(self) -> "HeapType": | ||||
return HeapType() | ||||
def copy(self) -> "StringAnnotation": | ||||
return StringAnnotation() | ||||
krassowski
|
r28678 | CustomIntType = NewType("CustomIntType", int) | ||
CustomHeapType = NewType("CustomHeapType", HeapType) | ||||
IntTypeAlias = TypeAliasType("IntTypeAlias", int) | ||||
HeapTypeAlias = TypeAliasType("HeapTypeAlias", HeapType) | ||||
krassowski
|
r28680 | class TestProtocol(Protocol): | ||
def test_method(self) -> bool: | ||||
pass | ||||
class TestProtocolImplementer(TestProtocol): | ||||
def test_method(self) -> bool: | ||||
return True | ||||
class Movie(TypedDict): | ||||
name: str | ||||
year: int | ||||
krassowski
|
r28678 | class SpecialTyping: | ||
def custom_int_type(self) -> CustomIntType: | ||||
return CustomIntType(1) | ||||
def custom_heap_type(self) -> CustomHeapType: | ||||
return CustomHeapType(HeapType()) | ||||
# TODO: remove type:ignore comment once mypy | ||||
# supports explicit calls to `TypeAliasType`, see: | ||||
# https://github.com/python/mypy/issues/16614 | ||||
def int_type_alias(self) -> IntTypeAlias: # type:ignore[valid-type] | ||||
return 1 | ||||
def heap_type_alias(self) -> HeapTypeAlias: # type:ignore[valid-type] | ||||
return 1 | ||||
def literal(self) -> Literal[False]: | ||||
return False | ||||
krassowski
|
r28680 | def literal_string(self) -> LiteralString: | ||
return "test" | ||||
krassowski
|
r28678 | def self(self) -> Self: | ||
return self | ||||
krassowski
|
r28680 | def any_str(self, x: AnyStr) -> AnyStr: | ||
return x | ||||
def annotated(self) -> Annotated[float, "positive number"]: | ||||
return 1 | ||||
def annotated_self(self) -> Annotated[Self, "self with metadata"]: | ||||
self._metadata = "test" | ||||
return self | ||||
def int_type_guard(self, x) -> TypeGuard[int]: | ||||
return isinstance(x, int) | ||||
def optional_float(self) -> Optional[float]: | ||||
return 1.0 | ||||
def union_str_and_int(self) -> Union[str, int]: | ||||
return "" | ||||
def protocol(self) -> TestProtocol: | ||||
return TestProtocolImplementer() | ||||
def typed_dict(self) -> Movie: | ||||
return {"name": "The Matrix", "year": 1999} | ||||
krassowski
|
r28678 | |||
krassowski
|
r27906 | @pytest.mark.parametrize( | ||
krassowski
|
r28680 | "data,code,expected,equality", | ||
krassowski
|
r27906 | [ | ||
krassowski
|
r28675 | [[1, 2, 3], "data.index(2)", 1, True], | ||
[{"a": 1}, "data.keys().isdisjoint({})", True, True], | ||||
krassowski
|
r28676 | [StringAnnotation(), "data.heap()", HeapType, False], | ||
[StringAnnotation(), "data.copy()", StringAnnotation, False], | ||||
krassowski
|
r28675 | # test cases for `__call__` | ||
[CallCreatesHeapType(), "data()", HeapType, False], | ||||
[CallCreatesBuiltin(), "data()", frozenset, False], | ||||
# Test cases for `__init__` | ||||
[HeapType, "data()", HeapType, False], | ||||
[InitReturnsFrozenset, "data()", frozenset, False], | ||||
[HeapType(), "data.__class__()", HeapType, False], | ||||
krassowski
|
r28678 | # supported special cases for typing | ||
[SpecialTyping(), "data.custom_int_type()", int, False], | ||||
[SpecialTyping(), "data.custom_heap_type()", HeapType, False], | ||||
[SpecialTyping(), "data.int_type_alias()", int, False], | ||||
[SpecialTyping(), "data.heap_type_alias()", HeapType, False], | ||||
[SpecialTyping(), "data.self()", SpecialTyping, False], | ||||
[SpecialTyping(), "data.literal()", False, True], | ||||
krassowski
|
r28680 | [SpecialTyping(), "data.literal_string()", str, False], | ||
[SpecialTyping(), "data.any_str('a')", str, False], | ||||
[SpecialTyping(), "data.any_str(b'a')", bytes, False], | ||||
[SpecialTyping(), "data.annotated()", float, False], | ||||
[SpecialTyping(), "data.annotated_self()", SpecialTyping, False], | ||||
[SpecialTyping(), "data.int_type_guard()", int, False], | ||||
krassowski
|
r28676 | # test cases for static methods | ||
krassowski
|
r28675 | [HasStaticMethod, "data.static_method()", HeapType, False], | ||
krassowski
|
r27913 | ], | ||
krassowski
|
r27906 | ) | ||
krassowski
|
r28680 | def test_evaluates_calls(data, code, expected, equality): | ||
krassowski
|
r28676 | context = limited(data=data, HeapType=HeapType, StringAnnotation=StringAnnotation) | ||
krassowski
|
r28680 | value = guarded_eval(code, context) | ||
krassowski
|
r28433 | if equality: | ||
assert value == expected | ||||
else: | ||||
assert isinstance(value, expected) | ||||
krassowski
|
r27906 | |||
krassowski
|
r28675 | |||
@pytest.mark.parametrize( | ||||
krassowski
|
r28680 | "data,code,expected_attributes", | ||
[ | ||||
[SpecialTyping(), "data.optional_float()", ["is_integer"]], | ||||
[ | ||||
SpecialTyping(), | ||||
"data.union_str_and_int()", | ||||
["capitalize", "as_integer_ratio"], | ||||
], | ||||
[SpecialTyping(), "data.protocol()", ["test_method"]], | ||||
[SpecialTyping(), "data.typed_dict()", ["keys", "values", "items"]], | ||||
], | ||||
) | ||||
def test_mocks_attributes_of_call_results(data, code, expected_attributes): | ||||
context = limited(data=data, HeapType=HeapType, StringAnnotation=StringAnnotation) | ||||
result = guarded_eval(code, context) | ||||
for attr in expected_attributes: | ||||
assert hasattr(result, attr) | ||||
assert attr in dir(result) | ||||
@pytest.mark.parametrize( | ||||
"data,code,expected_items", | ||||
[ | ||||
[SpecialTyping(), "data.typed_dict()", {"year": int, "name": str}], | ||||
], | ||||
) | ||||
def test_mocks_items_of_call_results(data, code, expected_items): | ||||
context = limited(data=data, HeapType=HeapType, StringAnnotation=StringAnnotation) | ||||
result = guarded_eval(code, context) | ||||
ipython_keys = result._ipython_key_completions_() | ||||
for key, value in expected_items.items(): | ||||
assert isinstance(result[key], value) | ||||
assert key in ipython_keys | ||||
@pytest.mark.parametrize( | ||||
krassowski
|
r28675 | "data,bad", | ||
[ | ||||
[[1, 2, 3], "data.append(4)"], | ||||
[{"a": 1}, "data.update()"], | ||||
], | ||||
) | ||||
def test_rejects_calls_with_side_effects(data, bad): | ||||
context = limited(data=data) | ||||
krassowski
|
r27906 | with pytest.raises(GuardRejection): | ||
guarded_eval(bad, context) | ||||
@pytest.mark.parametrize( | ||||
"code,expected", | ||||
[ | ||||
krassowski
|
r27913 | ["(1\n+\n1)", 2], | ||
["list(range(10))[-1:]", [9]], | ||||
["list(range(20))[3:-2:3]", [3, 6, 9, 12, 15]], | ||||
], | ||||
krassowski
|
r27906 | ) | ||
krassowski
|
r27921 | @pytest.mark.parametrize("context", LIMITED_OR_HIGHER) | ||
def test_evaluates_complex_cases(code, expected, context): | ||||
assert guarded_eval(code, context()) == expected | ||||
@pytest.mark.parametrize( | ||||
"code,expected", | ||||
[ | ||||
["1", 1], | ||||
["1.0", 1.0], | ||||
["0xdeedbeef", 0xDEEDBEEF], | ||||
["True", True], | ||||
["None", None], | ||||
["{}", {}], | ||||
["[]", []], | ||||
], | ||||
) | ||||
@pytest.mark.parametrize("context", MINIMAL_OR_HIGHER) | ||||
def test_evaluates_literals(code, expected, context): | ||||
assert guarded_eval(code, context()) == expected | ||||
krassowski
|
r27906 | |||
krassowski
|
r27920 | @pytest.mark.parametrize( | ||
"code,expected", | ||||
[ | ||||
["-5", -5], | ||||
["+5", +5], | ||||
["~5", -6], | ||||
], | ||||
) | ||||
krassowski
|
r27921 | @pytest.mark.parametrize("context", LIMITED_OR_HIGHER) | ||
def test_evaluates_unary_operations(code, expected, context): | ||||
assert guarded_eval(code, context()) == expected | ||||
krassowski
|
r27920 | |||
@pytest.mark.parametrize( | ||||
"code,expected", | ||||
[ | ||||
["1 + 1", 2], | ||||
["3 - 1", 2], | ||||
["2 * 3", 6], | ||||
["5 // 2", 2], | ||||
["5 / 2", 2.5], | ||||
["5**2", 25], | ||||
["2 >> 1", 1], | ||||
["2 << 1", 4], | ||||
["1 | 2", 3], | ||||
["1 & 1", 1], | ||||
["1 & 2", 0], | ||||
], | ||||
) | ||||
krassowski
|
r27921 | @pytest.mark.parametrize("context", LIMITED_OR_HIGHER) | ||
def test_evaluates_binary_operations(code, expected, context): | ||||
assert guarded_eval(code, context()) == expected | ||||
krassowski
|
r27920 | |||
@pytest.mark.parametrize( | ||||
"code,expected", | ||||
[ | ||||
["2 > 1", True], | ||||
["2 < 1", False], | ||||
["2 <= 1", False], | ||||
["2 <= 2", True], | ||||
["1 >= 2", False], | ||||
["2 >= 2", True], | ||||
["2 == 2", True], | ||||
["1 == 2", False], | ||||
["1 != 2", True], | ||||
["1 != 1", False], | ||||
["1 < 4 < 3", False], | ||||
["(1 < 4) < 3", True], | ||||
["4 > 3 > 2 > 1", True], | ||||
["4 > 3 > 2 > 9", False], | ||||
["1 < 2 < 3 < 4", True], | ||||
["9 < 2 < 3 < 4", False], | ||||
["1 < 2 > 1 > 0 > -1 < 1", True], | ||||
["1 in [1] in [[1]]", True], | ||||
["1 in [1] in [[2]]", False], | ||||
["1 in [1]", True], | ||||
["0 in [1]", False], | ||||
["1 not in [1]", False], | ||||
["0 not in [1]", True], | ||||
["True is True", True], | ||||
["False is False", True], | ||||
["True is False", False], | ||||
krassowski
|
r27921 | ["True is not True", False], | ||
["False is not True", True], | ||||
krassowski
|
r27920 | ], | ||
) | ||||
krassowski
|
r27921 | @pytest.mark.parametrize("context", LIMITED_OR_HIGHER) | ||
def test_evaluates_comparisons(code, expected, context): | ||||
assert guarded_eval(code, context()) == expected | ||||
def test_guards_comparisons(): | ||||
class GoodEq(int): | ||||
pass | ||||
class BadEq(int): | ||||
def __eq__(self, other): | ||||
assert False | ||||
context = limited(bad=BadEq(1), good=GoodEq(1)) | ||||
with pytest.raises(GuardRejection): | ||||
guarded_eval("bad == 1", context) | ||||
with pytest.raises(GuardRejection): | ||||
guarded_eval("bad != 1", context) | ||||
with pytest.raises(GuardRejection): | ||||
guarded_eval("1 == bad", context) | ||||
with pytest.raises(GuardRejection): | ||||
guarded_eval("1 != bad", context) | ||||
assert guarded_eval("good == 1", context) is True | ||||
assert guarded_eval("good != 1", context) is False | ||||
assert guarded_eval("1 == good", context) is True | ||||
assert guarded_eval("1 != good", context) is False | ||||
def test_guards_unary_operations(): | ||||
class GoodOp(int): | ||||
pass | ||||
class BadOpInv(int): | ||||
def __inv__(self, other): | ||||
assert False | ||||
class BadOpInverse(int): | ||||
def __inv__(self, other): | ||||
assert False | ||||
context = limited(good=GoodOp(1), bad1=BadOpInv(1), bad2=BadOpInverse(1)) | ||||
with pytest.raises(GuardRejection): | ||||
guarded_eval("~bad1", context) | ||||
with pytest.raises(GuardRejection): | ||||
guarded_eval("~bad2", context) | ||||
def test_guards_binary_operations(): | ||||
class GoodOp(int): | ||||
pass | ||||
krassowski
|
r27920 | |||
krassowski
|
r27921 | class BadOp(int): | ||
def __add__(self, other): | ||||
assert False | ||||
krassowski
|
r27920 | |||
krassowski
|
r27921 | context = limited(good=GoodOp(1), bad=BadOp(1)) | ||
with pytest.raises(GuardRejection): | ||||
guarded_eval("1 + bad", context) | ||||
with pytest.raises(GuardRejection): | ||||
guarded_eval("bad + 1", context) | ||||
assert guarded_eval("good + 1", context) == 2 | ||||
assert guarded_eval("1 + good", context) == 2 | ||||
def test_guards_attributes(): | ||||
class GoodAttr(float): | ||||
pass | ||||
class BadAttr1(float): | ||||
def __getattr__(self, key): | ||||
assert False | ||||
class BadAttr2(float): | ||||
def __getattribute__(self, key): | ||||
assert False | ||||
context = limited(good=GoodAttr(0.5), bad1=BadAttr1(0.5), bad2=BadAttr2(0.5)) | ||||
with pytest.raises(GuardRejection): | ||||
guarded_eval("bad1.as_integer_ratio", context) | ||||
with pytest.raises(GuardRejection): | ||||
guarded_eval("bad2.as_integer_ratio", context) | ||||
assert guarded_eval("good.as_integer_ratio()", context) == (1, 2) | ||||
@pytest.mark.parametrize("context", MINIMAL_OR_HIGHER) | ||||
def test_access_builtins(context): | ||||
assert guarded_eval("round", context()) == round | ||||
def test_access_builtins_fails(): | ||||
krassowski
|
r27915 | context = limited() | ||
krassowski
|
r27921 | with pytest.raises(NameError): | ||
guarded_eval("this_is_not_builtin", context) | ||||
def test_rejects_forbidden(): | ||||
context = forbidden() | ||||
with pytest.raises(GuardRejection): | ||||
guarded_eval("1", context) | ||||
def test_guards_locals_and_globals(): | ||||
context = EvaluationContext( | ||||
locals={"local_a": "a"}, globals={"global_b": "b"}, evaluation="minimal" | ||||
) | ||||
with pytest.raises(GuardRejection): | ||||
guarded_eval("local_a", context) | ||||
with pytest.raises(GuardRejection): | ||||
guarded_eval("global_b", context) | ||||
def test_access_locals_and_globals(): | ||||
context = EvaluationContext( | ||||
locals={"local_a": "a"}, globals={"global_b": "b"}, evaluation="limited" | ||||
) | ||||
assert guarded_eval("local_a", context) == "a" | ||||
assert guarded_eval("global_b", context) == "b" | ||||
@pytest.mark.parametrize( | ||||
"code", | ||||
["def func(): pass", "class C: pass", "x = 1", "x += 1", "del x", "import ast"], | ||||
) | ||||
@pytest.mark.parametrize("context", [minimal(), limited(), unsafe()]) | ||||
def test_rejects_side_effect_syntax(code, context): | ||||
with pytest.raises(SyntaxError): | ||||
guarded_eval(code, context) | ||||
krassowski
|
r27915 | |||
krassowski
|
r27906 | def test_subscript(): | ||
context = EvaluationContext( | ||||
krassowski
|
r27918 | locals={}, globals={}, evaluation="limited", in_subscript=True | ||
krassowski
|
r27906 | ) | ||
empty_slice = slice(None, None, None) | ||||
krassowski
|
r27913 | assert guarded_eval("", context) == tuple() | ||
assert guarded_eval(":", context) == empty_slice | ||||
assert guarded_eval("1:2:3", context) == slice(1, 2, 3) | ||||
krassowski
|
r27906 | assert guarded_eval(':, "a"', context) == (empty_slice, "a") | ||
def test_unbind_method(): | ||||
class X(list): | ||||
def index(self, k): | ||||
krassowski
|
r27913 | return "CUSTOM" | ||
krassowski
|
r27906 | x = X() | ||
krassowski
|
r27918 | assert _unbind_method(x.index) is X.index | ||
assert _unbind_method([].index) is list.index | ||||
krassowski
|
r27929 | assert _unbind_method(list.index) is None | ||
krassowski
|
r27906 | |||
def test_assumption_instance_attr_do_not_matter(): | ||||
"""This is semi-specified in Python documentation. | ||||
krassowski
|
r28433 | However, since the specification says 'not guaranteed | ||
krassowski
|
r27906 | 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. | ||||
""" | ||||
krassowski
|
r27913 | |||
krassowski
|
r27906 | class T: | ||
def __getitem__(self, k): | ||||
krassowski
|
r27913 | return "a" | ||
krassowski
|
r27906 | def __getattr__(self, k): | ||
krassowski
|
r27913 | return "a" | ||
krassowski
|
r27926 | def f(self): | ||
return "b" | ||||
krassowski
|
r27906 | t = T() | ||
krassowski
|
r27926 | t.__getitem__ = f | ||
t.__getattr__ = f | ||||
krassowski
|
r27913 | assert t[1] == "a" | ||
assert t[1] == "a" | ||||
krassowski
|
r27906 | |||
def test_assumption_named_tuples_share_getitem(): | ||||
"""Check assumption on named tuples sharing __getitem__""" | ||||
from typing import NamedTuple | ||||
class A(NamedTuple): | ||||
pass | ||||
class B(NamedTuple): | ||||
pass | ||||
assert A.__getitem__ == B.__getitem__ | ||||
Carlos Cordoba
|
r28227 | |||
@dec.skip_without("numpy") | ||||
def test_module_access(): | ||||
import numpy | ||||
context = limited(numpy=numpy) | ||||
assert guarded_eval("numpy.linalg.norm", context) == numpy.linalg.norm | ||||
context = minimal(numpy=numpy) | ||||
with pytest.raises(GuardRejection): | ||||
guarded_eval("np.linalg.norm", context) | ||||