##// END OF EJS Templates
Guard against custom properties
krassowski -
Show More
@@ -1,695 +1,738 b''
1 from typing import (
1 from typing import (
2 Any,
2 Any,
3 Callable,
3 Callable,
4 Dict,
4 Dict,
5 Set,
5 Set,
6 Sequence,
6 Tuple,
7 Tuple,
7 NamedTuple,
8 NamedTuple,
8 Type,
9 Type,
9 Literal,
10 Literal,
10 Union,
11 Union,
11 TYPE_CHECKING,
12 TYPE_CHECKING,
12 )
13 )
13 import ast
14 import ast
14 import builtins
15 import builtins
15 import collections
16 import collections
16 import operator
17 import operator
17 import sys
18 import sys
18 from functools import cached_property
19 from functools import cached_property
19 from dataclasses import dataclass, field
20 from dataclasses import dataclass, field
20
21
21 from IPython.utils.docs import GENERATING_DOCUMENTATION
22 from IPython.utils.docs import GENERATING_DOCUMENTATION
22 from IPython.utils.decorators import undoc
23 from IPython.utils.decorators import undoc
23
24
24
25
25 if TYPE_CHECKING or GENERATING_DOCUMENTATION:
26 if TYPE_CHECKING or GENERATING_DOCUMENTATION:
26 from typing_extensions import Protocol
27 from typing_extensions import Protocol
27 else:
28 else:
28 # do not require on runtime
29 # do not require on runtime
29 Protocol = object # requires Python >=3.8
30 Protocol = object # requires Python >=3.8
30
31
31
32
32 @undoc
33 @undoc
33 class HasGetItem(Protocol):
34 class HasGetItem(Protocol):
34 def __getitem__(self, key) -> None:
35 def __getitem__(self, key) -> None:
35 ...
36 ...
36
37
37
38
38 @undoc
39 @undoc
39 class InstancesHaveGetItem(Protocol):
40 class InstancesHaveGetItem(Protocol):
40 def __call__(self, *args, **kwargs) -> HasGetItem:
41 def __call__(self, *args, **kwargs) -> HasGetItem:
41 ...
42 ...
42
43
43
44
44 @undoc
45 @undoc
45 class HasGetAttr(Protocol):
46 class HasGetAttr(Protocol):
46 def __getattr__(self, key) -> None:
47 def __getattr__(self, key) -> None:
47 ...
48 ...
48
49
49
50
50 @undoc
51 @undoc
51 class DoesNotHaveGetAttr(Protocol):
52 class DoesNotHaveGetAttr(Protocol):
52 pass
53 pass
53
54
54
55
55 # By default `__getattr__` is not explicitly implemented on most objects
56 # By default `__getattr__` is not explicitly implemented on most objects
56 MayHaveGetattr = Union[HasGetAttr, DoesNotHaveGetAttr]
57 MayHaveGetattr = Union[HasGetAttr, DoesNotHaveGetAttr]
57
58
58
59
59 def _unbind_method(func: Callable) -> Union[Callable, None]:
60 def _unbind_method(func: Callable) -> Union[Callable, None]:
60 """Get unbound method for given bound method.
61 """Get unbound method for given bound method.
61
62
62 Returns None if cannot get unbound method."""
63 Returns None if cannot get unbound method."""
63 owner = getattr(func, "__self__", None)
64 owner = getattr(func, "__self__", None)
64 owner_class = type(owner)
65 owner_class = type(owner)
65 name = getattr(func, "__name__", None)
66 name = getattr(func, "__name__", None)
66 instance_dict_overrides = getattr(owner, "__dict__", None)
67 instance_dict_overrides = getattr(owner, "__dict__", None)
67 if (
68 if (
68 owner is not None
69 owner is not None
69 and name
70 and name
70 and (
71 and (
71 not instance_dict_overrides
72 not instance_dict_overrides
72 or (instance_dict_overrides and name not in instance_dict_overrides)
73 or (instance_dict_overrides and name not in instance_dict_overrides)
73 )
74 )
74 ):
75 ):
75 return getattr(owner_class, name)
76 return getattr(owner_class, name)
76 return None
77 return None
77
78
78
79
79 @undoc
80 @undoc
80 @dataclass
81 @dataclass
81 class EvaluationPolicy:
82 class EvaluationPolicy:
82 """Definition of evaluation policy."""
83 """Definition of evaluation policy."""
83
84
84 allow_locals_access: bool = False
85 allow_locals_access: bool = False
85 allow_globals_access: bool = False
86 allow_globals_access: bool = False
86 allow_item_access: bool = False
87 allow_item_access: bool = False
87 allow_attr_access: bool = False
88 allow_attr_access: bool = False
88 allow_builtins_access: bool = False
89 allow_builtins_access: bool = False
89 allow_all_operations: bool = False
90 allow_all_operations: bool = False
90 allow_any_calls: bool = False
91 allow_any_calls: bool = False
91 allowed_calls: Set[Callable] = field(default_factory=set)
92 allowed_calls: Set[Callable] = field(default_factory=set)
92
93
93 def can_get_item(self, value, item):
94 def can_get_item(self, value, item):
94 return self.allow_item_access
95 return self.allow_item_access
95
96
96 def can_get_attr(self, value, attr):
97 def can_get_attr(self, value, attr):
97 return self.allow_attr_access
98 return self.allow_attr_access
98
99
99 def can_operate(self, dunders: Tuple[str, ...], a, b=None):
100 def can_operate(self, dunders: Tuple[str, ...], a, b=None):
100 if self.allow_all_operations:
101 if self.allow_all_operations:
101 return True
102 return True
102
103
103 def can_call(self, func):
104 def can_call(self, func):
104 if self.allow_any_calls:
105 if self.allow_any_calls:
105 return True
106 return True
106
107
107 if func in self.allowed_calls:
108 if func in self.allowed_calls:
108 return True
109 return True
109
110
110 owner_method = _unbind_method(func)
111 owner_method = _unbind_method(func)
111
112
112 if owner_method and owner_method in self.allowed_calls:
113 if owner_method and owner_method in self.allowed_calls:
113 return True
114 return True
114
115
115
116
117 def _get_external(module_name: str, access_path: Sequence[str]):
118 """Get value from external module given a dotted access path.
119
120 Raises:
121 * `KeyError` if module is removed not found, and
122 * `AttributeError` if acess path does not match an exported object
123 """
124 member_type = sys.modules[module_name]
125 for attr in access_path:
126 member_type = getattr(member_type, attr)
127 return member_type
128
129
116 def _has_original_dunder_external(
130 def _has_original_dunder_external(
117 value,
131 value,
118 module_name,
132 module_name: str,
119 access_path,
133 access_path: Sequence[str],
120 method_name,
134 method_name: str,
121 ):
135 ):
122 try:
123 if module_name not in sys.modules:
136 if module_name not in sys.modules:
137 # LBYLB as it is faster
124 return False
138 return False
125 member_type = sys.modules[module_name]
139 try:
126 for attr in access_path:
140 member_type = _get_external(module_name, access_path)
127 member_type = getattr(member_type, attr)
128 value_type = type(value)
141 value_type = type(value)
129 if type(value) == member_type:
142 if type(value) == member_type:
130 return True
143 return True
131 if method_name == "__getattribute__":
144 if method_name == "__getattribute__":
132 # we have to short-circuit here due to an unresolved issue in
145 # we have to short-circuit here due to an unresolved issue in
133 # `isinstance` implementation: https://bugs.python.org/issue32683
146 # `isinstance` implementation: https://bugs.python.org/issue32683
134 return False
147 return False
135 if isinstance(value, member_type):
148 if isinstance(value, member_type):
136 method = getattr(value_type, method_name, None)
149 method = getattr(value_type, method_name, None)
137 member_method = getattr(member_type, method_name, None)
150 member_method = getattr(member_type, method_name, None)
138 if member_method == method:
151 if member_method == method:
139 return True
152 return True
140 except (AttributeError, KeyError):
153 except (AttributeError, KeyError):
141 return False
154 return False
142
155
143
156
144 def _has_original_dunder(
157 def _has_original_dunder(
145 value, allowed_types, allowed_methods, allowed_external, method_name
158 value, allowed_types, allowed_methods, allowed_external, method_name
146 ):
159 ):
147 # note: Python ignores `__getattr__`/`__getitem__` on instances,
160 # note: Python ignores `__getattr__`/`__getitem__` on instances,
148 # we only need to check at class level
161 # we only need to check at class level
149 value_type = type(value)
162 value_type = type(value)
150
163
151 # strict type check passes β†’ no need to check method
164 # strict type check passes β†’ no need to check method
152 if value_type in allowed_types:
165 if value_type in allowed_types:
153 return True
166 return True
154
167
155 method = getattr(value_type, method_name, None)
168 method = getattr(value_type, method_name, None)
156
169
157 if method is None:
170 if method is None:
158 return None
171 return None
159
172
160 if method in allowed_methods:
173 if method in allowed_methods:
161 return True
174 return True
162
175
163 for module_name, *access_path in allowed_external:
176 for module_name, *access_path in allowed_external:
164 if _has_original_dunder_external(value, module_name, access_path, method_name):
177 if _has_original_dunder_external(value, module_name, access_path, method_name):
165 return True
178 return True
166
179
167 return False
180 return False
168
181
169
182
170 @undoc
183 @undoc
171 @dataclass
184 @dataclass
172 class SelectivePolicy(EvaluationPolicy):
185 class SelectivePolicy(EvaluationPolicy):
173 allowed_getitem: Set[InstancesHaveGetItem] = field(default_factory=set)
186 allowed_getitem: Set[InstancesHaveGetItem] = field(default_factory=set)
174 allowed_getitem_external: Set[Tuple[str, ...]] = field(default_factory=set)
187 allowed_getitem_external: Set[Tuple[str, ...]] = field(default_factory=set)
175
188
176 allowed_getattr: Set[MayHaveGetattr] = field(default_factory=set)
189 allowed_getattr: Set[MayHaveGetattr] = field(default_factory=set)
177 allowed_getattr_external: Set[Tuple[str, ...]] = field(default_factory=set)
190 allowed_getattr_external: Set[Tuple[str, ...]] = field(default_factory=set)
178
191
179 allowed_operations: Set = field(default_factory=set)
192 allowed_operations: Set = field(default_factory=set)
180 allowed_operations_external: Set[Tuple[str, ...]] = field(default_factory=set)
193 allowed_operations_external: Set[Tuple[str, ...]] = field(default_factory=set)
181
194
182 _operation_methods_cache: Dict[str, Set[Callable]] = field(
195 _operation_methods_cache: Dict[str, Set[Callable]] = field(
183 default_factory=dict, init=False
196 default_factory=dict, init=False
184 )
197 )
185
198
186 def can_get_attr(self, value, attr):
199 def can_get_attr(self, value, attr):
187 has_original_attribute = _has_original_dunder(
200 has_original_attribute = _has_original_dunder(
188 value,
201 value,
189 allowed_types=self.allowed_getattr,
202 allowed_types=self.allowed_getattr,
190 allowed_methods=self._getattribute_methods,
203 allowed_methods=self._getattribute_methods,
191 allowed_external=self.allowed_getattr_external,
204 allowed_external=self.allowed_getattr_external,
192 method_name="__getattribute__",
205 method_name="__getattribute__",
193 )
206 )
194 has_original_attr = _has_original_dunder(
207 has_original_attr = _has_original_dunder(
195 value,
208 value,
196 allowed_types=self.allowed_getattr,
209 allowed_types=self.allowed_getattr,
197 allowed_methods=self._getattr_methods,
210 allowed_methods=self._getattr_methods,
198 allowed_external=self.allowed_getattr_external,
211 allowed_external=self.allowed_getattr_external,
199 method_name="__getattr__",
212 method_name="__getattr__",
200 )
213 )
201
214
215 accept = False
216
202 # Many objects do not have `__getattr__`, this is fine
217 # Many objects do not have `__getattr__`, this is fine
203 if has_original_attr is None and has_original_attribute:
218 if has_original_attr is None and has_original_attribute:
219 accept = True
220 else:
221 # Accept objects without modifications to `__getattr__` and `__getattribute__`
222 accept = has_original_attr and has_original_attribute
223
224 if accept:
225 # We still need to check for overriden properties.
226
227 value_class = type(value)
228 if not hasattr(value_class, attr):
204 return True
229 return True
205
230
206 # Accept objects without modifications to `__getattr__` and `__getattribute__`
231 class_attr_val = getattr(value_class, attr)
207 return has_original_attr and has_original_attribute
232 is_property = isinstance(class_attr_val, property)
233
234 if not is_property:
235 return True
236
237 # Properties in allowed types are ok
238 if type(value) in self.allowed_getattr:
239 return True
240
241 # Properties in subclasses of allowed types may be ok if not changed
242 for module_name, *access_path in self.allowed_getattr_external:
243 try:
244 external_class = _get_external(module_name, access_path)
245 external_class_attr_val = getattr(external_class, attr)
246 except (KeyError, AttributeError):
247 return False # pragma: no cover
248 return class_attr_val == external_class_attr_val
249
250 return False
208
251
209 def can_get_item(self, value, item):
252 def can_get_item(self, value, item):
210 """Allow accessing `__getiitem__` of allow-listed instances unless it was not modified."""
253 """Allow accessing `__getiitem__` of allow-listed instances unless it was not modified."""
211 return _has_original_dunder(
254 return _has_original_dunder(
212 value,
255 value,
213 allowed_types=self.allowed_getitem,
256 allowed_types=self.allowed_getitem,
214 allowed_methods=self._getitem_methods,
257 allowed_methods=self._getitem_methods,
215 allowed_external=self.allowed_getitem_external,
258 allowed_external=self.allowed_getitem_external,
216 method_name="__getitem__",
259 method_name="__getitem__",
217 )
260 )
218
261
219 def can_operate(self, dunders: Tuple[str, ...], a, b=None):
262 def can_operate(self, dunders: Tuple[str, ...], a, b=None):
220 objects = [a]
263 objects = [a]
221 if b is not None:
264 if b is not None:
222 objects.append(b)
265 objects.append(b)
223 return all(
266 return all(
224 [
267 [
225 _has_original_dunder(
268 _has_original_dunder(
226 obj,
269 obj,
227 allowed_types=self.allowed_operations,
270 allowed_types=self.allowed_operations,
228 allowed_methods=self._operator_dunder_methods(dunder),
271 allowed_methods=self._operator_dunder_methods(dunder),
229 allowed_external=self.allowed_operations_external,
272 allowed_external=self.allowed_operations_external,
230 method_name=dunder,
273 method_name=dunder,
231 )
274 )
232 for dunder in dunders
275 for dunder in dunders
233 for obj in objects
276 for obj in objects
234 ]
277 ]
235 )
278 )
236
279
237 def _operator_dunder_methods(self, dunder: str) -> Set[Callable]:
280 def _operator_dunder_methods(self, dunder: str) -> Set[Callable]:
238 if dunder not in self._operation_methods_cache:
281 if dunder not in self._operation_methods_cache:
239 self._operation_methods_cache[dunder] = self._safe_get_methods(
282 self._operation_methods_cache[dunder] = self._safe_get_methods(
240 self.allowed_operations, dunder
283 self.allowed_operations, dunder
241 )
284 )
242 return self._operation_methods_cache[dunder]
285 return self._operation_methods_cache[dunder]
243
286
244 @cached_property
287 @cached_property
245 def _getitem_methods(self) -> Set[Callable]:
288 def _getitem_methods(self) -> Set[Callable]:
246 return self._safe_get_methods(self.allowed_getitem, "__getitem__")
289 return self._safe_get_methods(self.allowed_getitem, "__getitem__")
247
290
248 @cached_property
291 @cached_property
249 def _getattr_methods(self) -> Set[Callable]:
292 def _getattr_methods(self) -> Set[Callable]:
250 return self._safe_get_methods(self.allowed_getattr, "__getattr__")
293 return self._safe_get_methods(self.allowed_getattr, "__getattr__")
251
294
252 @cached_property
295 @cached_property
253 def _getattribute_methods(self) -> Set[Callable]:
296 def _getattribute_methods(self) -> Set[Callable]:
254 return self._safe_get_methods(self.allowed_getattr, "__getattribute__")
297 return self._safe_get_methods(self.allowed_getattr, "__getattribute__")
255
298
256 def _safe_get_methods(self, classes, name) -> Set[Callable]:
299 def _safe_get_methods(self, classes, name) -> Set[Callable]:
257 return {
300 return {
258 method
301 method
259 for class_ in classes
302 for class_ in classes
260 for method in [getattr(class_, name, None)]
303 for method in [getattr(class_, name, None)]
261 if method
304 if method
262 }
305 }
263
306
264
307
265 class _DummyNamedTuple(NamedTuple):
308 class _DummyNamedTuple(NamedTuple):
266 """Used internally to retrieve methods of named tuple instance."""
309 """Used internally to retrieve methods of named tuple instance."""
267
310
268
311
269 class EvaluationContext(NamedTuple):
312 class EvaluationContext(NamedTuple):
270 #: Local namespace
313 #: Local namespace
271 locals: dict
314 locals: dict
272 #: Global namespace
315 #: Global namespace
273 globals: dict
316 globals: dict
274 #: Evaluation policy identifier
317 #: Evaluation policy identifier
275 evaluation: Literal[
318 evaluation: Literal[
276 "forbidden", "minimal", "limited", "unsafe", "dangerous"
319 "forbidden", "minimal", "limited", "unsafe", "dangerous"
277 ] = "forbidden"
320 ] = "forbidden"
278 #: Whether the evalution of code takes place inside of a subscript.
321 #: Whether the evalution of code takes place inside of a subscript.
279 #: Useful for evaluating ``:-1, 'col'`` in ``df[:-1, 'col']``.
322 #: Useful for evaluating ``:-1, 'col'`` in ``df[:-1, 'col']``.
280 in_subscript: bool = False
323 in_subscript: bool = False
281
324
282
325
283 class _IdentitySubscript:
326 class _IdentitySubscript:
284 """Returns the key itself when item is requested via subscript."""
327 """Returns the key itself when item is requested via subscript."""
285
328
286 def __getitem__(self, key):
329 def __getitem__(self, key):
287 return key
330 return key
288
331
289
332
290 IDENTITY_SUBSCRIPT = _IdentitySubscript()
333 IDENTITY_SUBSCRIPT = _IdentitySubscript()
291 SUBSCRIPT_MARKER = "__SUBSCRIPT_SENTINEL__"
334 SUBSCRIPT_MARKER = "__SUBSCRIPT_SENTINEL__"
292
335
293
336
294 class GuardRejection(Exception):
337 class GuardRejection(Exception):
295 """Exception raised when guard rejects evaluation attempt."""
338 """Exception raised when guard rejects evaluation attempt."""
296
339
297 pass
340 pass
298
341
299
342
300 def guarded_eval(code: str, context: EvaluationContext):
343 def guarded_eval(code: str, context: EvaluationContext):
301 """Evaluate provided code in the evaluation context.
344 """Evaluate provided code in the evaluation context.
302
345
303 If evaluation policy given by context is set to ``forbidden``
346 If evaluation policy given by context is set to ``forbidden``
304 no evaluation will be performed; if it is set to ``dangerous``
347 no evaluation will be performed; if it is set to ``dangerous``
305 standard :func:`eval` will be used; finally, for any other,
348 standard :func:`eval` will be used; finally, for any other,
306 policy :func:`eval_node` will be called on parsed AST.
349 policy :func:`eval_node` will be called on parsed AST.
307 """
350 """
308 locals_ = context.locals
351 locals_ = context.locals
309
352
310 if context.evaluation == "forbidden":
353 if context.evaluation == "forbidden":
311 raise GuardRejection("Forbidden mode")
354 raise GuardRejection("Forbidden mode")
312
355
313 # note: not using `ast.literal_eval` as it does not implement
356 # note: not using `ast.literal_eval` as it does not implement
314 # getitem at all, for example it fails on simple `[0][1]`
357 # getitem at all, for example it fails on simple `[0][1]`
315
358
316 if context.in_subscript:
359 if context.in_subscript:
317 # syntatic sugar for ellipsis (:) is only available in susbcripts
360 # syntatic sugar for ellipsis (:) is only available in susbcripts
318 # so we need to trick the ast parser into thinking that we have
361 # so we need to trick the ast parser into thinking that we have
319 # a subscript, but we need to be able to later recognise that we did
362 # a subscript, but we need to be able to later recognise that we did
320 # it so we can ignore the actual __getitem__ operation
363 # it so we can ignore the actual __getitem__ operation
321 if not code:
364 if not code:
322 return tuple()
365 return tuple()
323 locals_ = locals_.copy()
366 locals_ = locals_.copy()
324 locals_[SUBSCRIPT_MARKER] = IDENTITY_SUBSCRIPT
367 locals_[SUBSCRIPT_MARKER] = IDENTITY_SUBSCRIPT
325 code = SUBSCRIPT_MARKER + "[" + code + "]"
368 code = SUBSCRIPT_MARKER + "[" + code + "]"
326 context = EvaluationContext(**{**context._asdict(), **{"locals": locals_}})
369 context = EvaluationContext(**{**context._asdict(), **{"locals": locals_}})
327
370
328 if context.evaluation == "dangerous":
371 if context.evaluation == "dangerous":
329 return eval(code, context.globals, context.locals)
372 return eval(code, context.globals, context.locals)
330
373
331 expression = ast.parse(code, mode="eval")
374 expression = ast.parse(code, mode="eval")
332
375
333 return eval_node(expression, context)
376 return eval_node(expression, context)
334
377
335
378
336 BINARY_OP_DUNDERS: Dict[Type[ast.operator], Tuple[str]] = {
379 BINARY_OP_DUNDERS: Dict[Type[ast.operator], Tuple[str]] = {
337 ast.Add: ("__add__",),
380 ast.Add: ("__add__",),
338 ast.Sub: ("__sub__",),
381 ast.Sub: ("__sub__",),
339 ast.Mult: ("__mul__",),
382 ast.Mult: ("__mul__",),
340 ast.Div: ("__truediv__",),
383 ast.Div: ("__truediv__",),
341 ast.FloorDiv: ("__floordiv__",),
384 ast.FloorDiv: ("__floordiv__",),
342 ast.Mod: ("__mod__",),
385 ast.Mod: ("__mod__",),
343 ast.Pow: ("__pow__",),
386 ast.Pow: ("__pow__",),
344 ast.LShift: ("__lshift__",),
387 ast.LShift: ("__lshift__",),
345 ast.RShift: ("__rshift__",),
388 ast.RShift: ("__rshift__",),
346 ast.BitOr: ("__or__",),
389 ast.BitOr: ("__or__",),
347 ast.BitXor: ("__xor__",),
390 ast.BitXor: ("__xor__",),
348 ast.BitAnd: ("__and__",),
391 ast.BitAnd: ("__and__",),
349 ast.MatMult: ("__matmul__",),
392 ast.MatMult: ("__matmul__",),
350 }
393 }
351
394
352 COMP_OP_DUNDERS: Dict[Type[ast.cmpop], Tuple[str, ...]] = {
395 COMP_OP_DUNDERS: Dict[Type[ast.cmpop], Tuple[str, ...]] = {
353 ast.Eq: ("__eq__",),
396 ast.Eq: ("__eq__",),
354 ast.NotEq: ("__ne__", "__eq__"),
397 ast.NotEq: ("__ne__", "__eq__"),
355 ast.Lt: ("__lt__", "__gt__"),
398 ast.Lt: ("__lt__", "__gt__"),
356 ast.LtE: ("__le__", "__ge__"),
399 ast.LtE: ("__le__", "__ge__"),
357 ast.Gt: ("__gt__", "__lt__"),
400 ast.Gt: ("__gt__", "__lt__"),
358 ast.GtE: ("__ge__", "__le__"),
401 ast.GtE: ("__ge__", "__le__"),
359 ast.In: ("__contains__",),
402 ast.In: ("__contains__",),
360 # Note: ast.Is, ast.IsNot, ast.NotIn are handled specially
403 # Note: ast.Is, ast.IsNot, ast.NotIn are handled specially
361 }
404 }
362
405
363 UNARY_OP_DUNDERS: Dict[Type[ast.unaryop], Tuple[str, ...]] = {
406 UNARY_OP_DUNDERS: Dict[Type[ast.unaryop], Tuple[str, ...]] = {
364 ast.USub: ("__neg__",),
407 ast.USub: ("__neg__",),
365 ast.UAdd: ("__pos__",),
408 ast.UAdd: ("__pos__",),
366 # we have to check both __inv__ and __invert__!
409 # we have to check both __inv__ and __invert__!
367 ast.Invert: ("__invert__", "__inv__"),
410 ast.Invert: ("__invert__", "__inv__"),
368 ast.Not: ("__not__",),
411 ast.Not: ("__not__",),
369 }
412 }
370
413
371
414
372 def _find_dunder(node_op, dunders) -> Union[Tuple[str, ...], None]:
415 def _find_dunder(node_op, dunders) -> Union[Tuple[str, ...], None]:
373 dunder = None
416 dunder = None
374 for op, candidate_dunder in dunders.items():
417 for op, candidate_dunder in dunders.items():
375 if isinstance(node_op, op):
418 if isinstance(node_op, op):
376 dunder = candidate_dunder
419 dunder = candidate_dunder
377 return dunder
420 return dunder
378
421
379
422
380 def eval_node(node: Union[ast.AST, None], context: EvaluationContext):
423 def eval_node(node: Union[ast.AST, None], context: EvaluationContext):
381 """Evaluate AST node in provided context.
424 """Evaluate AST node in provided context.
382
425
383 Applies evaluation restrictions defined in the context. Currently does not support evaluation of functions with keyword arguments.
426 Applies evaluation restrictions defined in the context. Currently does not support evaluation of functions with keyword arguments.
384
427
385 Does not evaluate actions that always have side effects:
428 Does not evaluate actions that always have side effects:
386
429
387 - class definitions (``class sth: ...``)
430 - class definitions (``class sth: ...``)
388 - function definitions (``def sth: ...``)
431 - function definitions (``def sth: ...``)
389 - variable assignments (``x = 1``)
432 - variable assignments (``x = 1``)
390 - augmented assignments (``x += 1``)
433 - augmented assignments (``x += 1``)
391 - deletions (``del x``)
434 - deletions (``del x``)
392
435
393 Does not evaluate operations which do not return values:
436 Does not evaluate operations which do not return values:
394
437
395 - assertions (``assert x``)
438 - assertions (``assert x``)
396 - pass (``pass``)
439 - pass (``pass``)
397 - imports (``import x``)
440 - imports (``import x``)
398 - control flow:
441 - control flow:
399
442
400 - conditionals (``if x:``) except for ternary IfExp (``a if x else b``)
443 - conditionals (``if x:``) except for ternary IfExp (``a if x else b``)
401 - loops (``for`` and `while``)
444 - loops (``for`` and `while``)
402 - exception handling
445 - exception handling
403
446
404 The purpose of this function is to guard against unwanted side-effects;
447 The purpose of this function is to guard against unwanted side-effects;
405 it does not give guarantees on protection from malicious code execution.
448 it does not give guarantees on protection from malicious code execution.
406 """
449 """
407 policy = EVALUATION_POLICIES[context.evaluation]
450 policy = EVALUATION_POLICIES[context.evaluation]
408 if node is None:
451 if node is None:
409 return None
452 return None
410 if isinstance(node, ast.Expression):
453 if isinstance(node, ast.Expression):
411 return eval_node(node.body, context)
454 return eval_node(node.body, context)
412 if isinstance(node, ast.BinOp):
455 if isinstance(node, ast.BinOp):
413 left = eval_node(node.left, context)
456 left = eval_node(node.left, context)
414 right = eval_node(node.right, context)
457 right = eval_node(node.right, context)
415 dunders = _find_dunder(node.op, BINARY_OP_DUNDERS)
458 dunders = _find_dunder(node.op, BINARY_OP_DUNDERS)
416 if dunders:
459 if dunders:
417 if policy.can_operate(dunders, left, right):
460 if policy.can_operate(dunders, left, right):
418 return getattr(left, dunders[0])(right)
461 return getattr(left, dunders[0])(right)
419 else:
462 else:
420 raise GuardRejection(
463 raise GuardRejection(
421 f"Operation (`{dunders}`) for",
464 f"Operation (`{dunders}`) for",
422 type(left),
465 type(left),
423 f"not allowed in {context.evaluation} mode",
466 f"not allowed in {context.evaluation} mode",
424 )
467 )
425 if isinstance(node, ast.Compare):
468 if isinstance(node, ast.Compare):
426 left = eval_node(node.left, context)
469 left = eval_node(node.left, context)
427 all_true = True
470 all_true = True
428 negate = False
471 negate = False
429 for op, right in zip(node.ops, node.comparators):
472 for op, right in zip(node.ops, node.comparators):
430 right = eval_node(right, context)
473 right = eval_node(right, context)
431 dunder = None
474 dunder = None
432 dunders = _find_dunder(op, COMP_OP_DUNDERS)
475 dunders = _find_dunder(op, COMP_OP_DUNDERS)
433 if not dunders:
476 if not dunders:
434 if isinstance(op, ast.NotIn):
477 if isinstance(op, ast.NotIn):
435 dunders = COMP_OP_DUNDERS[ast.In]
478 dunders = COMP_OP_DUNDERS[ast.In]
436 negate = True
479 negate = True
437 if isinstance(op, ast.Is):
480 if isinstance(op, ast.Is):
438 dunder = "is_"
481 dunder = "is_"
439 if isinstance(op, ast.IsNot):
482 if isinstance(op, ast.IsNot):
440 dunder = "is_"
483 dunder = "is_"
441 negate = True
484 negate = True
442 if not dunder and dunders:
485 if not dunder and dunders:
443 dunder = dunders[0]
486 dunder = dunders[0]
444 if dunder:
487 if dunder:
445 a, b = (right, left) if dunder == "__contains__" else (left, right)
488 a, b = (right, left) if dunder == "__contains__" else (left, right)
446 if dunder == "is_" or dunders and policy.can_operate(dunders, a, b):
489 if dunder == "is_" or dunders and policy.can_operate(dunders, a, b):
447 result = getattr(operator, dunder)(a, b)
490 result = getattr(operator, dunder)(a, b)
448 if negate:
491 if negate:
449 result = not result
492 result = not result
450 if not result:
493 if not result:
451 all_true = False
494 all_true = False
452 left = right
495 left = right
453 else:
496 else:
454 raise GuardRejection(
497 raise GuardRejection(
455 f"Comparison (`{dunder}`) for",
498 f"Comparison (`{dunder}`) for",
456 type(left),
499 type(left),
457 f"not allowed in {context.evaluation} mode",
500 f"not allowed in {context.evaluation} mode",
458 )
501 )
459 else:
502 else:
460 raise ValueError(
503 raise ValueError(
461 f"Comparison `{dunder}` not supported"
504 f"Comparison `{dunder}` not supported"
462 ) # pragma: no cover
505 ) # pragma: no cover
463 return all_true
506 return all_true
464 if isinstance(node, ast.Constant):
507 if isinstance(node, ast.Constant):
465 return node.value
508 return node.value
466 if isinstance(node, ast.Index):
509 if isinstance(node, ast.Index):
467 # deprecated since Python 3.9
510 # deprecated since Python 3.9
468 return eval_node(node.value, context) # pragma: no cover
511 return eval_node(node.value, context) # pragma: no cover
469 if isinstance(node, ast.Tuple):
512 if isinstance(node, ast.Tuple):
470 return tuple(eval_node(e, context) for e in node.elts)
513 return tuple(eval_node(e, context) for e in node.elts)
471 if isinstance(node, ast.List):
514 if isinstance(node, ast.List):
472 return [eval_node(e, context) for e in node.elts]
515 return [eval_node(e, context) for e in node.elts]
473 if isinstance(node, ast.Set):
516 if isinstance(node, ast.Set):
474 return {eval_node(e, context) for e in node.elts}
517 return {eval_node(e, context) for e in node.elts}
475 if isinstance(node, ast.Dict):
518 if isinstance(node, ast.Dict):
476 return dict(
519 return dict(
477 zip(
520 zip(
478 [eval_node(k, context) for k in node.keys],
521 [eval_node(k, context) for k in node.keys],
479 [eval_node(v, context) for v in node.values],
522 [eval_node(v, context) for v in node.values],
480 )
523 )
481 )
524 )
482 if isinstance(node, ast.Slice):
525 if isinstance(node, ast.Slice):
483 return slice(
526 return slice(
484 eval_node(node.lower, context),
527 eval_node(node.lower, context),
485 eval_node(node.upper, context),
528 eval_node(node.upper, context),
486 eval_node(node.step, context),
529 eval_node(node.step, context),
487 )
530 )
488 if isinstance(node, ast.ExtSlice):
531 if isinstance(node, ast.ExtSlice):
489 # deprecated since Python 3.9
532 # deprecated since Python 3.9
490 return tuple([eval_node(dim, context) for dim in node.dims]) # pragma: no cover
533 return tuple([eval_node(dim, context) for dim in node.dims]) # pragma: no cover
491 if isinstance(node, ast.UnaryOp):
534 if isinstance(node, ast.UnaryOp):
492 value = eval_node(node.operand, context)
535 value = eval_node(node.operand, context)
493 dunders = _find_dunder(node.op, UNARY_OP_DUNDERS)
536 dunders = _find_dunder(node.op, UNARY_OP_DUNDERS)
494 if dunders:
537 if dunders:
495 if policy.can_operate(dunders, value):
538 if policy.can_operate(dunders, value):
496 return getattr(value, dunders[0])()
539 return getattr(value, dunders[0])()
497 else:
540 else:
498 raise GuardRejection(
541 raise GuardRejection(
499 f"Operation (`{dunders}`) for",
542 f"Operation (`{dunders}`) for",
500 type(value),
543 type(value),
501 f"not allowed in {context.evaluation} mode",
544 f"not allowed in {context.evaluation} mode",
502 )
545 )
503 if isinstance(node, ast.Subscript):
546 if isinstance(node, ast.Subscript):
504 value = eval_node(node.value, context)
547 value = eval_node(node.value, context)
505 slice_ = eval_node(node.slice, context)
548 slice_ = eval_node(node.slice, context)
506 if policy.can_get_item(value, slice_):
549 if policy.can_get_item(value, slice_):
507 return value[slice_]
550 return value[slice_]
508 raise GuardRejection(
551 raise GuardRejection(
509 "Subscript access (`__getitem__`) for",
552 "Subscript access (`__getitem__`) for",
510 type(value), # not joined to avoid calling `repr`
553 type(value), # not joined to avoid calling `repr`
511 f" not allowed in {context.evaluation} mode",
554 f" not allowed in {context.evaluation} mode",
512 )
555 )
513 if isinstance(node, ast.Name):
556 if isinstance(node, ast.Name):
514 if policy.allow_locals_access and node.id in context.locals:
557 if policy.allow_locals_access and node.id in context.locals:
515 return context.locals[node.id]
558 return context.locals[node.id]
516 if policy.allow_globals_access and node.id in context.globals:
559 if policy.allow_globals_access and node.id in context.globals:
517 return context.globals[node.id]
560 return context.globals[node.id]
518 if policy.allow_builtins_access and hasattr(builtins, node.id):
561 if policy.allow_builtins_access and hasattr(builtins, node.id):
519 # note: do not use __builtins__, it is implementation detail of cPython
562 # note: do not use __builtins__, it is implementation detail of cPython
520 return getattr(builtins, node.id)
563 return getattr(builtins, node.id)
521 if not policy.allow_globals_access and not policy.allow_locals_access:
564 if not policy.allow_globals_access and not policy.allow_locals_access:
522 raise GuardRejection(
565 raise GuardRejection(
523 f"Namespace access not allowed in {context.evaluation} mode"
566 f"Namespace access not allowed in {context.evaluation} mode"
524 )
567 )
525 else:
568 else:
526 raise NameError(f"{node.id} not found in locals, globals, nor builtins")
569 raise NameError(f"{node.id} not found in locals, globals, nor builtins")
527 if isinstance(node, ast.Attribute):
570 if isinstance(node, ast.Attribute):
528 value = eval_node(node.value, context)
571 value = eval_node(node.value, context)
529 if policy.can_get_attr(value, node.attr):
572 if policy.can_get_attr(value, node.attr):
530 return getattr(value, node.attr)
573 return getattr(value, node.attr)
531 raise GuardRejection(
574 raise GuardRejection(
532 "Attribute access (`__getattr__`) for",
575 "Attribute access (`__getattr__`) for",
533 type(value), # not joined to avoid calling `repr`
576 type(value), # not joined to avoid calling `repr`
534 f"not allowed in {context.evaluation} mode",
577 f"not allowed in {context.evaluation} mode",
535 )
578 )
536 if isinstance(node, ast.IfExp):
579 if isinstance(node, ast.IfExp):
537 test = eval_node(node.test, context)
580 test = eval_node(node.test, context)
538 if test:
581 if test:
539 return eval_node(node.body, context)
582 return eval_node(node.body, context)
540 else:
583 else:
541 return eval_node(node.orelse, context)
584 return eval_node(node.orelse, context)
542 if isinstance(node, ast.Call):
585 if isinstance(node, ast.Call):
543 func = eval_node(node.func, context)
586 func = eval_node(node.func, context)
544 if policy.can_call(func) and not node.keywords:
587 if policy.can_call(func) and not node.keywords:
545 args = [eval_node(arg, context) for arg in node.args]
588 args = [eval_node(arg, context) for arg in node.args]
546 return func(*args)
589 return func(*args)
547 raise GuardRejection(
590 raise GuardRejection(
548 "Call for",
591 "Call for",
549 func, # not joined to avoid calling `repr`
592 func, # not joined to avoid calling `repr`
550 f"not allowed in {context.evaluation} mode",
593 f"not allowed in {context.evaluation} mode",
551 )
594 )
552 raise ValueError("Unhandled node", ast.dump(node))
595 raise ValueError("Unhandled node", ast.dump(node))
553
596
554
597
555 SUPPORTED_EXTERNAL_GETITEM = {
598 SUPPORTED_EXTERNAL_GETITEM = {
556 ("pandas", "core", "indexing", "_iLocIndexer"),
599 ("pandas", "core", "indexing", "_iLocIndexer"),
557 ("pandas", "core", "indexing", "_LocIndexer"),
600 ("pandas", "core", "indexing", "_LocIndexer"),
558 ("pandas", "DataFrame"),
601 ("pandas", "DataFrame"),
559 ("pandas", "Series"),
602 ("pandas", "Series"),
560 ("numpy", "ndarray"),
603 ("numpy", "ndarray"),
561 ("numpy", "void"),
604 ("numpy", "void"),
562 }
605 }
563
606
564
607
565 BUILTIN_GETITEM: Set[InstancesHaveGetItem] = {
608 BUILTIN_GETITEM: Set[InstancesHaveGetItem] = {
566 dict,
609 dict,
567 str,
610 str,
568 bytes,
611 bytes,
569 list,
612 list,
570 tuple,
613 tuple,
571 collections.defaultdict,
614 collections.defaultdict,
572 collections.deque,
615 collections.deque,
573 collections.OrderedDict,
616 collections.OrderedDict,
574 collections.ChainMap,
617 collections.ChainMap,
575 collections.UserDict,
618 collections.UserDict,
576 collections.UserList,
619 collections.UserList,
577 collections.UserString,
620 collections.UserString,
578 _DummyNamedTuple,
621 _DummyNamedTuple,
579 _IdentitySubscript,
622 _IdentitySubscript,
580 }
623 }
581
624
582
625
583 def _list_methods(cls, source=None):
626 def _list_methods(cls, source=None):
584 """For use on immutable objects or with methods returning a copy"""
627 """For use on immutable objects or with methods returning a copy"""
585 return [getattr(cls, k) for k in (source if source else dir(cls))]
628 return [getattr(cls, k) for k in (source if source else dir(cls))]
586
629
587
630
588 dict_non_mutating_methods = ("copy", "keys", "values", "items")
631 dict_non_mutating_methods = ("copy", "keys", "values", "items")
589 list_non_mutating_methods = ("copy", "index", "count")
632 list_non_mutating_methods = ("copy", "index", "count")
590 set_non_mutating_methods = set(dir(set)) & set(dir(frozenset))
633 set_non_mutating_methods = set(dir(set)) & set(dir(frozenset))
591
634
592
635
593 dict_keys: Type[collections.abc.KeysView] = type({}.keys())
636 dict_keys: Type[collections.abc.KeysView] = type({}.keys())
594 method_descriptor: Any = type(list.copy)
637 method_descriptor: Any = type(list.copy)
595
638
596 NUMERICS = {int, float, complex}
639 NUMERICS = {int, float, complex}
597
640
598 ALLOWED_CALLS = {
641 ALLOWED_CALLS = {
599 bytes,
642 bytes,
600 *_list_methods(bytes),
643 *_list_methods(bytes),
601 dict,
644 dict,
602 *_list_methods(dict, dict_non_mutating_methods),
645 *_list_methods(dict, dict_non_mutating_methods),
603 dict_keys.isdisjoint,
646 dict_keys.isdisjoint,
604 list,
647 list,
605 *_list_methods(list, list_non_mutating_methods),
648 *_list_methods(list, list_non_mutating_methods),
606 set,
649 set,
607 *_list_methods(set, set_non_mutating_methods),
650 *_list_methods(set, set_non_mutating_methods),
608 frozenset,
651 frozenset,
609 *_list_methods(frozenset),
652 *_list_methods(frozenset),
610 range,
653 range,
611 str,
654 str,
612 *_list_methods(str),
655 *_list_methods(str),
613 tuple,
656 tuple,
614 *_list_methods(tuple),
657 *_list_methods(tuple),
615 *NUMERICS,
658 *NUMERICS,
616 *[method for numeric_cls in NUMERICS for method in _list_methods(numeric_cls)],
659 *[method for numeric_cls in NUMERICS for method in _list_methods(numeric_cls)],
617 collections.deque,
660 collections.deque,
618 *_list_methods(collections.deque, list_non_mutating_methods),
661 *_list_methods(collections.deque, list_non_mutating_methods),
619 collections.defaultdict,
662 collections.defaultdict,
620 *_list_methods(collections.defaultdict, dict_non_mutating_methods),
663 *_list_methods(collections.defaultdict, dict_non_mutating_methods),
621 collections.OrderedDict,
664 collections.OrderedDict,
622 *_list_methods(collections.OrderedDict, dict_non_mutating_methods),
665 *_list_methods(collections.OrderedDict, dict_non_mutating_methods),
623 collections.UserDict,
666 collections.UserDict,
624 *_list_methods(collections.UserDict, dict_non_mutating_methods),
667 *_list_methods(collections.UserDict, dict_non_mutating_methods),
625 collections.UserList,
668 collections.UserList,
626 *_list_methods(collections.UserList, list_non_mutating_methods),
669 *_list_methods(collections.UserList, list_non_mutating_methods),
627 collections.UserString,
670 collections.UserString,
628 *_list_methods(collections.UserString, dir(str)),
671 *_list_methods(collections.UserString, dir(str)),
629 collections.Counter,
672 collections.Counter,
630 *_list_methods(collections.Counter, dict_non_mutating_methods),
673 *_list_methods(collections.Counter, dict_non_mutating_methods),
631 collections.Counter.elements,
674 collections.Counter.elements,
632 collections.Counter.most_common,
675 collections.Counter.most_common,
633 }
676 }
634
677
635 BUILTIN_GETATTR: Set[MayHaveGetattr] = {
678 BUILTIN_GETATTR: Set[MayHaveGetattr] = {
636 *BUILTIN_GETITEM,
679 *BUILTIN_GETITEM,
637 set,
680 set,
638 frozenset,
681 frozenset,
639 object,
682 object,
640 type, # `type` handles a lot of generic cases, e.g. numbers as in `int.real`.
683 type, # `type` handles a lot of generic cases, e.g. numbers as in `int.real`.
641 *NUMERICS,
684 *NUMERICS,
642 dict_keys,
685 dict_keys,
643 method_descriptor,
686 method_descriptor,
644 }
687 }
645
688
646
689
647 BUILTIN_OPERATIONS = {*BUILTIN_GETATTR}
690 BUILTIN_OPERATIONS = {*BUILTIN_GETATTR}
648
691
649 EVALUATION_POLICIES = {
692 EVALUATION_POLICIES = {
650 "minimal": EvaluationPolicy(
693 "minimal": EvaluationPolicy(
651 allow_builtins_access=True,
694 allow_builtins_access=True,
652 allow_locals_access=False,
695 allow_locals_access=False,
653 allow_globals_access=False,
696 allow_globals_access=False,
654 allow_item_access=False,
697 allow_item_access=False,
655 allow_attr_access=False,
698 allow_attr_access=False,
656 allowed_calls=set(),
699 allowed_calls=set(),
657 allow_any_calls=False,
700 allow_any_calls=False,
658 allow_all_operations=False,
701 allow_all_operations=False,
659 ),
702 ),
660 "limited": SelectivePolicy(
703 "limited": SelectivePolicy(
661 # TODO:
704 # TODO:
662 # - should reject binary and unary operations if custom methods would be dispatched
705 # - should reject binary and unary operations if custom methods would be dispatched
663 allowed_getitem=BUILTIN_GETITEM,
706 allowed_getitem=BUILTIN_GETITEM,
664 allowed_getitem_external=SUPPORTED_EXTERNAL_GETITEM,
707 allowed_getitem_external=SUPPORTED_EXTERNAL_GETITEM,
665 allowed_getattr=BUILTIN_GETATTR,
708 allowed_getattr=BUILTIN_GETATTR,
666 allowed_getattr_external={
709 allowed_getattr_external={
667 # pandas Series/Frame implements custom `__getattr__`
710 # pandas Series/Frame implements custom `__getattr__`
668 ("pandas", "DataFrame"),
711 ("pandas", "DataFrame"),
669 ("pandas", "Series"),
712 ("pandas", "Series"),
670 },
713 },
671 allowed_operations=BUILTIN_OPERATIONS,
714 allowed_operations=BUILTIN_OPERATIONS,
672 allow_builtins_access=True,
715 allow_builtins_access=True,
673 allow_locals_access=True,
716 allow_locals_access=True,
674 allow_globals_access=True,
717 allow_globals_access=True,
675 allowed_calls=ALLOWED_CALLS,
718 allowed_calls=ALLOWED_CALLS,
676 ),
719 ),
677 "unsafe": EvaluationPolicy(
720 "unsafe": EvaluationPolicy(
678 allow_builtins_access=True,
721 allow_builtins_access=True,
679 allow_locals_access=True,
722 allow_locals_access=True,
680 allow_globals_access=True,
723 allow_globals_access=True,
681 allow_attr_access=True,
724 allow_attr_access=True,
682 allow_item_access=True,
725 allow_item_access=True,
683 allow_any_calls=True,
726 allow_any_calls=True,
684 allow_all_operations=True,
727 allow_all_operations=True,
685 ),
728 ),
686 }
729 }
687
730
688
731
689 __all__ = [
732 __all__ = [
690 "guarded_eval",
733 "guarded_eval",
691 "eval_node",
734 "eval_node",
692 "GuardRejection",
735 "GuardRejection",
693 "EvaluationContext",
736 "EvaluationContext",
694 "_unbind_method",
737 "_unbind_method",
695 ]
738 ]
@@ -1,492 +1,537 b''
1 from contextlib import contextmanager
1 from typing import NamedTuple
2 from typing import NamedTuple
2 from functools import partial
3 from functools import partial
3 from IPython.core.guarded_eval import (
4 from IPython.core.guarded_eval import (
4 EvaluationContext,
5 EvaluationContext,
5 GuardRejection,
6 GuardRejection,
6 guarded_eval,
7 guarded_eval,
7 _unbind_method,
8 _unbind_method,
8 )
9 )
9 from IPython.testing import decorators as dec
10 from IPython.testing import decorators as dec
10 import pytest
11 import pytest
11
12
12
13
13 def create_context(evaluation: str, **kwargs):
14 def create_context(evaluation: str, **kwargs):
14 return EvaluationContext(locals=kwargs, globals={}, evaluation=evaluation)
15 return EvaluationContext(locals=kwargs, globals={}, evaluation=evaluation)
15
16
16
17
17 forbidden = partial(create_context, "forbidden")
18 forbidden = partial(create_context, "forbidden")
18 minimal = partial(create_context, "minimal")
19 minimal = partial(create_context, "minimal")
19 limited = partial(create_context, "limited")
20 limited = partial(create_context, "limited")
20 unsafe = partial(create_context, "unsafe")
21 unsafe = partial(create_context, "unsafe")
21 dangerous = partial(create_context, "dangerous")
22 dangerous = partial(create_context, "dangerous")
22
23
23 LIMITED_OR_HIGHER = [limited, unsafe, dangerous]
24 LIMITED_OR_HIGHER = [limited, unsafe, dangerous]
24
25
25 MINIMAL_OR_HIGHER = [minimal, *LIMITED_OR_HIGHER]
26 MINIMAL_OR_HIGHER = [minimal, *LIMITED_OR_HIGHER]
26
27
27
28
29 @contextmanager
30 def module_not_installed(module: str):
31 import sys
32
33 try:
34 to_restore = sys.modules[module]
35 del sys.modules[module]
36 except KeyError:
37 to_restore = None
38 try:
39 yield
40 finally:
41 sys.modules[module] = to_restore
42
43
28 @dec.skip_without("pandas")
44 @dec.skip_without("pandas")
29 def test_pandas_series_iloc():
45 def test_pandas_series_iloc():
30 import pandas as pd
46 import pandas as pd
31
47
32 series = pd.Series([1], index=["a"])
48 series = pd.Series([1], index=["a"])
33 context = limited(data=series)
49 context = limited(data=series)
34 assert guarded_eval("data.iloc[0]", context) == 1
50 assert guarded_eval("data.iloc[0]", context) == 1
35
51
36
52
53 def test_rejects_custom_properties():
54 class BadProperty:
55 @property
56 def iloc(self):
57 return [None]
58
59 series = BadProperty()
60 context = limited(data=series)
61
62 with pytest.raises(GuardRejection):
63 guarded_eval("data.iloc[0]", context)
64
65
66 @dec.skip_without("pandas")
67 def test_accepts_non_overriden_properties():
68 import pandas as pd
69
70 class GoodProperty(pd.Series):
71 pass
72
73 series = GoodProperty([1], index=["a"])
74 context = limited(data=series)
75
76 assert guarded_eval("data.iloc[0]", context) == 1
77
78
37 @dec.skip_without("pandas")
79 @dec.skip_without("pandas")
38 def test_pandas_series():
80 def test_pandas_series():
39 import pandas as pd
81 import pandas as pd
40
82
41 context = limited(data=pd.Series([1], index=["a"]))
83 context = limited(data=pd.Series([1], index=["a"]))
42 assert guarded_eval('data["a"]', context) == 1
84 assert guarded_eval('data["a"]', context) == 1
43 with pytest.raises(KeyError):
85 with pytest.raises(KeyError):
44 guarded_eval('data["c"]', context)
86 guarded_eval('data["c"]', context)
45
87
46
88
47 @dec.skip_without("pandas")
89 @dec.skip_without("pandas")
48 def test_pandas_bad_series():
90 def test_pandas_bad_series():
49 import pandas as pd
91 import pandas as pd
50
92
51 class BadItemSeries(pd.Series):
93 class BadItemSeries(pd.Series):
52 def __getitem__(self, key):
94 def __getitem__(self, key):
53 return "CUSTOM_ITEM"
95 return "CUSTOM_ITEM"
54
96
55 class BadAttrSeries(pd.Series):
97 class BadAttrSeries(pd.Series):
56 def __getattr__(self, key):
98 def __getattr__(self, key):
57 return "CUSTOM_ATTR"
99 return "CUSTOM_ATTR"
58
100
59 bad_series = BadItemSeries([1], index=["a"])
101 bad_series = BadItemSeries([1], index=["a"])
60 context = limited(data=bad_series)
102 context = limited(data=bad_series)
61
103
62 with pytest.raises(GuardRejection):
104 with pytest.raises(GuardRejection):
63 guarded_eval('data["a"]', context)
105 guarded_eval('data["a"]', context)
64 with pytest.raises(GuardRejection):
106 with pytest.raises(GuardRejection):
65 guarded_eval('data["c"]', context)
107 guarded_eval('data["c"]', context)
66
108
67 # note: here result is a bit unexpected because
109 # note: here result is a bit unexpected because
68 # pandas `__getattr__` calls `__getitem__`;
110 # pandas `__getattr__` calls `__getitem__`;
69 # FIXME - special case to handle it?
111 # FIXME - special case to handle it?
70 assert guarded_eval("data.a", context) == "CUSTOM_ITEM"
112 assert guarded_eval("data.a", context) == "CUSTOM_ITEM"
71
113
72 context = unsafe(data=bad_series)
114 context = unsafe(data=bad_series)
73 assert guarded_eval('data["a"]', context) == "CUSTOM_ITEM"
115 assert guarded_eval('data["a"]', context) == "CUSTOM_ITEM"
74
116
75 bad_attr_series = BadAttrSeries([1], index=["a"])
117 bad_attr_series = BadAttrSeries([1], index=["a"])
76 context = limited(data=bad_attr_series)
118 context = limited(data=bad_attr_series)
77 assert guarded_eval('data["a"]', context) == 1
119 assert guarded_eval('data["a"]', context) == 1
78 with pytest.raises(GuardRejection):
120 with pytest.raises(GuardRejection):
79 guarded_eval("data.a", context)
121 guarded_eval("data.a", context)
80
122
81
123
82 @dec.skip_without("pandas")
124 @dec.skip_without("pandas")
83 def test_pandas_dataframe_loc():
125 def test_pandas_dataframe_loc():
84 import pandas as pd
126 import pandas as pd
85 from pandas.testing import assert_series_equal
127 from pandas.testing import assert_series_equal
86
128
87 data = pd.DataFrame([{"a": 1}])
129 data = pd.DataFrame([{"a": 1}])
88 context = limited(data=data)
130 context = limited(data=data)
89 assert_series_equal(guarded_eval('data.loc[:, "a"]', context), data["a"])
131 assert_series_equal(guarded_eval('data.loc[:, "a"]', context), data["a"])
90
132
91
133
92 def test_named_tuple():
134 def test_named_tuple():
93 class GoodNamedTuple(NamedTuple):
135 class GoodNamedTuple(NamedTuple):
94 a: str
136 a: str
95 pass
137 pass
96
138
97 class BadNamedTuple(NamedTuple):
139 class BadNamedTuple(NamedTuple):
98 a: str
140 a: str
99
141
100 def __getitem__(self, key):
142 def __getitem__(self, key):
101 return None
143 return None
102
144
103 good = GoodNamedTuple(a="x")
145 good = GoodNamedTuple(a="x")
104 bad = BadNamedTuple(a="x")
146 bad = BadNamedTuple(a="x")
105
147
106 context = limited(data=good)
148 context = limited(data=good)
107 assert guarded_eval("data[0]", context) == "x"
149 assert guarded_eval("data[0]", context) == "x"
108
150
109 context = limited(data=bad)
151 context = limited(data=bad)
110 with pytest.raises(GuardRejection):
152 with pytest.raises(GuardRejection):
111 guarded_eval("data[0]", context)
153 guarded_eval("data[0]", context)
112
154
113
155
114 def test_dict():
156 def test_dict():
115 context = limited(data={"a": 1, "b": {"x": 2}, ("x", "y"): 3})
157 context = limited(data={"a": 1, "b": {"x": 2}, ("x", "y"): 3})
116 assert guarded_eval('data["a"]', context) == 1
158 assert guarded_eval('data["a"]', context) == 1
117 assert guarded_eval('data["b"]', context) == {"x": 2}
159 assert guarded_eval('data["b"]', context) == {"x": 2}
118 assert guarded_eval('data["b"]["x"]', context) == 2
160 assert guarded_eval('data["b"]["x"]', context) == 2
119 assert guarded_eval('data["x", "y"]', context) == 3
161 assert guarded_eval('data["x", "y"]', context) == 3
120
162
121 assert guarded_eval("data.keys", context)
163 assert guarded_eval("data.keys", context)
122
164
123
165
124 def test_set():
166 def test_set():
125 context = limited(data={"a", "b"})
167 context = limited(data={"a", "b"})
126 assert guarded_eval("data.difference", context)
168 assert guarded_eval("data.difference", context)
127
169
128
170
129 def test_list():
171 def test_list():
130 context = limited(data=[1, 2, 3])
172 context = limited(data=[1, 2, 3])
131 assert guarded_eval("data[1]", context) == 2
173 assert guarded_eval("data[1]", context) == 2
132 assert guarded_eval("data.copy", context)
174 assert guarded_eval("data.copy", context)
133
175
134
176
135 def test_dict_literal():
177 def test_dict_literal():
136 context = limited()
178 context = limited()
137 assert guarded_eval("{}", context) == {}
179 assert guarded_eval("{}", context) == {}
138 assert guarded_eval('{"a": 1}', context) == {"a": 1}
180 assert guarded_eval('{"a": 1}', context) == {"a": 1}
139
181
140
182
141 def test_list_literal():
183 def test_list_literal():
142 context = limited()
184 context = limited()
143 assert guarded_eval("[]", context) == []
185 assert guarded_eval("[]", context) == []
144 assert guarded_eval('[1, "a"]', context) == [1, "a"]
186 assert guarded_eval('[1, "a"]', context) == [1, "a"]
145
187
146
188
147 def test_set_literal():
189 def test_set_literal():
148 context = limited()
190 context = limited()
149 assert guarded_eval("set()", context) == set()
191 assert guarded_eval("set()", context) == set()
150 assert guarded_eval('{"a"}', context) == {"a"}
192 assert guarded_eval('{"a"}', context) == {"a"}
151
193
152
194
153 def test_evaluates_if_expression():
195 def test_evaluates_if_expression():
154 context = limited()
196 context = limited()
155 assert guarded_eval("2 if True else 3", context) == 2
197 assert guarded_eval("2 if True else 3", context) == 2
156 assert guarded_eval("4 if False else 5", context) == 5
198 assert guarded_eval("4 if False else 5", context) == 5
157
199
158
200
159 def test_object():
201 def test_object():
160 obj = object()
202 obj = object()
161 context = limited(obj=obj)
203 context = limited(obj=obj)
162 assert guarded_eval("obj.__dir__", context) == obj.__dir__
204 assert guarded_eval("obj.__dir__", context) == obj.__dir__
163
205
164
206
165 @pytest.mark.parametrize(
207 @pytest.mark.parametrize(
166 "code,expected",
208 "code,expected",
167 [
209 [
168 ["int.numerator", int.numerator],
210 ["int.numerator", int.numerator],
169 ["float.is_integer", float.is_integer],
211 ["float.is_integer", float.is_integer],
170 ["complex.real", complex.real],
212 ["complex.real", complex.real],
171 ],
213 ],
172 )
214 )
173 def test_number_attributes(code, expected):
215 def test_number_attributes(code, expected):
174 assert guarded_eval(code, limited()) == expected
216 assert guarded_eval(code, limited()) == expected
175
217
176
218
177 def test_method_descriptor():
219 def test_method_descriptor():
178 context = limited()
220 context = limited()
179 assert guarded_eval("list.copy.__name__", context) == "copy"
221 assert guarded_eval("list.copy.__name__", context) == "copy"
180
222
181
223
182 @pytest.mark.parametrize(
224 @pytest.mark.parametrize(
183 "data,good,bad,expected",
225 "data,good,bad,expected",
184 [
226 [
185 [[1, 2, 3], "data.index(2)", "data.append(4)", 1],
227 [[1, 2, 3], "data.index(2)", "data.append(4)", 1],
186 [{"a": 1}, "data.keys().isdisjoint({})", "data.update()", True],
228 [{"a": 1}, "data.keys().isdisjoint({})", "data.update()", True],
187 ],
229 ],
188 )
230 )
189 def test_evaluates_calls(data, good, bad, expected):
231 def test_evaluates_calls(data, good, bad, expected):
190 context = limited(data=data)
232 context = limited(data=data)
191 assert guarded_eval(good, context) == expected
233 assert guarded_eval(good, context) == expected
192
234
193 with pytest.raises(GuardRejection):
235 with pytest.raises(GuardRejection):
194 guarded_eval(bad, context)
236 guarded_eval(bad, context)
195
237
196
238
197 @pytest.mark.parametrize(
239 @pytest.mark.parametrize(
198 "code,expected",
240 "code,expected",
199 [
241 [
200 ["(1\n+\n1)", 2],
242 ["(1\n+\n1)", 2],
201 ["list(range(10))[-1:]", [9]],
243 ["list(range(10))[-1:]", [9]],
202 ["list(range(20))[3:-2:3]", [3, 6, 9, 12, 15]],
244 ["list(range(20))[3:-2:3]", [3, 6, 9, 12, 15]],
203 ],
245 ],
204 )
246 )
205 @pytest.mark.parametrize("context", LIMITED_OR_HIGHER)
247 @pytest.mark.parametrize("context", LIMITED_OR_HIGHER)
206 def test_evaluates_complex_cases(code, expected, context):
248 def test_evaluates_complex_cases(code, expected, context):
207 assert guarded_eval(code, context()) == expected
249 assert guarded_eval(code, context()) == expected
208
250
209
251
210 @pytest.mark.parametrize(
252 @pytest.mark.parametrize(
211 "code,expected",
253 "code,expected",
212 [
254 [
213 ["1", 1],
255 ["1", 1],
214 ["1.0", 1.0],
256 ["1.0", 1.0],
215 ["0xdeedbeef", 0xDEEDBEEF],
257 ["0xdeedbeef", 0xDEEDBEEF],
216 ["True", True],
258 ["True", True],
217 ["None", None],
259 ["None", None],
218 ["{}", {}],
260 ["{}", {}],
219 ["[]", []],
261 ["[]", []],
220 ],
262 ],
221 )
263 )
222 @pytest.mark.parametrize("context", MINIMAL_OR_HIGHER)
264 @pytest.mark.parametrize("context", MINIMAL_OR_HIGHER)
223 def test_evaluates_literals(code, expected, context):
265 def test_evaluates_literals(code, expected, context):
224 assert guarded_eval(code, context()) == expected
266 assert guarded_eval(code, context()) == expected
225
267
226
268
227 @pytest.mark.parametrize(
269 @pytest.mark.parametrize(
228 "code,expected",
270 "code,expected",
229 [
271 [
230 ["-5", -5],
272 ["-5", -5],
231 ["+5", +5],
273 ["+5", +5],
232 ["~5", -6],
274 ["~5", -6],
233 ],
275 ],
234 )
276 )
235 @pytest.mark.parametrize("context", LIMITED_OR_HIGHER)
277 @pytest.mark.parametrize("context", LIMITED_OR_HIGHER)
236 def test_evaluates_unary_operations(code, expected, context):
278 def test_evaluates_unary_operations(code, expected, context):
237 assert guarded_eval(code, context()) == expected
279 assert guarded_eval(code, context()) == expected
238
280
239
281
240 @pytest.mark.parametrize(
282 @pytest.mark.parametrize(
241 "code,expected",
283 "code,expected",
242 [
284 [
243 ["1 + 1", 2],
285 ["1 + 1", 2],
244 ["3 - 1", 2],
286 ["3 - 1", 2],
245 ["2 * 3", 6],
287 ["2 * 3", 6],
246 ["5 // 2", 2],
288 ["5 // 2", 2],
247 ["5 / 2", 2.5],
289 ["5 / 2", 2.5],
248 ["5**2", 25],
290 ["5**2", 25],
249 ["2 >> 1", 1],
291 ["2 >> 1", 1],
250 ["2 << 1", 4],
292 ["2 << 1", 4],
251 ["1 | 2", 3],
293 ["1 | 2", 3],
252 ["1 & 1", 1],
294 ["1 & 1", 1],
253 ["1 & 2", 0],
295 ["1 & 2", 0],
254 ],
296 ],
255 )
297 )
256 @pytest.mark.parametrize("context", LIMITED_OR_HIGHER)
298 @pytest.mark.parametrize("context", LIMITED_OR_HIGHER)
257 def test_evaluates_binary_operations(code, expected, context):
299 def test_evaluates_binary_operations(code, expected, context):
258 assert guarded_eval(code, context()) == expected
300 assert guarded_eval(code, context()) == expected
259
301
260
302
261 @pytest.mark.parametrize(
303 @pytest.mark.parametrize(
262 "code,expected",
304 "code,expected",
263 [
305 [
264 ["2 > 1", True],
306 ["2 > 1", True],
265 ["2 < 1", False],
307 ["2 < 1", False],
266 ["2 <= 1", False],
308 ["2 <= 1", False],
267 ["2 <= 2", True],
309 ["2 <= 2", True],
268 ["1 >= 2", False],
310 ["1 >= 2", False],
269 ["2 >= 2", True],
311 ["2 >= 2", True],
270 ["2 == 2", True],
312 ["2 == 2", True],
271 ["1 == 2", False],
313 ["1 == 2", False],
272 ["1 != 2", True],
314 ["1 != 2", True],
273 ["1 != 1", False],
315 ["1 != 1", False],
274 ["1 < 4 < 3", False],
316 ["1 < 4 < 3", False],
275 ["(1 < 4) < 3", True],
317 ["(1 < 4) < 3", True],
276 ["4 > 3 > 2 > 1", True],
318 ["4 > 3 > 2 > 1", True],
277 ["4 > 3 > 2 > 9", False],
319 ["4 > 3 > 2 > 9", False],
278 ["1 < 2 < 3 < 4", True],
320 ["1 < 2 < 3 < 4", True],
279 ["9 < 2 < 3 < 4", False],
321 ["9 < 2 < 3 < 4", False],
280 ["1 < 2 > 1 > 0 > -1 < 1", True],
322 ["1 < 2 > 1 > 0 > -1 < 1", True],
281 ["1 in [1] in [[1]]", True],
323 ["1 in [1] in [[1]]", True],
282 ["1 in [1] in [[2]]", False],
324 ["1 in [1] in [[2]]", False],
283 ["1 in [1]", True],
325 ["1 in [1]", True],
284 ["0 in [1]", False],
326 ["0 in [1]", False],
285 ["1 not in [1]", False],
327 ["1 not in [1]", False],
286 ["0 not in [1]", True],
328 ["0 not in [1]", True],
287 ["True is True", True],
329 ["True is True", True],
288 ["False is False", True],
330 ["False is False", True],
289 ["True is False", False],
331 ["True is False", False],
290 ["True is not True", False],
332 ["True is not True", False],
291 ["False is not True", True],
333 ["False is not True", True],
292 ],
334 ],
293 )
335 )
294 @pytest.mark.parametrize("context", LIMITED_OR_HIGHER)
336 @pytest.mark.parametrize("context", LIMITED_OR_HIGHER)
295 def test_evaluates_comparisons(code, expected, context):
337 def test_evaluates_comparisons(code, expected, context):
296 assert guarded_eval(code, context()) == expected
338 assert guarded_eval(code, context()) == expected
297
339
298
340
299 def test_guards_comparisons():
341 def test_guards_comparisons():
300 class GoodEq(int):
342 class GoodEq(int):
301 pass
343 pass
302
344
303 class BadEq(int):
345 class BadEq(int):
304 def __eq__(self, other):
346 def __eq__(self, other):
305 assert False
347 assert False
306
348
307 context = limited(bad=BadEq(1), good=GoodEq(1))
349 context = limited(bad=BadEq(1), good=GoodEq(1))
308
350
309 with pytest.raises(GuardRejection):
351 with pytest.raises(GuardRejection):
310 guarded_eval("bad == 1", context)
352 guarded_eval("bad == 1", context)
311
353
312 with pytest.raises(GuardRejection):
354 with pytest.raises(GuardRejection):
313 guarded_eval("bad != 1", context)
355 guarded_eval("bad != 1", context)
314
356
315 with pytest.raises(GuardRejection):
357 with pytest.raises(GuardRejection):
316 guarded_eval("1 == bad", context)
358 guarded_eval("1 == bad", context)
317
359
318 with pytest.raises(GuardRejection):
360 with pytest.raises(GuardRejection):
319 guarded_eval("1 != bad", context)
361 guarded_eval("1 != bad", context)
320
362
321 assert guarded_eval("good == 1", context) is True
363 assert guarded_eval("good == 1", context) is True
322 assert guarded_eval("good != 1", context) is False
364 assert guarded_eval("good != 1", context) is False
323 assert guarded_eval("1 == good", context) is True
365 assert guarded_eval("1 == good", context) is True
324 assert guarded_eval("1 != good", context) is False
366 assert guarded_eval("1 != good", context) is False
325
367
326
368
327 def test_guards_unary_operations():
369 def test_guards_unary_operations():
328 class GoodOp(int):
370 class GoodOp(int):
329 pass
371 pass
330
372
331 class BadOpInv(int):
373 class BadOpInv(int):
332 def __inv__(self, other):
374 def __inv__(self, other):
333 assert False
375 assert False
334
376
335 class BadOpInverse(int):
377 class BadOpInverse(int):
336 def __inv__(self, other):
378 def __inv__(self, other):
337 assert False
379 assert False
338
380
339 context = limited(good=GoodOp(1), bad1=BadOpInv(1), bad2=BadOpInverse(1))
381 context = limited(good=GoodOp(1), bad1=BadOpInv(1), bad2=BadOpInverse(1))
340
382
341 with pytest.raises(GuardRejection):
383 with pytest.raises(GuardRejection):
342 guarded_eval("~bad1", context)
384 guarded_eval("~bad1", context)
343
385
344 with pytest.raises(GuardRejection):
386 with pytest.raises(GuardRejection):
345 guarded_eval("~bad2", context)
387 guarded_eval("~bad2", context)
346
388
347
389
348 def test_guards_binary_operations():
390 def test_guards_binary_operations():
349 class GoodOp(int):
391 class GoodOp(int):
350 pass
392 pass
351
393
352 class BadOp(int):
394 class BadOp(int):
353 def __add__(self, other):
395 def __add__(self, other):
354 assert False
396 assert False
355
397
356 context = limited(good=GoodOp(1), bad=BadOp(1))
398 context = limited(good=GoodOp(1), bad=BadOp(1))
357
399
358 with pytest.raises(GuardRejection):
400 with pytest.raises(GuardRejection):
359 guarded_eval("1 + bad", context)
401 guarded_eval("1 + bad", context)
360
402
361 with pytest.raises(GuardRejection):
403 with pytest.raises(GuardRejection):
362 guarded_eval("bad + 1", context)
404 guarded_eval("bad + 1", context)
363
405
364 assert guarded_eval("good + 1", context) == 2
406 assert guarded_eval("good + 1", context) == 2
365 assert guarded_eval("1 + good", context) == 2
407 assert guarded_eval("1 + good", context) == 2
366
408
367
409
368 def test_guards_attributes():
410 def test_guards_attributes():
369 class GoodAttr(float):
411 class GoodAttr(float):
370 pass
412 pass
371
413
372 class BadAttr1(float):
414 class BadAttr1(float):
373 def __getattr__(self, key):
415 def __getattr__(self, key):
374 assert False
416 assert False
375
417
376 class BadAttr2(float):
418 class BadAttr2(float):
377 def __getattribute__(self, key):
419 def __getattribute__(self, key):
378 assert False
420 assert False
379
421
380 context = limited(good=GoodAttr(0.5), bad1=BadAttr1(0.5), bad2=BadAttr2(0.5))
422 context = limited(good=GoodAttr(0.5), bad1=BadAttr1(0.5), bad2=BadAttr2(0.5))
381
423
382 with pytest.raises(GuardRejection):
424 with pytest.raises(GuardRejection):
383 guarded_eval("bad1.as_integer_ratio", context)
425 guarded_eval("bad1.as_integer_ratio", context)
384
426
385 with pytest.raises(GuardRejection):
427 with pytest.raises(GuardRejection):
386 guarded_eval("bad2.as_integer_ratio", context)
428 guarded_eval("bad2.as_integer_ratio", context)
387
429
388 assert guarded_eval("good.as_integer_ratio()", context) == (1, 2)
430 assert guarded_eval("good.as_integer_ratio()", context) == (1, 2)
389
431
390
432
391 @pytest.mark.parametrize("context", MINIMAL_OR_HIGHER)
433 @pytest.mark.parametrize("context", MINIMAL_OR_HIGHER)
392 def test_access_builtins(context):
434 def test_access_builtins(context):
393 assert guarded_eval("round", context()) == round
435 assert guarded_eval("round", context()) == round
394
436
395
437
396 def test_access_builtins_fails():
438 def test_access_builtins_fails():
397 context = limited()
439 context = limited()
398 with pytest.raises(NameError):
440 with pytest.raises(NameError):
399 guarded_eval("this_is_not_builtin", context)
441 guarded_eval("this_is_not_builtin", context)
400
442
401
443
402 def test_rejects_forbidden():
444 def test_rejects_forbidden():
403 context = forbidden()
445 context = forbidden()
404 with pytest.raises(GuardRejection):
446 with pytest.raises(GuardRejection):
405 guarded_eval("1", context)
447 guarded_eval("1", context)
406
448
407
449
408 def test_guards_locals_and_globals():
450 def test_guards_locals_and_globals():
409 context = EvaluationContext(
451 context = EvaluationContext(
410 locals={"local_a": "a"}, globals={"global_b": "b"}, evaluation="minimal"
452 locals={"local_a": "a"}, globals={"global_b": "b"}, evaluation="minimal"
411 )
453 )
412
454
413 with pytest.raises(GuardRejection):
455 with pytest.raises(GuardRejection):
414 guarded_eval("local_a", context)
456 guarded_eval("local_a", context)
415
457
416 with pytest.raises(GuardRejection):
458 with pytest.raises(GuardRejection):
417 guarded_eval("global_b", context)
459 guarded_eval("global_b", context)
418
460
419
461
420 def test_access_locals_and_globals():
462 def test_access_locals_and_globals():
421 context = EvaluationContext(
463 context = EvaluationContext(
422 locals={"local_a": "a"}, globals={"global_b": "b"}, evaluation="limited"
464 locals={"local_a": "a"}, globals={"global_b": "b"}, evaluation="limited"
423 )
465 )
424 assert guarded_eval("local_a", context) == "a"
466 assert guarded_eval("local_a", context) == "a"
425 assert guarded_eval("global_b", context) == "b"
467 assert guarded_eval("global_b", context) == "b"
426
468
427
469
428 @pytest.mark.parametrize(
470 @pytest.mark.parametrize(
429 "code",
471 "code",
430 ["def func(): pass", "class C: pass", "x = 1", "x += 1", "del x", "import ast"],
472 ["def func(): pass", "class C: pass", "x = 1", "x += 1", "del x", "import ast"],
431 )
473 )
432 @pytest.mark.parametrize("context", [minimal(), limited(), unsafe()])
474 @pytest.mark.parametrize("context", [minimal(), limited(), unsafe()])
433 def test_rejects_side_effect_syntax(code, context):
475 def test_rejects_side_effect_syntax(code, context):
434 with pytest.raises(SyntaxError):
476 with pytest.raises(SyntaxError):
435 guarded_eval(code, context)
477 guarded_eval(code, context)
436
478
437
479
438 def test_subscript():
480 def test_subscript():
439 context = EvaluationContext(
481 context = EvaluationContext(
440 locals={}, globals={}, evaluation="limited", in_subscript=True
482 locals={}, globals={}, evaluation="limited", in_subscript=True
441 )
483 )
442 empty_slice = slice(None, None, None)
484 empty_slice = slice(None, None, None)
443 assert guarded_eval("", context) == tuple()
485 assert guarded_eval("", context) == tuple()
444 assert guarded_eval(":", context) == empty_slice
486 assert guarded_eval(":", context) == empty_slice
445 assert guarded_eval("1:2:3", context) == slice(1, 2, 3)
487 assert guarded_eval("1:2:3", context) == slice(1, 2, 3)
446 assert guarded_eval(':, "a"', context) == (empty_slice, "a")
488 assert guarded_eval(':, "a"', context) == (empty_slice, "a")
447
489
448
490
449 def test_unbind_method():
491 def test_unbind_method():
450 class X(list):
492 class X(list):
451 def index(self, k):
493 def index(self, k):
452 return "CUSTOM"
494 return "CUSTOM"
453
495
454 x = X()
496 x = X()
455 assert _unbind_method(x.index) is X.index
497 assert _unbind_method(x.index) is X.index
456 assert _unbind_method([].index) is list.index
498 assert _unbind_method([].index) is list.index
457
499
458
500
459 def test_assumption_instance_attr_do_not_matter():
501 def test_assumption_instance_attr_do_not_matter():
460 """This is semi-specified in Python documentation.
502 """This is semi-specified in Python documentation.
461
503
462 However, since the specification says 'not guaranted
504 However, since the specification says 'not guaranted
463 to work' rather than 'is forbidden to work', future
505 to work' rather than 'is forbidden to work', future
464 versions could invalidate this assumptions. This test
506 versions could invalidate this assumptions. This test
465 is meant to catch such a change if it ever comes true.
507 is meant to catch such a change if it ever comes true.
466 """
508 """
467
509
468 class T:
510 class T:
469 def __getitem__(self, k):
511 def __getitem__(self, k):
470 return "a"
512 return "a"
471
513
472 def __getattr__(self, k):
514 def __getattr__(self, k):
473 return "a"
515 return "a"
474
516
517 def f(self):
518 return "b"
519
475 t = T()
520 t = T()
476 t.__getitem__ = lambda f: "b"
521 t.__getitem__ = f
477 t.__getattr__ = lambda f: "b"
522 t.__getattr__ = f
478 assert t[1] == "a"
523 assert t[1] == "a"
479 assert t[1] == "a"
524 assert t[1] == "a"
480
525
481
526
482 def test_assumption_named_tuples_share_getitem():
527 def test_assumption_named_tuples_share_getitem():
483 """Check assumption on named tuples sharing __getitem__"""
528 """Check assumption on named tuples sharing __getitem__"""
484 from typing import NamedTuple
529 from typing import NamedTuple
485
530
486 class A(NamedTuple):
531 class A(NamedTuple):
487 pass
532 pass
488
533
489 class B(NamedTuple):
534 class B(NamedTuple):
490 pass
535 pass
491
536
492 assert A.__getitem__ == B.__getitem__
537 assert A.__getitem__ == B.__getitem__
General Comments 0
You need to be logged in to leave comments. Login now