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