##// END OF EJS Templates
TST: Check that we can get completions for second-level Numpy modules
Carlos Cordoba -
Show More
@@ -1,570 +1,582 b''
1 1 from contextlib import contextmanager
2 2 from typing import NamedTuple
3 3 from functools import partial
4 4 from IPython.core.guarded_eval import (
5 5 EvaluationContext,
6 6 GuardRejection,
7 7 guarded_eval,
8 8 _unbind_method,
9 9 )
10 10 from IPython.testing import decorators as dec
11 11 import pytest
12 12
13 13
14 14 def create_context(evaluation: str, **kwargs):
15 15 return EvaluationContext(locals=kwargs, globals={}, evaluation=evaluation)
16 16
17 17
18 18 forbidden = partial(create_context, "forbidden")
19 19 minimal = partial(create_context, "minimal")
20 20 limited = partial(create_context, "limited")
21 21 unsafe = partial(create_context, "unsafe")
22 22 dangerous = partial(create_context, "dangerous")
23 23
24 24 LIMITED_OR_HIGHER = [limited, unsafe, dangerous]
25 25 MINIMAL_OR_HIGHER = [minimal, *LIMITED_OR_HIGHER]
26 26
27 27
28 28 @contextmanager
29 29 def module_not_installed(module: str):
30 30 import sys
31 31
32 32 try:
33 33 to_restore = sys.modules[module]
34 34 del sys.modules[module]
35 35 except KeyError:
36 36 to_restore = None
37 37 try:
38 38 yield
39 39 finally:
40 40 sys.modules[module] = to_restore
41 41
42 42
43 43 def test_external_not_installed():
44 44 """
45 45 Because attribute check requires checking if object is not of allowed
46 46 external type, this tests logic for absence of external module.
47 47 """
48 48
49 49 class Custom:
50 50 def __init__(self):
51 51 self.test = 1
52 52
53 53 def __getattr__(self, key):
54 54 return key
55 55
56 56 with module_not_installed("pandas"):
57 57 context = limited(x=Custom())
58 58 with pytest.raises(GuardRejection):
59 59 guarded_eval("x.test", context)
60 60
61 61
62 62 @dec.skip_without("pandas")
63 63 def test_external_changed_api(monkeypatch):
64 64 """Check that the execution rejects if external API changed paths"""
65 65 import pandas as pd
66 66
67 67 series = pd.Series([1], index=["a"])
68 68
69 69 with monkeypatch.context() as m:
70 70 m.delattr(pd, "Series")
71 71 context = limited(data=series)
72 72 with pytest.raises(GuardRejection):
73 73 guarded_eval("data.iloc[0]", context)
74 74
75 75
76 76 @dec.skip_without("pandas")
77 77 def test_pandas_series_iloc():
78 78 import pandas as pd
79 79
80 80 series = pd.Series([1], index=["a"])
81 81 context = limited(data=series)
82 82 assert guarded_eval("data.iloc[0]", context) == 1
83 83
84 84
85 85 def test_rejects_custom_properties():
86 86 class BadProperty:
87 87 @property
88 88 def iloc(self):
89 89 return [None]
90 90
91 91 series = BadProperty()
92 92 context = limited(data=series)
93 93
94 94 with pytest.raises(GuardRejection):
95 95 guarded_eval("data.iloc[0]", context)
96 96
97 97
98 98 @dec.skip_without("pandas")
99 99 def test_accepts_non_overriden_properties():
100 100 import pandas as pd
101 101
102 102 class GoodProperty(pd.Series):
103 103 pass
104 104
105 105 series = GoodProperty([1], index=["a"])
106 106 context = limited(data=series)
107 107
108 108 assert guarded_eval("data.iloc[0]", context) == 1
109 109
110 110
111 111 @dec.skip_without("pandas")
112 112 def test_pandas_series():
113 113 import pandas as pd
114 114
115 115 context = limited(data=pd.Series([1], index=["a"]))
116 116 assert guarded_eval('data["a"]', context) == 1
117 117 with pytest.raises(KeyError):
118 118 guarded_eval('data["c"]', context)
119 119
120 120
121 121 @dec.skip_without("pandas")
122 122 def test_pandas_bad_series():
123 123 import pandas as pd
124 124
125 125 class BadItemSeries(pd.Series):
126 126 def __getitem__(self, key):
127 127 return "CUSTOM_ITEM"
128 128
129 129 class BadAttrSeries(pd.Series):
130 130 def __getattr__(self, key):
131 131 return "CUSTOM_ATTR"
132 132
133 133 bad_series = BadItemSeries([1], index=["a"])
134 134 context = limited(data=bad_series)
135 135
136 136 with pytest.raises(GuardRejection):
137 137 guarded_eval('data["a"]', context)
138 138 with pytest.raises(GuardRejection):
139 139 guarded_eval('data["c"]', context)
140 140
141 141 # note: here result is a bit unexpected because
142 142 # pandas `__getattr__` calls `__getitem__`;
143 143 # FIXME - special case to handle it?
144 144 assert guarded_eval("data.a", context) == "CUSTOM_ITEM"
145 145
146 146 context = unsafe(data=bad_series)
147 147 assert guarded_eval('data["a"]', context) == "CUSTOM_ITEM"
148 148
149 149 bad_attr_series = BadAttrSeries([1], index=["a"])
150 150 context = limited(data=bad_attr_series)
151 151 assert guarded_eval('data["a"]', context) == 1
152 152 with pytest.raises(GuardRejection):
153 153 guarded_eval("data.a", context)
154 154
155 155
156 156 @dec.skip_without("pandas")
157 157 def test_pandas_dataframe_loc():
158 158 import pandas as pd
159 159 from pandas.testing import assert_series_equal
160 160
161 161 data = pd.DataFrame([{"a": 1}])
162 162 context = limited(data=data)
163 163 assert_series_equal(guarded_eval('data.loc[:, "a"]', context), data["a"])
164 164
165 165
166 166 def test_named_tuple():
167 167 class GoodNamedTuple(NamedTuple):
168 168 a: str
169 169 pass
170 170
171 171 class BadNamedTuple(NamedTuple):
172 172 a: str
173 173
174 174 def __getitem__(self, key):
175 175 return None
176 176
177 177 good = GoodNamedTuple(a="x")
178 178 bad = BadNamedTuple(a="x")
179 179
180 180 context = limited(data=good)
181 181 assert guarded_eval("data[0]", context) == "x"
182 182
183 183 context = limited(data=bad)
184 184 with pytest.raises(GuardRejection):
185 185 guarded_eval("data[0]", context)
186 186
187 187
188 188 def test_dict():
189 189 context = limited(data={"a": 1, "b": {"x": 2}, ("x", "y"): 3})
190 190 assert guarded_eval('data["a"]', context) == 1
191 191 assert guarded_eval('data["b"]', context) == {"x": 2}
192 192 assert guarded_eval('data["b"]["x"]', context) == 2
193 193 assert guarded_eval('data["x", "y"]', context) == 3
194 194
195 195 assert guarded_eval("data.keys", context)
196 196
197 197
198 198 def test_set():
199 199 context = limited(data={"a", "b"})
200 200 assert guarded_eval("data.difference", context)
201 201
202 202
203 203 def test_list():
204 204 context = limited(data=[1, 2, 3])
205 205 assert guarded_eval("data[1]", context) == 2
206 206 assert guarded_eval("data.copy", context)
207 207
208 208
209 209 def test_dict_literal():
210 210 context = limited()
211 211 assert guarded_eval("{}", context) == {}
212 212 assert guarded_eval('{"a": 1}', context) == {"a": 1}
213 213
214 214
215 215 def test_list_literal():
216 216 context = limited()
217 217 assert guarded_eval("[]", context) == []
218 218 assert guarded_eval('[1, "a"]', context) == [1, "a"]
219 219
220 220
221 221 def test_set_literal():
222 222 context = limited()
223 223 assert guarded_eval("set()", context) == set()
224 224 assert guarded_eval('{"a"}', context) == {"a"}
225 225
226 226
227 227 def test_evaluates_if_expression():
228 228 context = limited()
229 229 assert guarded_eval("2 if True else 3", context) == 2
230 230 assert guarded_eval("4 if False else 5", context) == 5
231 231
232 232
233 233 def test_object():
234 234 obj = object()
235 235 context = limited(obj=obj)
236 236 assert guarded_eval("obj.__dir__", context) == obj.__dir__
237 237
238 238
239 239 @pytest.mark.parametrize(
240 240 "code,expected",
241 241 [
242 242 ["int.numerator", int.numerator],
243 243 ["float.is_integer", float.is_integer],
244 244 ["complex.real", complex.real],
245 245 ],
246 246 )
247 247 def test_number_attributes(code, expected):
248 248 assert guarded_eval(code, limited()) == expected
249 249
250 250
251 251 def test_method_descriptor():
252 252 context = limited()
253 253 assert guarded_eval("list.copy.__name__", context) == "copy"
254 254
255 255
256 256 @pytest.mark.parametrize(
257 257 "data,good,bad,expected",
258 258 [
259 259 [[1, 2, 3], "data.index(2)", "data.append(4)", 1],
260 260 [{"a": 1}, "data.keys().isdisjoint({})", "data.update()", True],
261 261 ],
262 262 )
263 263 def test_evaluates_calls(data, good, bad, expected):
264 264 context = limited(data=data)
265 265 assert guarded_eval(good, context) == expected
266 266
267 267 with pytest.raises(GuardRejection):
268 268 guarded_eval(bad, context)
269 269
270 270
271 271 @pytest.mark.parametrize(
272 272 "code,expected",
273 273 [
274 274 ["(1\n+\n1)", 2],
275 275 ["list(range(10))[-1:]", [9]],
276 276 ["list(range(20))[3:-2:3]", [3, 6, 9, 12, 15]],
277 277 ],
278 278 )
279 279 @pytest.mark.parametrize("context", LIMITED_OR_HIGHER)
280 280 def test_evaluates_complex_cases(code, expected, context):
281 281 assert guarded_eval(code, context()) == expected
282 282
283 283
284 284 @pytest.mark.parametrize(
285 285 "code,expected",
286 286 [
287 287 ["1", 1],
288 288 ["1.0", 1.0],
289 289 ["0xdeedbeef", 0xDEEDBEEF],
290 290 ["True", True],
291 291 ["None", None],
292 292 ["{}", {}],
293 293 ["[]", []],
294 294 ],
295 295 )
296 296 @pytest.mark.parametrize("context", MINIMAL_OR_HIGHER)
297 297 def test_evaluates_literals(code, expected, context):
298 298 assert guarded_eval(code, context()) == expected
299 299
300 300
301 301 @pytest.mark.parametrize(
302 302 "code,expected",
303 303 [
304 304 ["-5", -5],
305 305 ["+5", +5],
306 306 ["~5", -6],
307 307 ],
308 308 )
309 309 @pytest.mark.parametrize("context", LIMITED_OR_HIGHER)
310 310 def test_evaluates_unary_operations(code, expected, context):
311 311 assert guarded_eval(code, context()) == expected
312 312
313 313
314 314 @pytest.mark.parametrize(
315 315 "code,expected",
316 316 [
317 317 ["1 + 1", 2],
318 318 ["3 - 1", 2],
319 319 ["2 * 3", 6],
320 320 ["5 // 2", 2],
321 321 ["5 / 2", 2.5],
322 322 ["5**2", 25],
323 323 ["2 >> 1", 1],
324 324 ["2 << 1", 4],
325 325 ["1 | 2", 3],
326 326 ["1 & 1", 1],
327 327 ["1 & 2", 0],
328 328 ],
329 329 )
330 330 @pytest.mark.parametrize("context", LIMITED_OR_HIGHER)
331 331 def test_evaluates_binary_operations(code, expected, context):
332 332 assert guarded_eval(code, context()) == expected
333 333
334 334
335 335 @pytest.mark.parametrize(
336 336 "code,expected",
337 337 [
338 338 ["2 > 1", True],
339 339 ["2 < 1", False],
340 340 ["2 <= 1", False],
341 341 ["2 <= 2", True],
342 342 ["1 >= 2", False],
343 343 ["2 >= 2", True],
344 344 ["2 == 2", True],
345 345 ["1 == 2", False],
346 346 ["1 != 2", True],
347 347 ["1 != 1", False],
348 348 ["1 < 4 < 3", False],
349 349 ["(1 < 4) < 3", True],
350 350 ["4 > 3 > 2 > 1", True],
351 351 ["4 > 3 > 2 > 9", False],
352 352 ["1 < 2 < 3 < 4", True],
353 353 ["9 < 2 < 3 < 4", False],
354 354 ["1 < 2 > 1 > 0 > -1 < 1", True],
355 355 ["1 in [1] in [[1]]", True],
356 356 ["1 in [1] in [[2]]", False],
357 357 ["1 in [1]", True],
358 358 ["0 in [1]", False],
359 359 ["1 not in [1]", False],
360 360 ["0 not in [1]", True],
361 361 ["True is True", True],
362 362 ["False is False", True],
363 363 ["True is False", False],
364 364 ["True is not True", False],
365 365 ["False is not True", True],
366 366 ],
367 367 )
368 368 @pytest.mark.parametrize("context", LIMITED_OR_HIGHER)
369 369 def test_evaluates_comparisons(code, expected, context):
370 370 assert guarded_eval(code, context()) == expected
371 371
372 372
373 373 def test_guards_comparisons():
374 374 class GoodEq(int):
375 375 pass
376 376
377 377 class BadEq(int):
378 378 def __eq__(self, other):
379 379 assert False
380 380
381 381 context = limited(bad=BadEq(1), good=GoodEq(1))
382 382
383 383 with pytest.raises(GuardRejection):
384 384 guarded_eval("bad == 1", context)
385 385
386 386 with pytest.raises(GuardRejection):
387 387 guarded_eval("bad != 1", context)
388 388
389 389 with pytest.raises(GuardRejection):
390 390 guarded_eval("1 == bad", context)
391 391
392 392 with pytest.raises(GuardRejection):
393 393 guarded_eval("1 != bad", context)
394 394
395 395 assert guarded_eval("good == 1", context) is True
396 396 assert guarded_eval("good != 1", context) is False
397 397 assert guarded_eval("1 == good", context) is True
398 398 assert guarded_eval("1 != good", context) is False
399 399
400 400
401 401 def test_guards_unary_operations():
402 402 class GoodOp(int):
403 403 pass
404 404
405 405 class BadOpInv(int):
406 406 def __inv__(self, other):
407 407 assert False
408 408
409 409 class BadOpInverse(int):
410 410 def __inv__(self, other):
411 411 assert False
412 412
413 413 context = limited(good=GoodOp(1), bad1=BadOpInv(1), bad2=BadOpInverse(1))
414 414
415 415 with pytest.raises(GuardRejection):
416 416 guarded_eval("~bad1", context)
417 417
418 418 with pytest.raises(GuardRejection):
419 419 guarded_eval("~bad2", context)
420 420
421 421
422 422 def test_guards_binary_operations():
423 423 class GoodOp(int):
424 424 pass
425 425
426 426 class BadOp(int):
427 427 def __add__(self, other):
428 428 assert False
429 429
430 430 context = limited(good=GoodOp(1), bad=BadOp(1))
431 431
432 432 with pytest.raises(GuardRejection):
433 433 guarded_eval("1 + bad", context)
434 434
435 435 with pytest.raises(GuardRejection):
436 436 guarded_eval("bad + 1", context)
437 437
438 438 assert guarded_eval("good + 1", context) == 2
439 439 assert guarded_eval("1 + good", context) == 2
440 440
441 441
442 442 def test_guards_attributes():
443 443 class GoodAttr(float):
444 444 pass
445 445
446 446 class BadAttr1(float):
447 447 def __getattr__(self, key):
448 448 assert False
449 449
450 450 class BadAttr2(float):
451 451 def __getattribute__(self, key):
452 452 assert False
453 453
454 454 context = limited(good=GoodAttr(0.5), bad1=BadAttr1(0.5), bad2=BadAttr2(0.5))
455 455
456 456 with pytest.raises(GuardRejection):
457 457 guarded_eval("bad1.as_integer_ratio", context)
458 458
459 459 with pytest.raises(GuardRejection):
460 460 guarded_eval("bad2.as_integer_ratio", context)
461 461
462 462 assert guarded_eval("good.as_integer_ratio()", context) == (1, 2)
463 463
464 464
465 465 @pytest.mark.parametrize("context", MINIMAL_OR_HIGHER)
466 466 def test_access_builtins(context):
467 467 assert guarded_eval("round", context()) == round
468 468
469 469
470 470 def test_access_builtins_fails():
471 471 context = limited()
472 472 with pytest.raises(NameError):
473 473 guarded_eval("this_is_not_builtin", context)
474 474
475 475
476 476 def test_rejects_forbidden():
477 477 context = forbidden()
478 478 with pytest.raises(GuardRejection):
479 479 guarded_eval("1", context)
480 480
481 481
482 482 def test_guards_locals_and_globals():
483 483 context = EvaluationContext(
484 484 locals={"local_a": "a"}, globals={"global_b": "b"}, evaluation="minimal"
485 485 )
486 486
487 487 with pytest.raises(GuardRejection):
488 488 guarded_eval("local_a", context)
489 489
490 490 with pytest.raises(GuardRejection):
491 491 guarded_eval("global_b", context)
492 492
493 493
494 494 def test_access_locals_and_globals():
495 495 context = EvaluationContext(
496 496 locals={"local_a": "a"}, globals={"global_b": "b"}, evaluation="limited"
497 497 )
498 498 assert guarded_eval("local_a", context) == "a"
499 499 assert guarded_eval("global_b", context) == "b"
500 500
501 501
502 502 @pytest.mark.parametrize(
503 503 "code",
504 504 ["def func(): pass", "class C: pass", "x = 1", "x += 1", "del x", "import ast"],
505 505 )
506 506 @pytest.mark.parametrize("context", [minimal(), limited(), unsafe()])
507 507 def test_rejects_side_effect_syntax(code, context):
508 508 with pytest.raises(SyntaxError):
509 509 guarded_eval(code, context)
510 510
511 511
512 512 def test_subscript():
513 513 context = EvaluationContext(
514 514 locals={}, globals={}, evaluation="limited", in_subscript=True
515 515 )
516 516 empty_slice = slice(None, None, None)
517 517 assert guarded_eval("", context) == tuple()
518 518 assert guarded_eval(":", context) == empty_slice
519 519 assert guarded_eval("1:2:3", context) == slice(1, 2, 3)
520 520 assert guarded_eval(':, "a"', context) == (empty_slice, "a")
521 521
522 522
523 523 def test_unbind_method():
524 524 class X(list):
525 525 def index(self, k):
526 526 return "CUSTOM"
527 527
528 528 x = X()
529 529 assert _unbind_method(x.index) is X.index
530 530 assert _unbind_method([].index) is list.index
531 531 assert _unbind_method(list.index) is None
532 532
533 533
534 534 def test_assumption_instance_attr_do_not_matter():
535 535 """This is semi-specified in Python documentation.
536 536
537 537 However, since the specification says 'not guaranted
538 538 to work' rather than 'is forbidden to work', future
539 539 versions could invalidate this assumptions. This test
540 540 is meant to catch such a change if it ever comes true.
541 541 """
542 542
543 543 class T:
544 544 def __getitem__(self, k):
545 545 return "a"
546 546
547 547 def __getattr__(self, k):
548 548 return "a"
549 549
550 550 def f(self):
551 551 return "b"
552 552
553 553 t = T()
554 554 t.__getitem__ = f
555 555 t.__getattr__ = f
556 556 assert t[1] == "a"
557 557 assert t[1] == "a"
558 558
559 559
560 560 def test_assumption_named_tuples_share_getitem():
561 561 """Check assumption on named tuples sharing __getitem__"""
562 562 from typing import NamedTuple
563 563
564 564 class A(NamedTuple):
565 565 pass
566 566
567 567 class B(NamedTuple):
568 568 pass
569 569
570 570 assert A.__getitem__ == B.__getitem__
571
572
573 @dec.skip_without("numpy")
574 def test_module_access():
575 import numpy
576
577 context = limited(numpy=numpy)
578 assert guarded_eval("numpy.linalg.norm", context) == numpy.linalg.norm
579
580 context = minimal(numpy=numpy)
581 with pytest.raises(GuardRejection):
582 guarded_eval("np.linalg.norm", context)
General Comments 0
You need to be logged in to leave comments. Login now