##// END OF EJS Templates
Support for `Self`, custom types, type aliases...
krassowski -
Show More
@@ -1,803 +1,816 b''
1 1 from inspect import isclass, signature, Signature
2 2 from typing import (
3 3 Callable,
4 4 Dict,
5 Literal,
6 NamedTuple,
7 NewType,
5 8 Set,
6 9 Sequence,
7 10 Tuple,
8 NamedTuple,
9 11 Type,
10 Literal,
12 Protocol,
11 13 Union,
12 TYPE_CHECKING,
14 get_args,
15 get_origin,
16 )
17 from typing_extensions import (
18 Self, # Python >=3.10
19 TypeAliasType, # Python >=3.12
13 20 )
14 21 import ast
15 22 import builtins
16 23 import collections
17 24 import operator
18 25 import sys
19 26 from functools import cached_property
20 27 from dataclasses import dataclass, field
21 28 from types import MethodDescriptorType, ModuleType
22 29
23 from IPython.utils.docs import GENERATING_DOCUMENTATION
24 from IPython.utils.decorators import undoc
25
26 30
27 if TYPE_CHECKING or GENERATING_DOCUMENTATION:
28 from typing_extensions import Protocol
29 else:
30 # do not require on runtime
31 Protocol = object # requires Python >=3.8
31 from IPython.utils.decorators import undoc
32 32
33 33
34 34 @undoc
35 35 class HasGetItem(Protocol):
36 36 def __getitem__(self, key) -> None:
37 37 ...
38 38
39 39
40 40 @undoc
41 41 class InstancesHaveGetItem(Protocol):
42 42 def __call__(self, *args, **kwargs) -> HasGetItem:
43 43 ...
44 44
45 45
46 46 @undoc
47 47 class HasGetAttr(Protocol):
48 48 def __getattr__(self, key) -> None:
49 49 ...
50 50
51 51
52 52 @undoc
53 53 class DoesNotHaveGetAttr(Protocol):
54 54 pass
55 55
56 56
57 57 # By default `__getattr__` is not explicitly implemented on most objects
58 58 MayHaveGetattr = Union[HasGetAttr, DoesNotHaveGetAttr]
59 59
60 60
61 61 def _unbind_method(func: Callable) -> Union[Callable, None]:
62 62 """Get unbound method for given bound method.
63 63
64 64 Returns None if cannot get unbound method, or method is already unbound.
65 65 """
66 66 owner = getattr(func, "__self__", None)
67 67 owner_class = type(owner)
68 68 name = getattr(func, "__name__", None)
69 69 instance_dict_overrides = getattr(owner, "__dict__", None)
70 70 if (
71 71 owner is not None
72 72 and name
73 73 and (
74 74 not instance_dict_overrides
75 75 or (instance_dict_overrides and name not in instance_dict_overrides)
76 76 )
77 77 ):
78 78 return getattr(owner_class, name)
79 79 return None
80 80
81 81
82 82 @undoc
83 83 @dataclass
84 84 class EvaluationPolicy:
85 85 """Definition of evaluation policy."""
86 86
87 87 allow_locals_access: bool = False
88 88 allow_globals_access: bool = False
89 89 allow_item_access: bool = False
90 90 allow_attr_access: bool = False
91 91 allow_builtins_access: bool = False
92 92 allow_all_operations: bool = False
93 93 allow_any_calls: bool = False
94 94 allowed_calls: Set[Callable] = field(default_factory=set)
95 95
96 96 def can_get_item(self, value, item):
97 97 return self.allow_item_access
98 98
99 99 def can_get_attr(self, value, attr):
100 100 return self.allow_attr_access
101 101
102 102 def can_operate(self, dunders: Tuple[str, ...], a, b=None):
103 103 if self.allow_all_operations:
104 104 return True
105 105
106 106 def can_call(self, func):
107 107 if self.allow_any_calls:
108 108 return True
109 109
110 110 if func in self.allowed_calls:
111 111 return True
112 112
113 113 owner_method = _unbind_method(func)
114 114
115 115 if owner_method and owner_method in self.allowed_calls:
116 116 return True
117 117
118 118
119 119 def _get_external(module_name: str, access_path: Sequence[str]):
120 120 """Get value from external module given a dotted access path.
121 121
122 122 Raises:
123 123 * `KeyError` if module is removed not found, and
124 124 * `AttributeError` if acess path does not match an exported object
125 125 """
126 126 member_type = sys.modules[module_name]
127 127 for attr in access_path:
128 128 member_type = getattr(member_type, attr)
129 129 return member_type
130 130
131 131
132 132 def _has_original_dunder_external(
133 133 value,
134 134 module_name: str,
135 135 access_path: Sequence[str],
136 136 method_name: str,
137 137 ):
138 138 if module_name not in sys.modules:
139 139 # LBYLB as it is faster
140 140 return False
141 141 try:
142 142 member_type = _get_external(module_name, access_path)
143 143 value_type = type(value)
144 144 if type(value) == member_type:
145 145 return True
146 146 if method_name == "__getattribute__":
147 147 # we have to short-circuit here due to an unresolved issue in
148 148 # `isinstance` implementation: https://bugs.python.org/issue32683
149 149 return False
150 150 if isinstance(value, member_type):
151 151 method = getattr(value_type, method_name, None)
152 152 member_method = getattr(member_type, method_name, None)
153 153 if member_method == method:
154 154 return True
155 155 except (AttributeError, KeyError):
156 156 return False
157 157
158 158
159 159 def _has_original_dunder(
160 160 value, allowed_types, allowed_methods, allowed_external, method_name
161 161 ):
162 162 # note: Python ignores `__getattr__`/`__getitem__` on instances,
163 163 # we only need to check at class level
164 164 value_type = type(value)
165 165
166 166 # strict type check passes β†’ no need to check method
167 167 if value_type in allowed_types:
168 168 return True
169 169
170 170 method = getattr(value_type, method_name, None)
171 171
172 172 if method is None:
173 173 return None
174 174
175 175 if method in allowed_methods:
176 176 return True
177 177
178 178 for module_name, *access_path in allowed_external:
179 179 if _has_original_dunder_external(value, module_name, access_path, method_name):
180 180 return True
181 181
182 182 return False
183 183
184 184
185 185 @undoc
186 186 @dataclass
187 187 class SelectivePolicy(EvaluationPolicy):
188 188 allowed_getitem: Set[InstancesHaveGetItem] = field(default_factory=set)
189 189 allowed_getitem_external: Set[Tuple[str, ...]] = field(default_factory=set)
190 190
191 191 allowed_getattr: Set[MayHaveGetattr] = field(default_factory=set)
192 192 allowed_getattr_external: Set[Tuple[str, ...]] = field(default_factory=set)
193 193
194 194 allowed_operations: Set = field(default_factory=set)
195 195 allowed_operations_external: Set[Tuple[str, ...]] = field(default_factory=set)
196 196
197 197 _operation_methods_cache: Dict[str, Set[Callable]] = field(
198 198 default_factory=dict, init=False
199 199 )
200 200
201 201 def can_get_attr(self, value, attr):
202 202 has_original_attribute = _has_original_dunder(
203 203 value,
204 204 allowed_types=self.allowed_getattr,
205 205 allowed_methods=self._getattribute_methods,
206 206 allowed_external=self.allowed_getattr_external,
207 207 method_name="__getattribute__",
208 208 )
209 209 has_original_attr = _has_original_dunder(
210 210 value,
211 211 allowed_types=self.allowed_getattr,
212 212 allowed_methods=self._getattr_methods,
213 213 allowed_external=self.allowed_getattr_external,
214 214 method_name="__getattr__",
215 215 )
216 216
217 217 accept = False
218 218
219 219 # Many objects do not have `__getattr__`, this is fine.
220 220 if has_original_attr is None and has_original_attribute:
221 221 accept = True
222 222 else:
223 223 # Accept objects without modifications to `__getattr__` and `__getattribute__`
224 224 accept = has_original_attr and has_original_attribute
225 225
226 226 if accept:
227 227 # We still need to check for overriden properties.
228 228
229 229 value_class = type(value)
230 230 if not hasattr(value_class, attr):
231 231 return True
232 232
233 233 class_attr_val = getattr(value_class, attr)
234 234 is_property = isinstance(class_attr_val, property)
235 235
236 236 if not is_property:
237 237 return True
238 238
239 239 # Properties in allowed types are ok (although we do not include any
240 240 # properties in our default allow list currently).
241 241 if type(value) in self.allowed_getattr:
242 242 return True # pragma: no cover
243 243
244 244 # Properties in subclasses of allowed types may be ok if not changed
245 245 for module_name, *access_path in self.allowed_getattr_external:
246 246 try:
247 247 external_class = _get_external(module_name, access_path)
248 248 external_class_attr_val = getattr(external_class, attr)
249 249 except (KeyError, AttributeError):
250 250 return False # pragma: no cover
251 251 return class_attr_val == external_class_attr_val
252 252
253 253 return False
254 254
255 255 def can_get_item(self, value, item):
256 256 """Allow accessing `__getiitem__` of allow-listed instances unless it was not modified."""
257 257 return _has_original_dunder(
258 258 value,
259 259 allowed_types=self.allowed_getitem,
260 260 allowed_methods=self._getitem_methods,
261 261 allowed_external=self.allowed_getitem_external,
262 262 method_name="__getitem__",
263 263 )
264 264
265 265 def can_operate(self, dunders: Tuple[str, ...], a, b=None):
266 266 objects = [a]
267 267 if b is not None:
268 268 objects.append(b)
269 269 return all(
270 270 [
271 271 _has_original_dunder(
272 272 obj,
273 273 allowed_types=self.allowed_operations,
274 274 allowed_methods=self._operator_dunder_methods(dunder),
275 275 allowed_external=self.allowed_operations_external,
276 276 method_name=dunder,
277 277 )
278 278 for dunder in dunders
279 279 for obj in objects
280 280 ]
281 281 )
282 282
283 283 def _operator_dunder_methods(self, dunder: str) -> Set[Callable]:
284 284 if dunder not in self._operation_methods_cache:
285 285 self._operation_methods_cache[dunder] = self._safe_get_methods(
286 286 self.allowed_operations, dunder
287 287 )
288 288 return self._operation_methods_cache[dunder]
289 289
290 290 @cached_property
291 291 def _getitem_methods(self) -> Set[Callable]:
292 292 return self._safe_get_methods(self.allowed_getitem, "__getitem__")
293 293
294 294 @cached_property
295 295 def _getattr_methods(self) -> Set[Callable]:
296 296 return self._safe_get_methods(self.allowed_getattr, "__getattr__")
297 297
298 298 @cached_property
299 299 def _getattribute_methods(self) -> Set[Callable]:
300 300 return self._safe_get_methods(self.allowed_getattr, "__getattribute__")
301 301
302 302 def _safe_get_methods(self, classes, name) -> Set[Callable]:
303 303 return {
304 304 method
305 305 for class_ in classes
306 306 for method in [getattr(class_, name, None)]
307 307 if method
308 308 }
309 309
310 310
311 311 class _DummyNamedTuple(NamedTuple):
312 312 """Used internally to retrieve methods of named tuple instance."""
313 313
314 314
315 315 class EvaluationContext(NamedTuple):
316 316 #: Local namespace
317 317 locals: dict
318 318 #: Global namespace
319 319 globals: dict
320 320 #: Evaluation policy identifier
321 321 evaluation: Literal[
322 322 "forbidden", "minimal", "limited", "unsafe", "dangerous"
323 323 ] = "forbidden"
324 324 #: Whether the evalution of code takes place inside of a subscript.
325 325 #: Useful for evaluating ``:-1, 'col'`` in ``df[:-1, 'col']``.
326 326 in_subscript: bool = False
327 327
328 328
329 329 class _IdentitySubscript:
330 330 """Returns the key itself when item is requested via subscript."""
331 331
332 332 def __getitem__(self, key):
333 333 return key
334 334
335 335
336 336 IDENTITY_SUBSCRIPT = _IdentitySubscript()
337 337 SUBSCRIPT_MARKER = "__SUBSCRIPT_SENTINEL__"
338 338 UNKNOWN_SIGNATURE = Signature()
339 339 NOT_EVALUATED = object()
340 340
341 341
342 342 class GuardRejection(Exception):
343 343 """Exception raised when guard rejects evaluation attempt."""
344 344
345 345 pass
346 346
347 347
348 348 def guarded_eval(code: str, context: EvaluationContext):
349 349 """Evaluate provided code in the evaluation context.
350 350
351 351 If evaluation policy given by context is set to ``forbidden``
352 352 no evaluation will be performed; if it is set to ``dangerous``
353 353 standard :func:`eval` will be used; finally, for any other,
354 354 policy :func:`eval_node` will be called on parsed AST.
355 355 """
356 356 locals_ = context.locals
357 357
358 358 if context.evaluation == "forbidden":
359 359 raise GuardRejection("Forbidden mode")
360 360
361 361 # note: not using `ast.literal_eval` as it does not implement
362 362 # getitem at all, for example it fails on simple `[0][1]`
363 363
364 364 if context.in_subscript:
365 365 # syntatic sugar for ellipsis (:) is only available in susbcripts
366 366 # so we need to trick the ast parser into thinking that we have
367 367 # a subscript, but we need to be able to later recognise that we did
368 368 # it so we can ignore the actual __getitem__ operation
369 369 if not code:
370 370 return tuple()
371 371 locals_ = locals_.copy()
372 372 locals_[SUBSCRIPT_MARKER] = IDENTITY_SUBSCRIPT
373 373 code = SUBSCRIPT_MARKER + "[" + code + "]"
374 374 context = EvaluationContext(**{**context._asdict(), **{"locals": locals_}})
375 375
376 376 if context.evaluation == "dangerous":
377 377 return eval(code, context.globals, context.locals)
378 378
379 379 expression = ast.parse(code, mode="eval")
380 380
381 381 return eval_node(expression, context)
382 382
383 383
384 384 BINARY_OP_DUNDERS: Dict[Type[ast.operator], Tuple[str]] = {
385 385 ast.Add: ("__add__",),
386 386 ast.Sub: ("__sub__",),
387 387 ast.Mult: ("__mul__",),
388 388 ast.Div: ("__truediv__",),
389 389 ast.FloorDiv: ("__floordiv__",),
390 390 ast.Mod: ("__mod__",),
391 391 ast.Pow: ("__pow__",),
392 392 ast.LShift: ("__lshift__",),
393 393 ast.RShift: ("__rshift__",),
394 394 ast.BitOr: ("__or__",),
395 395 ast.BitXor: ("__xor__",),
396 396 ast.BitAnd: ("__and__",),
397 397 ast.MatMult: ("__matmul__",),
398 398 }
399 399
400 400 COMP_OP_DUNDERS: Dict[Type[ast.cmpop], Tuple[str, ...]] = {
401 401 ast.Eq: ("__eq__",),
402 402 ast.NotEq: ("__ne__", "__eq__"),
403 403 ast.Lt: ("__lt__", "__gt__"),
404 404 ast.LtE: ("__le__", "__ge__"),
405 405 ast.Gt: ("__gt__", "__lt__"),
406 406 ast.GtE: ("__ge__", "__le__"),
407 407 ast.In: ("__contains__",),
408 408 # Note: ast.Is, ast.IsNot, ast.NotIn are handled specially
409 409 }
410 410
411 411 UNARY_OP_DUNDERS: Dict[Type[ast.unaryop], Tuple[str, ...]] = {
412 412 ast.USub: ("__neg__",),
413 413 ast.UAdd: ("__pos__",),
414 414 # we have to check both __inv__ and __invert__!
415 415 ast.Invert: ("__invert__", "__inv__"),
416 416 ast.Not: ("__not__",),
417 417 }
418 418
419 419
420 420 class Duck:
421 421 """A dummy class used to create objects of other classes without calling their ``__init__``"""
422 422
423 423
424 424 def _find_dunder(node_op, dunders) -> Union[Tuple[str, ...], None]:
425 425 dunder = None
426 426 for op, candidate_dunder in dunders.items():
427 427 if isinstance(node_op, op):
428 428 dunder = candidate_dunder
429 429 return dunder
430 430
431 431
432 432 def eval_node(node: Union[ast.AST, None], context: EvaluationContext):
433 433 """Evaluate AST node in provided context.
434 434
435 435 Applies evaluation restrictions defined in the context. Currently does not support evaluation of functions with keyword arguments.
436 436
437 437 Does not evaluate actions that always have side effects:
438 438
439 439 - class definitions (``class sth: ...``)
440 440 - function definitions (``def sth: ...``)
441 441 - variable assignments (``x = 1``)
442 442 - augmented assignments (``x += 1``)
443 443 - deletions (``del x``)
444 444
445 445 Does not evaluate operations which do not return values:
446 446
447 447 - assertions (``assert x``)
448 448 - pass (``pass``)
449 449 - imports (``import x``)
450 450 - control flow:
451 451
452 452 - conditionals (``if x:``) except for ternary IfExp (``a if x else b``)
453 453 - loops (``for`` and ``while``)
454 454 - exception handling
455 455
456 456 The purpose of this function is to guard against unwanted side-effects;
457 457 it does not give guarantees on protection from malicious code execution.
458 458 """
459 459 policy = EVALUATION_POLICIES[context.evaluation]
460 460 if node is None:
461 461 return None
462 462 if isinstance(node, ast.Expression):
463 463 return eval_node(node.body, context)
464 464 if isinstance(node, ast.BinOp):
465 465 left = eval_node(node.left, context)
466 466 right = eval_node(node.right, context)
467 467 dunders = _find_dunder(node.op, BINARY_OP_DUNDERS)
468 468 if dunders:
469 469 if policy.can_operate(dunders, left, right):
470 470 return getattr(left, dunders[0])(right)
471 471 else:
472 472 raise GuardRejection(
473 473 f"Operation (`{dunders}`) for",
474 474 type(left),
475 475 f"not allowed in {context.evaluation} mode",
476 476 )
477 477 if isinstance(node, ast.Compare):
478 478 left = eval_node(node.left, context)
479 479 all_true = True
480 480 negate = False
481 481 for op, right in zip(node.ops, node.comparators):
482 482 right = eval_node(right, context)
483 483 dunder = None
484 484 dunders = _find_dunder(op, COMP_OP_DUNDERS)
485 485 if not dunders:
486 486 if isinstance(op, ast.NotIn):
487 487 dunders = COMP_OP_DUNDERS[ast.In]
488 488 negate = True
489 489 if isinstance(op, ast.Is):
490 490 dunder = "is_"
491 491 if isinstance(op, ast.IsNot):
492 492 dunder = "is_"
493 493 negate = True
494 494 if not dunder and dunders:
495 495 dunder = dunders[0]
496 496 if dunder:
497 497 a, b = (right, left) if dunder == "__contains__" else (left, right)
498 498 if dunder == "is_" or dunders and policy.can_operate(dunders, a, b):
499 499 result = getattr(operator, dunder)(a, b)
500 500 if negate:
501 501 result = not result
502 502 if not result:
503 503 all_true = False
504 504 left = right
505 505 else:
506 506 raise GuardRejection(
507 507 f"Comparison (`{dunder}`) for",
508 508 type(left),
509 509 f"not allowed in {context.evaluation} mode",
510 510 )
511 511 else:
512 512 raise ValueError(
513 513 f"Comparison `{dunder}` not supported"
514 514 ) # pragma: no cover
515 515 return all_true
516 516 if isinstance(node, ast.Constant):
517 517 return node.value
518 518 if isinstance(node, ast.Tuple):
519 519 return tuple(eval_node(e, context) for e in node.elts)
520 520 if isinstance(node, ast.List):
521 521 return [eval_node(e, context) for e in node.elts]
522 522 if isinstance(node, ast.Set):
523 523 return {eval_node(e, context) for e in node.elts}
524 524 if isinstance(node, ast.Dict):
525 525 return dict(
526 526 zip(
527 527 [eval_node(k, context) for k in node.keys],
528 528 [eval_node(v, context) for v in node.values],
529 529 )
530 530 )
531 531 if isinstance(node, ast.Slice):
532 532 return slice(
533 533 eval_node(node.lower, context),
534 534 eval_node(node.upper, context),
535 535 eval_node(node.step, context),
536 536 )
537 537 if isinstance(node, ast.UnaryOp):
538 538 value = eval_node(node.operand, context)
539 539 dunders = _find_dunder(node.op, UNARY_OP_DUNDERS)
540 540 if dunders:
541 541 if policy.can_operate(dunders, value):
542 542 return getattr(value, dunders[0])()
543 543 else:
544 544 raise GuardRejection(
545 545 f"Operation (`{dunders}`) for",
546 546 type(value),
547 547 f"not allowed in {context.evaluation} mode",
548 548 )
549 549 if isinstance(node, ast.Subscript):
550 550 value = eval_node(node.value, context)
551 551 slice_ = eval_node(node.slice, context)
552 552 if policy.can_get_item(value, slice_):
553 553 return value[slice_]
554 554 raise GuardRejection(
555 555 "Subscript access (`__getitem__`) for",
556 556 type(value), # not joined to avoid calling `repr`
557 557 f" not allowed in {context.evaluation} mode",
558 558 )
559 559 if isinstance(node, ast.Name):
560 return _eval_node_name(node.id, policy, context)
560 return _eval_node_name(node.id, context)
561 561 if isinstance(node, ast.Attribute):
562 562 value = eval_node(node.value, context)
563 563 if policy.can_get_attr(value, node.attr):
564 564 return getattr(value, node.attr)
565 565 raise GuardRejection(
566 566 "Attribute access (`__getattr__`) for",
567 567 type(value), # not joined to avoid calling `repr`
568 568 f"not allowed in {context.evaluation} mode",
569 569 )
570 570 if isinstance(node, ast.IfExp):
571 571 test = eval_node(node.test, context)
572 572 if test:
573 573 return eval_node(node.body, context)
574 574 else:
575 575 return eval_node(node.orelse, context)
576 576 if isinstance(node, ast.Call):
577 577 func = eval_node(node.func, context)
578 578 if policy.can_call(func) and not node.keywords:
579 579 args = [eval_node(arg, context) for arg in node.args]
580 580 return func(*args)
581 581 if isclass(func):
582 582 # this code path gets entered when calling class e.g. `MyClass()`
583 583 # or `my_instance.__class__()` - in both cases `func` is `MyClass`.
584 584 # Should return `MyClass` if `__new__` is not overridden,
585 585 # otherwise whatever `__new__` return type is.
586 overridden_return_type = _eval_return_type(
587 func.__new__, policy, node, context
588 )
586 overridden_return_type = _eval_return_type(func.__new__, node, context)
589 587 if overridden_return_type is not NOT_EVALUATED:
590 588 return overridden_return_type
591 return _create_duck_for_type(func)
589 return _create_duck_for_heap_type(func)
592 590 else:
593 return_type = _eval_return_type(func, policy, node, context)
591 return_type = _eval_return_type(func, node, context)
594 592 if return_type is not NOT_EVALUATED:
595 593 return return_type
596 594 raise GuardRejection(
597 595 "Call for",
598 596 func, # not joined to avoid calling `repr`
599 597 f"not allowed in {context.evaluation} mode",
600 598 )
601 599 raise ValueError("Unhandled node", ast.dump(node))
602 600
603 601
604 def _eval_return_type(
605 func: Callable, policy: EvaluationPolicy, node: ast.Call, context: EvaluationContext
606 ):
602 def _eval_return_type(func: Callable, node: ast.Call, context: EvaluationContext):
607 603 """Evaluate return type of a given callable function.
608 604
609 605 Returns the built-in type, a duck or NOT_EVALUATED sentinel.
610 606 """
611 607 try:
612 608 sig = signature(func)
613 609 except ValueError:
614 610 sig = UNKNOWN_SIGNATURE
615 611 # if annotation was not stringized, or it was stringized
616 612 # but resolved by signature call we know the return type
617 613 not_empty = sig.return_annotation is not Signature.empty
618 614 stringized = isinstance(sig.return_annotation, str)
619 615 if not_empty:
620 616 return_type = (
621 _eval_node_name(sig.return_annotation, policy, context)
617 _eval_node_name(sig.return_annotation, context)
622 618 if stringized
623 619 else sig.return_annotation
624 620 )
625 # if allow-listed builtin is on type annotation, instantiate it
626 if policy.can_call(return_type) and not node.keywords:
627 args = [eval_node(arg, context) for arg in node.args]
628 # if custom class is in type annotation, mock it;
629 return return_type(*args)
630 return _create_duck_for_type(return_type)
621 if return_type is Self and hasattr(func, "__self__"):
622 return func.__self__
623 elif get_origin(return_type) is Literal:
624 type_args = get_args(return_type)
625 if len(type_args) == 1:
626 return type_args[0]
627 elif isinstance(return_type, NewType):
628 return _eval_or_create_duck(return_type.__supertype__, node, context)
629 elif isinstance(return_type, TypeAliasType):
630 return _eval_or_create_duck(return_type.__value__, node, context)
631 else:
632 return _eval_or_create_duck(return_type, node, context)
631 633 return NOT_EVALUATED
632 634
633 635
634 def _eval_node_name(node_id: str, policy: EvaluationPolicy, context: EvaluationContext):
636 def _eval_node_name(node_id: str, context: EvaluationContext):
637 policy = EVALUATION_POLICIES[context.evaluation]
635 638 if policy.allow_locals_access and node_id in context.locals:
636 639 return context.locals[node_id]
637 640 if policy.allow_globals_access and node_id in context.globals:
638 641 return context.globals[node_id]
639 642 if policy.allow_builtins_access and hasattr(builtins, node_id):
640 643 # note: do not use __builtins__, it is implementation detail of cPython
641 644 return getattr(builtins, node_id)
642 645 if not policy.allow_globals_access and not policy.allow_locals_access:
643 646 raise GuardRejection(
644 647 f"Namespace access not allowed in {context.evaluation} mode"
645 648 )
646 649 else:
647 650 raise NameError(f"{node_id} not found in locals, globals, nor builtins")
648 651
649 652
650 def _create_duck_for_type(duck_type):
653 def _eval_or_create_duck(duck_type, node: ast.Call, context: EvaluationContext):
654 policy = EVALUATION_POLICIES[context.evaluation]
655 # if allow-listed builtin is on type annotation, instantiate it
656 if policy.can_call(duck_type) and not node.keywords:
657 args = [eval_node(arg, context) for arg in node.args]
658 return duck_type(*args)
659 # if custom class is in type annotation, mock it
660 return _create_duck_for_heap_type(duck_type)
661
662
663 def _create_duck_for_heap_type(duck_type):
651 664 """Create an imitation of an object of a given type (a duck).
652 665
653 666 Returns the duck or NOT_EVALUATED sentinel if duck could not be created.
654 667 """
655 668 duck = Duck()
656 669 try:
657 670 # this only works for heap types, not builtins
658 671 duck.__class__ = duck_type
659 672 return duck
660 673 except TypeError:
661 674 pass
662 675 return NOT_EVALUATED
663 676
664 677
665 678 SUPPORTED_EXTERNAL_GETITEM = {
666 679 ("pandas", "core", "indexing", "_iLocIndexer"),
667 680 ("pandas", "core", "indexing", "_LocIndexer"),
668 681 ("pandas", "DataFrame"),
669 682 ("pandas", "Series"),
670 683 ("numpy", "ndarray"),
671 684 ("numpy", "void"),
672 685 }
673 686
674 687
675 688 BUILTIN_GETITEM: Set[InstancesHaveGetItem] = {
676 689 dict,
677 690 str, # type: ignore[arg-type]
678 691 bytes, # type: ignore[arg-type]
679 692 list,
680 693 tuple,
681 694 collections.defaultdict,
682 695 collections.deque,
683 696 collections.OrderedDict,
684 697 collections.ChainMap,
685 698 collections.UserDict,
686 699 collections.UserList,
687 700 collections.UserString, # type: ignore[arg-type]
688 701 _DummyNamedTuple,
689 702 _IdentitySubscript,
690 703 }
691 704
692 705
693 706 def _list_methods(cls, source=None):
694 707 """For use on immutable objects or with methods returning a copy"""
695 708 return [getattr(cls, k) for k in (source if source else dir(cls))]
696 709
697 710
698 711 dict_non_mutating_methods = ("copy", "keys", "values", "items")
699 712 list_non_mutating_methods = ("copy", "index", "count")
700 713 set_non_mutating_methods = set(dir(set)) & set(dir(frozenset))
701 714
702 715
703 716 dict_keys: Type[collections.abc.KeysView] = type({}.keys())
704 717
705 718 NUMERICS = {int, float, complex}
706 719
707 720 ALLOWED_CALLS = {
708 721 bytes,
709 722 *_list_methods(bytes),
710 723 dict,
711 724 *_list_methods(dict, dict_non_mutating_methods),
712 725 dict_keys.isdisjoint,
713 726 list,
714 727 *_list_methods(list, list_non_mutating_methods),
715 728 set,
716 729 *_list_methods(set, set_non_mutating_methods),
717 730 frozenset,
718 731 *_list_methods(frozenset),
719 732 range,
720 733 str,
721 734 *_list_methods(str),
722 735 tuple,
723 736 *_list_methods(tuple),
724 737 *NUMERICS,
725 738 *[method for numeric_cls in NUMERICS for method in _list_methods(numeric_cls)],
726 739 collections.deque,
727 740 *_list_methods(collections.deque, list_non_mutating_methods),
728 741 collections.defaultdict,
729 742 *_list_methods(collections.defaultdict, dict_non_mutating_methods),
730 743 collections.OrderedDict,
731 744 *_list_methods(collections.OrderedDict, dict_non_mutating_methods),
732 745 collections.UserDict,
733 746 *_list_methods(collections.UserDict, dict_non_mutating_methods),
734 747 collections.UserList,
735 748 *_list_methods(collections.UserList, list_non_mutating_methods),
736 749 collections.UserString,
737 750 *_list_methods(collections.UserString, dir(str)),
738 751 collections.Counter,
739 752 *_list_methods(collections.Counter, dict_non_mutating_methods),
740 753 collections.Counter.elements,
741 754 collections.Counter.most_common,
742 755 }
743 756
744 757 BUILTIN_GETATTR: Set[MayHaveGetattr] = {
745 758 *BUILTIN_GETITEM,
746 759 set,
747 760 frozenset,
748 761 object,
749 762 type, # `type` handles a lot of generic cases, e.g. numbers as in `int.real`.
750 763 *NUMERICS,
751 764 dict_keys,
752 765 MethodDescriptorType,
753 766 ModuleType,
754 767 }
755 768
756 769
757 770 BUILTIN_OPERATIONS = {*BUILTIN_GETATTR}
758 771
759 772 EVALUATION_POLICIES = {
760 773 "minimal": EvaluationPolicy(
761 774 allow_builtins_access=True,
762 775 allow_locals_access=False,
763 776 allow_globals_access=False,
764 777 allow_item_access=False,
765 778 allow_attr_access=False,
766 779 allowed_calls=set(),
767 780 allow_any_calls=False,
768 781 allow_all_operations=False,
769 782 ),
770 783 "limited": SelectivePolicy(
771 784 allowed_getitem=BUILTIN_GETITEM,
772 785 allowed_getitem_external=SUPPORTED_EXTERNAL_GETITEM,
773 786 allowed_getattr=BUILTIN_GETATTR,
774 787 allowed_getattr_external={
775 788 # pandas Series/Frame implements custom `__getattr__`
776 789 ("pandas", "DataFrame"),
777 790 ("pandas", "Series"),
778 791 },
779 792 allowed_operations=BUILTIN_OPERATIONS,
780 793 allow_builtins_access=True,
781 794 allow_locals_access=True,
782 795 allow_globals_access=True,
783 796 allowed_calls=ALLOWED_CALLS,
784 797 ),
785 798 "unsafe": EvaluationPolicy(
786 799 allow_builtins_access=True,
787 800 allow_locals_access=True,
788 801 allow_globals_access=True,
789 802 allow_attr_access=True,
790 803 allow_item_access=True,
791 804 allow_any_calls=True,
792 805 allow_all_operations=True,
793 806 ),
794 807 }
795 808
796 809
797 810 __all__ = [
798 811 "guarded_eval",
799 812 "eval_node",
800 813 "GuardRejection",
801 814 "EvaluationContext",
802 815 "_unbind_method",
803 816 ]
@@ -1,641 +1,681 b''
1 1 from contextlib import contextmanager
2 from typing import NamedTuple
2 from typing import NamedTuple, Literal, NewType
3 3 from functools import partial
4 4 from IPython.core.guarded_eval import (
5 5 EvaluationContext,
6 6 GuardRejection,
7 7 guarded_eval,
8 8 _unbind_method,
9 9 )
10 from typing_extensions import (
11 Self, # Python >=3.10
12 TypeAliasType, # Python >=3.12
13 )
10 14 from IPython.testing import decorators as dec
11 15 import pytest
12 16
13 17
14 18 def create_context(evaluation: str, **kwargs):
15 19 return EvaluationContext(locals=kwargs, globals={}, evaluation=evaluation)
16 20
17 21
18 22 forbidden = partial(create_context, "forbidden")
19 23 minimal = partial(create_context, "minimal")
20 24 limited = partial(create_context, "limited")
21 25 unsafe = partial(create_context, "unsafe")
22 26 dangerous = partial(create_context, "dangerous")
23 27
24 28 LIMITED_OR_HIGHER = [limited, unsafe, dangerous]
25 29 MINIMAL_OR_HIGHER = [minimal, *LIMITED_OR_HIGHER]
26 30
27 31
28 32 @contextmanager
29 33 def module_not_installed(module: str):
30 34 import sys
31 35
32 36 try:
33 37 to_restore = sys.modules[module]
34 38 del sys.modules[module]
35 39 except KeyError:
36 40 to_restore = None
37 41 try:
38 42 yield
39 43 finally:
40 44 sys.modules[module] = to_restore
41 45
42 46
43 47 def test_external_not_installed():
44 48 """
45 49 Because attribute check requires checking if object is not of allowed
46 50 external type, this tests logic for absence of external module.
47 51 """
48 52
49 53 class Custom:
50 54 def __init__(self):
51 55 self.test = 1
52 56
53 57 def __getattr__(self, key):
54 58 return key
55 59
56 60 with module_not_installed("pandas"):
57 61 context = limited(x=Custom())
58 62 with pytest.raises(GuardRejection):
59 63 guarded_eval("x.test", context)
60 64
61 65
62 66 @dec.skip_without("pandas")
63 67 def test_external_changed_api(monkeypatch):
64 68 """Check that the execution rejects if external API changed paths"""
65 69 import pandas as pd
66 70
67 71 series = pd.Series([1], index=["a"])
68 72
69 73 with monkeypatch.context() as m:
70 74 m.delattr(pd, "Series")
71 75 context = limited(data=series)
72 76 with pytest.raises(GuardRejection):
73 77 guarded_eval("data.iloc[0]", context)
74 78
75 79
76 80 @dec.skip_without("pandas")
77 81 def test_pandas_series_iloc():
78 82 import pandas as pd
79 83
80 84 series = pd.Series([1], index=["a"])
81 85 context = limited(data=series)
82 86 assert guarded_eval("data.iloc[0]", context) == 1
83 87
84 88
85 89 def test_rejects_custom_properties():
86 90 class BadProperty:
87 91 @property
88 92 def iloc(self):
89 93 return [None]
90 94
91 95 series = BadProperty()
92 96 context = limited(data=series)
93 97
94 98 with pytest.raises(GuardRejection):
95 99 guarded_eval("data.iloc[0]", context)
96 100
97 101
98 102 @dec.skip_without("pandas")
99 103 def test_accepts_non_overriden_properties():
100 104 import pandas as pd
101 105
102 106 class GoodProperty(pd.Series):
103 107 pass
104 108
105 109 series = GoodProperty([1], index=["a"])
106 110 context = limited(data=series)
107 111
108 112 assert guarded_eval("data.iloc[0]", context) == 1
109 113
110 114
111 115 @dec.skip_without("pandas")
112 116 def test_pandas_series():
113 117 import pandas as pd
114 118
115 119 context = limited(data=pd.Series([1], index=["a"]))
116 120 assert guarded_eval('data["a"]', context) == 1
117 121 with pytest.raises(KeyError):
118 122 guarded_eval('data["c"]', context)
119 123
120 124
121 125 @dec.skip_without("pandas")
122 126 def test_pandas_bad_series():
123 127 import pandas as pd
124 128
125 129 class BadItemSeries(pd.Series):
126 130 def __getitem__(self, key):
127 131 return "CUSTOM_ITEM"
128 132
129 133 class BadAttrSeries(pd.Series):
130 134 def __getattr__(self, key):
131 135 return "CUSTOM_ATTR"
132 136
133 137 bad_series = BadItemSeries([1], index=["a"])
134 138 context = limited(data=bad_series)
135 139
136 140 with pytest.raises(GuardRejection):
137 141 guarded_eval('data["a"]', context)
138 142 with pytest.raises(GuardRejection):
139 143 guarded_eval('data["c"]', context)
140 144
141 145 # note: here result is a bit unexpected because
142 146 # pandas `__getattr__` calls `__getitem__`;
143 147 # FIXME - special case to handle it?
144 148 assert guarded_eval("data.a", context) == "CUSTOM_ITEM"
145 149
146 150 context = unsafe(data=bad_series)
147 151 assert guarded_eval('data["a"]', context) == "CUSTOM_ITEM"
148 152
149 153 bad_attr_series = BadAttrSeries([1], index=["a"])
150 154 context = limited(data=bad_attr_series)
151 155 assert guarded_eval('data["a"]', context) == 1
152 156 with pytest.raises(GuardRejection):
153 157 guarded_eval("data.a", context)
154 158
155 159
156 160 @dec.skip_without("pandas")
157 161 def test_pandas_dataframe_loc():
158 162 import pandas as pd
159 163 from pandas.testing import assert_series_equal
160 164
161 165 data = pd.DataFrame([{"a": 1}])
162 166 context = limited(data=data)
163 167 assert_series_equal(guarded_eval('data.loc[:, "a"]', context), data["a"])
164 168
165 169
166 170 def test_named_tuple():
167 171 class GoodNamedTuple(NamedTuple):
168 172 a: str
169 173 pass
170 174
171 175 class BadNamedTuple(NamedTuple):
172 176 a: str
173 177
174 178 def __getitem__(self, key):
175 179 return None
176 180
177 181 good = GoodNamedTuple(a="x")
178 182 bad = BadNamedTuple(a="x")
179 183
180 184 context = limited(data=good)
181 185 assert guarded_eval("data[0]", context) == "x"
182 186
183 187 context = limited(data=bad)
184 188 with pytest.raises(GuardRejection):
185 189 guarded_eval("data[0]", context)
186 190
187 191
188 192 def test_dict():
189 193 context = limited(data={"a": 1, "b": {"x": 2}, ("x", "y"): 3})
190 194 assert guarded_eval('data["a"]', context) == 1
191 195 assert guarded_eval('data["b"]', context) == {"x": 2}
192 196 assert guarded_eval('data["b"]["x"]', context) == 2
193 197 assert guarded_eval('data["x", "y"]', context) == 3
194 198
195 199 assert guarded_eval("data.keys", context)
196 200
197 201
198 202 def test_set():
199 203 context = limited(data={"a", "b"})
200 204 assert guarded_eval("data.difference", context)
201 205
202 206
203 207 def test_list():
204 208 context = limited(data=[1, 2, 3])
205 209 assert guarded_eval("data[1]", context) == 2
206 210 assert guarded_eval("data.copy", context)
207 211
208 212
209 213 def test_dict_literal():
210 214 context = limited()
211 215 assert guarded_eval("{}", context) == {}
212 216 assert guarded_eval('{"a": 1}', context) == {"a": 1}
213 217
214 218
215 219 def test_list_literal():
216 220 context = limited()
217 221 assert guarded_eval("[]", context) == []
218 222 assert guarded_eval('[1, "a"]', context) == [1, "a"]
219 223
220 224
221 225 def test_set_literal():
222 226 context = limited()
223 227 assert guarded_eval("set()", context) == set()
224 228 assert guarded_eval('{"a"}', context) == {"a"}
225 229
226 230
227 231 def test_evaluates_if_expression():
228 232 context = limited()
229 233 assert guarded_eval("2 if True else 3", context) == 2
230 234 assert guarded_eval("4 if False else 5", context) == 5
231 235
232 236
233 237 def test_object():
234 238 obj = object()
235 239 context = limited(obj=obj)
236 240 assert guarded_eval("obj.__dir__", context) == obj.__dir__
237 241
238 242
239 243 @pytest.mark.parametrize(
240 244 "code,expected",
241 245 [
242 246 ["int.numerator", int.numerator],
243 247 ["float.is_integer", float.is_integer],
244 248 ["complex.real", complex.real],
245 249 ],
246 250 )
247 251 def test_number_attributes(code, expected):
248 252 assert guarded_eval(code, limited()) == expected
249 253
250 254
251 255 def test_method_descriptor():
252 256 context = limited()
253 257 assert guarded_eval("list.copy.__name__", context) == "copy"
254 258
255 259
256 260 class HeapType:
257 261 pass
258 262
259 263
260 264 class CallCreatesHeapType:
261 265 def __call__(self) -> HeapType:
262 266 return HeapType()
263 267
264 268
265 269 class CallCreatesBuiltin:
266 270 def __call__(self) -> frozenset:
267 271 return frozenset()
268 272
269 273
270 274 class HasStaticMethod:
271 275 @staticmethod
272 276 def static_method() -> HeapType:
273 277 return HeapType()
274 278
275 279
276 280 class InitReturnsFrozenset:
277 281 def __new__(self) -> frozenset: # type:ignore[misc]
278 282 return frozenset()
279 283
280 284
281 285 class StringAnnotation:
282 286 def heap(self) -> "HeapType":
283 287 return HeapType()
284 288
285 289 def copy(self) -> "StringAnnotation":
286 290 return StringAnnotation()
287 291
288 292
293 CustomIntType = NewType("CustomIntType", int)
294 CustomHeapType = NewType("CustomHeapType", HeapType)
295 IntTypeAlias = TypeAliasType("IntTypeAlias", int)
296 HeapTypeAlias = TypeAliasType("HeapTypeAlias", HeapType)
297
298
299 class SpecialTyping:
300 def custom_int_type(self) -> CustomIntType:
301 return CustomIntType(1)
302
303 def custom_heap_type(self) -> CustomHeapType:
304 return CustomHeapType(HeapType())
305
306 # TODO: remove type:ignore comment once mypy
307 # supports explicit calls to `TypeAliasType`, see:
308 # https://github.com/python/mypy/issues/16614
309 def int_type_alias(self) -> IntTypeAlias: # type:ignore[valid-type]
310 return 1
311
312 def heap_type_alias(self) -> HeapTypeAlias: # type:ignore[valid-type]
313 return 1
314
315 def literal(self) -> Literal[False]:
316 return False
317
318 def self(self) -> Self:
319 return self
320
321
289 322 @pytest.mark.parametrize(
290 323 "data,good,expected,equality",
291 324 [
292 325 [[1, 2, 3], "data.index(2)", 1, True],
293 326 [{"a": 1}, "data.keys().isdisjoint({})", True, True],
294 327 [StringAnnotation(), "data.heap()", HeapType, False],
295 328 [StringAnnotation(), "data.copy()", StringAnnotation, False],
296 329 # test cases for `__call__`
297 330 [CallCreatesHeapType(), "data()", HeapType, False],
298 331 [CallCreatesBuiltin(), "data()", frozenset, False],
299 332 # Test cases for `__init__`
300 333 [HeapType, "data()", HeapType, False],
301 334 [InitReturnsFrozenset, "data()", frozenset, False],
302 335 [HeapType(), "data.__class__()", HeapType, False],
336 # supported special cases for typing
337 [SpecialTyping(), "data.custom_int_type()", int, False],
338 [SpecialTyping(), "data.custom_heap_type()", HeapType, False],
339 [SpecialTyping(), "data.int_type_alias()", int, False],
340 [SpecialTyping(), "data.heap_type_alias()", HeapType, False],
341 [SpecialTyping(), "data.self()", SpecialTyping, False],
342 [SpecialTyping(), "data.literal()", False, True],
303 343 # test cases for static methods
304 344 [HasStaticMethod, "data.static_method()", HeapType, False],
305 345 ],
306 346 )
307 347 def test_evaluates_calls(data, good, expected, equality):
308 348 context = limited(data=data, HeapType=HeapType, StringAnnotation=StringAnnotation)
309 349 value = guarded_eval(good, context)
310 350 if equality:
311 351 assert value == expected
312 352 else:
313 353 assert isinstance(value, expected)
314 354
315 355
316 356 @pytest.mark.parametrize(
317 357 "data,bad",
318 358 [
319 359 [[1, 2, 3], "data.append(4)"],
320 360 [{"a": 1}, "data.update()"],
321 361 ],
322 362 )
323 363 def test_rejects_calls_with_side_effects(data, bad):
324 364 context = limited(data=data)
325 365
326 366 with pytest.raises(GuardRejection):
327 367 guarded_eval(bad, context)
328 368
329 369
330 370 @pytest.mark.parametrize(
331 371 "code,expected",
332 372 [
333 373 ["(1\n+\n1)", 2],
334 374 ["list(range(10))[-1:]", [9]],
335 375 ["list(range(20))[3:-2:3]", [3, 6, 9, 12, 15]],
336 376 ],
337 377 )
338 378 @pytest.mark.parametrize("context", LIMITED_OR_HIGHER)
339 379 def test_evaluates_complex_cases(code, expected, context):
340 380 assert guarded_eval(code, context()) == expected
341 381
342 382
343 383 @pytest.mark.parametrize(
344 384 "code,expected",
345 385 [
346 386 ["1", 1],
347 387 ["1.0", 1.0],
348 388 ["0xdeedbeef", 0xDEEDBEEF],
349 389 ["True", True],
350 390 ["None", None],
351 391 ["{}", {}],
352 392 ["[]", []],
353 393 ],
354 394 )
355 395 @pytest.mark.parametrize("context", MINIMAL_OR_HIGHER)
356 396 def test_evaluates_literals(code, expected, context):
357 397 assert guarded_eval(code, context()) == expected
358 398
359 399
360 400 @pytest.mark.parametrize(
361 401 "code,expected",
362 402 [
363 403 ["-5", -5],
364 404 ["+5", +5],
365 405 ["~5", -6],
366 406 ],
367 407 )
368 408 @pytest.mark.parametrize("context", LIMITED_OR_HIGHER)
369 409 def test_evaluates_unary_operations(code, expected, context):
370 410 assert guarded_eval(code, context()) == expected
371 411
372 412
373 413 @pytest.mark.parametrize(
374 414 "code,expected",
375 415 [
376 416 ["1 + 1", 2],
377 417 ["3 - 1", 2],
378 418 ["2 * 3", 6],
379 419 ["5 // 2", 2],
380 420 ["5 / 2", 2.5],
381 421 ["5**2", 25],
382 422 ["2 >> 1", 1],
383 423 ["2 << 1", 4],
384 424 ["1 | 2", 3],
385 425 ["1 & 1", 1],
386 426 ["1 & 2", 0],
387 427 ],
388 428 )
389 429 @pytest.mark.parametrize("context", LIMITED_OR_HIGHER)
390 430 def test_evaluates_binary_operations(code, expected, context):
391 431 assert guarded_eval(code, context()) == expected
392 432
393 433
394 434 @pytest.mark.parametrize(
395 435 "code,expected",
396 436 [
397 437 ["2 > 1", True],
398 438 ["2 < 1", False],
399 439 ["2 <= 1", False],
400 440 ["2 <= 2", True],
401 441 ["1 >= 2", False],
402 442 ["2 >= 2", True],
403 443 ["2 == 2", True],
404 444 ["1 == 2", False],
405 445 ["1 != 2", True],
406 446 ["1 != 1", False],
407 447 ["1 < 4 < 3", False],
408 448 ["(1 < 4) < 3", True],
409 449 ["4 > 3 > 2 > 1", True],
410 450 ["4 > 3 > 2 > 9", False],
411 451 ["1 < 2 < 3 < 4", True],
412 452 ["9 < 2 < 3 < 4", False],
413 453 ["1 < 2 > 1 > 0 > -1 < 1", True],
414 454 ["1 in [1] in [[1]]", True],
415 455 ["1 in [1] in [[2]]", False],
416 456 ["1 in [1]", True],
417 457 ["0 in [1]", False],
418 458 ["1 not in [1]", False],
419 459 ["0 not in [1]", True],
420 460 ["True is True", True],
421 461 ["False is False", True],
422 462 ["True is False", False],
423 463 ["True is not True", False],
424 464 ["False is not True", True],
425 465 ],
426 466 )
427 467 @pytest.mark.parametrize("context", LIMITED_OR_HIGHER)
428 468 def test_evaluates_comparisons(code, expected, context):
429 469 assert guarded_eval(code, context()) == expected
430 470
431 471
432 472 def test_guards_comparisons():
433 473 class GoodEq(int):
434 474 pass
435 475
436 476 class BadEq(int):
437 477 def __eq__(self, other):
438 478 assert False
439 479
440 480 context = limited(bad=BadEq(1), good=GoodEq(1))
441 481
442 482 with pytest.raises(GuardRejection):
443 483 guarded_eval("bad == 1", context)
444 484
445 485 with pytest.raises(GuardRejection):
446 486 guarded_eval("bad != 1", context)
447 487
448 488 with pytest.raises(GuardRejection):
449 489 guarded_eval("1 == bad", context)
450 490
451 491 with pytest.raises(GuardRejection):
452 492 guarded_eval("1 != bad", context)
453 493
454 494 assert guarded_eval("good == 1", context) is True
455 495 assert guarded_eval("good != 1", context) is False
456 496 assert guarded_eval("1 == good", context) is True
457 497 assert guarded_eval("1 != good", context) is False
458 498
459 499
460 500 def test_guards_unary_operations():
461 501 class GoodOp(int):
462 502 pass
463 503
464 504 class BadOpInv(int):
465 505 def __inv__(self, other):
466 506 assert False
467 507
468 508 class BadOpInverse(int):
469 509 def __inv__(self, other):
470 510 assert False
471 511
472 512 context = limited(good=GoodOp(1), bad1=BadOpInv(1), bad2=BadOpInverse(1))
473 513
474 514 with pytest.raises(GuardRejection):
475 515 guarded_eval("~bad1", context)
476 516
477 517 with pytest.raises(GuardRejection):
478 518 guarded_eval("~bad2", context)
479 519
480 520
481 521 def test_guards_binary_operations():
482 522 class GoodOp(int):
483 523 pass
484 524
485 525 class BadOp(int):
486 526 def __add__(self, other):
487 527 assert False
488 528
489 529 context = limited(good=GoodOp(1), bad=BadOp(1))
490 530
491 531 with pytest.raises(GuardRejection):
492 532 guarded_eval("1 + bad", context)
493 533
494 534 with pytest.raises(GuardRejection):
495 535 guarded_eval("bad + 1", context)
496 536
497 537 assert guarded_eval("good + 1", context) == 2
498 538 assert guarded_eval("1 + good", context) == 2
499 539
500 540
501 541 def test_guards_attributes():
502 542 class GoodAttr(float):
503 543 pass
504 544
505 545 class BadAttr1(float):
506 546 def __getattr__(self, key):
507 547 assert False
508 548
509 549 class BadAttr2(float):
510 550 def __getattribute__(self, key):
511 551 assert False
512 552
513 553 context = limited(good=GoodAttr(0.5), bad1=BadAttr1(0.5), bad2=BadAttr2(0.5))
514 554
515 555 with pytest.raises(GuardRejection):
516 556 guarded_eval("bad1.as_integer_ratio", context)
517 557
518 558 with pytest.raises(GuardRejection):
519 559 guarded_eval("bad2.as_integer_ratio", context)
520 560
521 561 assert guarded_eval("good.as_integer_ratio()", context) == (1, 2)
522 562
523 563
524 564 @pytest.mark.parametrize("context", MINIMAL_OR_HIGHER)
525 565 def test_access_builtins(context):
526 566 assert guarded_eval("round", context()) == round
527 567
528 568
529 569 def test_access_builtins_fails():
530 570 context = limited()
531 571 with pytest.raises(NameError):
532 572 guarded_eval("this_is_not_builtin", context)
533 573
534 574
535 575 def test_rejects_forbidden():
536 576 context = forbidden()
537 577 with pytest.raises(GuardRejection):
538 578 guarded_eval("1", context)
539 579
540 580
541 581 def test_guards_locals_and_globals():
542 582 context = EvaluationContext(
543 583 locals={"local_a": "a"}, globals={"global_b": "b"}, evaluation="minimal"
544 584 )
545 585
546 586 with pytest.raises(GuardRejection):
547 587 guarded_eval("local_a", context)
548 588
549 589 with pytest.raises(GuardRejection):
550 590 guarded_eval("global_b", context)
551 591
552 592
553 593 def test_access_locals_and_globals():
554 594 context = EvaluationContext(
555 595 locals={"local_a": "a"}, globals={"global_b": "b"}, evaluation="limited"
556 596 )
557 597 assert guarded_eval("local_a", context) == "a"
558 598 assert guarded_eval("global_b", context) == "b"
559 599
560 600
561 601 @pytest.mark.parametrize(
562 602 "code",
563 603 ["def func(): pass", "class C: pass", "x = 1", "x += 1", "del x", "import ast"],
564 604 )
565 605 @pytest.mark.parametrize("context", [minimal(), limited(), unsafe()])
566 606 def test_rejects_side_effect_syntax(code, context):
567 607 with pytest.raises(SyntaxError):
568 608 guarded_eval(code, context)
569 609
570 610
571 611 def test_subscript():
572 612 context = EvaluationContext(
573 613 locals={}, globals={}, evaluation="limited", in_subscript=True
574 614 )
575 615 empty_slice = slice(None, None, None)
576 616 assert guarded_eval("", context) == tuple()
577 617 assert guarded_eval(":", context) == empty_slice
578 618 assert guarded_eval("1:2:3", context) == slice(1, 2, 3)
579 619 assert guarded_eval(':, "a"', context) == (empty_slice, "a")
580 620
581 621
582 622 def test_unbind_method():
583 623 class X(list):
584 624 def index(self, k):
585 625 return "CUSTOM"
586 626
587 627 x = X()
588 628 assert _unbind_method(x.index) is X.index
589 629 assert _unbind_method([].index) is list.index
590 630 assert _unbind_method(list.index) is None
591 631
592 632
593 633 def test_assumption_instance_attr_do_not_matter():
594 634 """This is semi-specified in Python documentation.
595 635
596 636 However, since the specification says 'not guaranteed
597 637 to work' rather than 'is forbidden to work', future
598 638 versions could invalidate this assumptions. This test
599 639 is meant to catch such a change if it ever comes true.
600 640 """
601 641
602 642 class T:
603 643 def __getitem__(self, k):
604 644 return "a"
605 645
606 646 def __getattr__(self, k):
607 647 return "a"
608 648
609 649 def f(self):
610 650 return "b"
611 651
612 652 t = T()
613 653 t.__getitem__ = f
614 654 t.__getattr__ = f
615 655 assert t[1] == "a"
616 656 assert t[1] == "a"
617 657
618 658
619 659 def test_assumption_named_tuples_share_getitem():
620 660 """Check assumption on named tuples sharing __getitem__"""
621 661 from typing import NamedTuple
622 662
623 663 class A(NamedTuple):
624 664 pass
625 665
626 666 class B(NamedTuple):
627 667 pass
628 668
629 669 assert A.__getitem__ == B.__getitem__
630 670
631 671
632 672 @dec.skip_without("numpy")
633 673 def test_module_access():
634 674 import numpy
635 675
636 676 context = limited(numpy=numpy)
637 677 assert guarded_eval("numpy.linalg.norm", context) == numpy.linalg.norm
638 678
639 679 context = minimal(numpy=numpy)
640 680 with pytest.raises(GuardRejection):
641 681 guarded_eval("np.linalg.norm", context)
@@ -1,206 +1,206 b''
1 1 [build-system]
2 2 requires = ["setuptools>=61.2"]
3 3 # We need access to the 'setupbase' module at build time.
4 4 # Hence we declare a custom build backend.
5 5 build-backend = "_build_meta" # just re-exports setuptools.build_meta definitions
6 6 backend-path = ["."]
7 7
8 8 [project]
9 9 name = "ipython"
10 10 description = "IPython: Productive Interactive Computing"
11 11 keywords = ["Interactive", "Interpreter", "Shell", "Embedding"]
12 12 classifiers = [
13 13 "Framework :: IPython",
14 14 "Framework :: Jupyter",
15 15 "Intended Audience :: Developers",
16 16 "Intended Audience :: Science/Research",
17 17 "License :: OSI Approved :: BSD License",
18 18 "Programming Language :: Python",
19 19 "Programming Language :: Python :: 3",
20 20 "Programming Language :: Python :: 3 :: Only",
21 21 "Topic :: System :: Shells",
22 22 ]
23 23 requires-python = ">=3.10"
24 24 dependencies = [
25 25 'colorama; sys_platform == "win32"',
26 26 "decorator",
27 27 "exceptiongroup; python_version<'3.11'",
28 28 "jedi>=0.16",
29 29 "matplotlib-inline",
30 30 'pexpect>4.3; sys_platform != "win32" and sys_platform != "emscripten"',
31 31 "prompt_toolkit>=3.0.41,<3.1.0",
32 32 "pygments>=2.4.0",
33 33 "stack_data",
34 34 "traitlets>=5.13.0",
35 "typing_extensions; python_version<'3.10'",
35 "typing_extensions; python_version<'3.12'",
36 36 ]
37 37 dynamic = ["authors", "license", "version"]
38 38
39 39 [project.entry-points."pygments.lexers"]
40 40 ipythonconsole = "IPython.lib.lexers:IPythonConsoleLexer"
41 41 ipython = "IPython.lib.lexers:IPythonLexer"
42 42 ipython3 = "IPython.lib.lexers:IPython3Lexer"
43 43
44 44 [project.scripts]
45 45 ipython = "IPython:start_ipython"
46 46 ipython3 = "IPython:start_ipython"
47 47
48 48 [project.readme]
49 49 file = "long_description.rst"
50 50 content-type = "text/x-rst"
51 51
52 52 [project.urls]
53 53 Homepage = "https://ipython.org"
54 54 Documentation = "https://ipython.readthedocs.io/"
55 55 Funding = "https://numfocus.org/"
56 56 Source = "https://github.com/ipython/ipython"
57 57 Tracker = "https://github.com/ipython/ipython/issues"
58 58
59 59 [project.optional-dependencies]
60 60 black = [
61 61 "black",
62 62 ]
63 63 doc = [
64 64 "ipykernel",
65 65 "setuptools>=18.5",
66 66 "sphinx>=1.3",
67 67 "sphinx-rtd-theme",
68 68 "sphinxcontrib-jquery",
69 69 "docrepr",
70 70 "matplotlib",
71 71 "stack_data",
72 72 "typing_extensions",
73 73 "exceptiongroup",
74 74 "ipython[test]",
75 75 ]
76 76 kernel = [
77 77 "ipykernel",
78 78 ]
79 79 nbconvert = [
80 80 "nbconvert",
81 81 ]
82 82 nbformat = [
83 83 "nbformat",
84 84 ]
85 85 notebook = [
86 86 "ipywidgets",
87 87 "notebook",
88 88 ]
89 89 parallel = [
90 90 "ipyparallel",
91 91 ]
92 92 qtconsole = [
93 93 "qtconsole",
94 94 ]
95 95 terminal = []
96 96 test = [
97 97 "pytest<8",
98 98 "pytest-asyncio<0.22",
99 99 "testpath",
100 100 "pickleshare",
101 101 ]
102 102 test_extra = [
103 103 "ipython[test]",
104 104 "curio",
105 105 "matplotlib!=3.2.0",
106 106 "nbformat",
107 107 "numpy>=1.23",
108 108 "pandas",
109 109 "trio",
110 110 ]
111 111 all = [
112 112 "ipython[black,doc,kernel,nbconvert,nbformat,notebook,parallel,qtconsole,terminal]",
113 113 "ipython[test,test_extra]",
114 114 ]
115 115
116 116 [tool.mypy]
117 117 python_version = "3.10"
118 118 ignore_missing_imports = true
119 119 follow_imports = 'silent'
120 120 exclude = [
121 121 'test_\.+\.py',
122 122 'IPython.utils.tests.test_wildcard',
123 123 'testing',
124 124 'tests',
125 125 'PyColorize.py',
126 126 '_process_win32_controller.py',
127 127 'IPython/core/application.py',
128 128 'IPython/core/profileapp.py',
129 129 'IPython/lib/deepreload.py',
130 130 'IPython/sphinxext/ipython_directive.py',
131 131 'IPython/terminal/ipapp.py',
132 132 'IPython/utils/_process_win32.py',
133 133 'IPython/utils/path.py',
134 134 ]
135 135
136 136 [tool.pytest.ini_options]
137 137 addopts = [
138 138 "--durations=10",
139 139 "-pIPython.testing.plugin.pytest_ipdoctest",
140 140 "--ipdoctest-modules",
141 141 "--ignore=docs",
142 142 "--ignore=examples",
143 143 "--ignore=htmlcov",
144 144 "--ignore=ipython_kernel",
145 145 "--ignore=ipython_parallel",
146 146 "--ignore=results",
147 147 "--ignore=tmp",
148 148 "--ignore=tools",
149 149 "--ignore=traitlets",
150 150 "--ignore=IPython/core/tests/daft_extension",
151 151 "--ignore=IPython/sphinxext",
152 152 "--ignore=IPython/terminal/pt_inputhooks",
153 153 "--ignore=IPython/__main__.py",
154 154 "--ignore=IPython/external/qt_for_kernel.py",
155 155 "--ignore=IPython/html/widgets/widget_link.py",
156 156 "--ignore=IPython/html/widgets/widget_output.py",
157 157 "--ignore=IPython/terminal/console.py",
158 158 "--ignore=IPython/utils/_process_cli.py",
159 159 "--ignore=IPython/utils/_process_posix.py",
160 160 "--ignore=IPython/utils/_process_win32.py",
161 161 "--ignore=IPython/utils/_process_win32_controller.py",
162 162 "--ignore=IPython/utils/daemonize.py",
163 163 "--ignore=IPython/utils/eventful.py",
164 164 "--ignore=IPython/kernel",
165 165 "--ignore=IPython/consoleapp.py",
166 166 "--ignore=IPython/core/inputsplitter.py",
167 167 "--ignore=IPython/lib/kernel.py",
168 168 "--ignore=IPython/utils/jsonutil.py",
169 169 "--ignore=IPython/utils/localinterfaces.py",
170 170 "--ignore=IPython/utils/log.py",
171 171 "--ignore=IPython/utils/signatures.py",
172 172 "--ignore=IPython/utils/traitlets.py",
173 173 "--ignore=IPython/utils/version.py"
174 174 ]
175 175 doctest_optionflags = [
176 176 "NORMALIZE_WHITESPACE",
177 177 "ELLIPSIS"
178 178 ]
179 179 ipdoctest_optionflags = [
180 180 "NORMALIZE_WHITESPACE",
181 181 "ELLIPSIS"
182 182 ]
183 183 asyncio_mode = "strict"
184 184
185 185 [tool.pyright]
186 186 pythonPlatform="All"
187 187
188 188 [tool.setuptools]
189 189 zip-safe = false
190 190 platforms = ["Linux", "Mac OSX", "Windows"]
191 191 license-files = ["LICENSE"]
192 192 include-package-data = false
193 193
194 194 [tool.setuptools.packages.find]
195 195 exclude = ["setupext"]
196 196 namespaces = false
197 197
198 198 [tool.setuptools.package-data]
199 199 "IPython" = ["py.typed"]
200 200 "IPython.core" = ["profile/README*"]
201 201 "IPython.core.tests" = ["*.png", "*.jpg", "daft_extension/*.py"]
202 202 "IPython.lib.tests" = ["*.wav"]
203 203 "IPython.testing.plugin" = ["*.txt"]
204 204
205 205 [tool.setuptools.dynamic]
206 206 version = {attr = "IPython.core.release.__version__"}
General Comments 0
You need to be logged in to leave comments. Login now