Show More
@@ -1,3 +1,4 | |||
|
1 | from inspect import signature, Signature | |
|
1 | 2 | from typing import ( |
|
2 | 3 | Any, |
|
3 | 4 | Callable, |
@@ -335,6 +336,7 class _IdentitySubscript: | |||
|
335 | 336 | |
|
336 | 337 | IDENTITY_SUBSCRIPT = _IdentitySubscript() |
|
337 | 338 | SUBSCRIPT_MARKER = "__SUBSCRIPT_SENTINEL__" |
|
339 | UNKNOWN_SIGNATURE = Signature() | |
|
338 | 340 | |
|
339 | 341 | |
|
340 | 342 | class GuardRejection(Exception): |
@@ -415,6 +417,10 UNARY_OP_DUNDERS: Dict[Type[ast.unaryop], Tuple[str, ...]] = { | |||
|
415 | 417 | } |
|
416 | 418 | |
|
417 | 419 | |
|
420 | class Duck: | |
|
421 | """A dummy class used to create objects of other classes without calling their ``__init__``""" | |
|
422 | ||
|
423 | ||
|
418 | 424 | def _find_dunder(node_op, dunders) -> Union[Tuple[str, ...], None]: |
|
419 | 425 | dunder = None |
|
420 | 426 | for op, candidate_dunder in dunders.items(): |
@@ -584,6 +590,27 def eval_node(node: Union[ast.AST, None], context: EvaluationContext): | |||
|
584 | 590 | if policy.can_call(func) and not node.keywords: |
|
585 | 591 | args = [eval_node(arg, context) for arg in node.args] |
|
586 | 592 | return func(*args) |
|
593 | try: | |
|
594 | sig = signature(func) | |
|
595 | except ValueError: | |
|
596 | sig = UNKNOWN_SIGNATURE | |
|
597 | # if annotation was not stringized, or it was stringized | |
|
598 | # but resolved by signature call we know the return type | |
|
599 | not_empty = sig.return_annotation is not Signature.empty | |
|
600 | not_stringized = not isinstance(sig.return_annotation, str) | |
|
601 | if not_empty and not_stringized: | |
|
602 | duck = Duck() | |
|
603 | # if allow-listed builtin is on type annotation, instantiate it | |
|
604 | if policy.can_call(sig.return_annotation) and not node.keywords: | |
|
605 | args = [eval_node(arg, context) for arg in node.args] | |
|
606 | return sig.return_annotation(*args) | |
|
607 | try: | |
|
608 | # if custom class is in type annotation, mock it; | |
|
609 | # this only works for heap types, not builtins | |
|
610 | duck.__class__ = sig.return_annotation | |
|
611 | return duck | |
|
612 | except TypeError: | |
|
613 | pass | |
|
587 | 614 | raise GuardRejection( |
|
588 | 615 | "Call for", |
|
589 | 616 | func, # not joined to avoid calling `repr` |
@@ -253,16 +253,36 def test_method_descriptor(): | |||
|
253 | 253 | assert guarded_eval("list.copy.__name__", context) == "copy" |
|
254 | 254 | |
|
255 | 255 | |
|
256 | class HeapType: | |
|
257 | pass | |
|
258 | ||
|
259 | ||
|
260 | class CallCreatesHeapType: | |
|
261 | def __call__(self) -> HeapType: | |
|
262 | return HeapType() | |
|
263 | ||
|
264 | ||
|
265 | class CallCreatesBuiltin: | |
|
266 | def __call__(self) -> frozenset: | |
|
267 | return frozenset() | |
|
268 | ||
|
269 | ||
|
256 | 270 | @pytest.mark.parametrize( |
|
257 | "data,good,bad,expected", | |
|
271 | "data,good,bad,expected, equality", | |
|
258 | 272 | [ |
|
259 | [[1, 2, 3], "data.index(2)", "data.append(4)", 1], | |
|
260 | [{"a": 1}, "data.keys().isdisjoint({})", "data.update()", True], | |
|
273 | [[1, 2, 3], "data.index(2)", "data.append(4)", 1, True], | |
|
274 | [{"a": 1}, "data.keys().isdisjoint({})", "data.update()", True, True], | |
|
275 | [CallCreatesHeapType(), "data()", "data.__class__()", HeapType, False], | |
|
276 | [CallCreatesBuiltin(), "data()", "data.__class__()", frozenset, False], | |
|
261 | 277 | ], |
|
262 | 278 | ) |
|
263 | def test_evaluates_calls(data, good, bad, expected): | |
|
279 | def test_evaluates_calls(data, good, bad, expected, equality): | |
|
264 | 280 | context = limited(data=data) |
|
265 |
|
|
|
281 | value = guarded_eval(good, context) | |
|
282 | if equality: | |
|
283 | assert value == expected | |
|
284 | else: | |
|
285 | assert isinstance(value, expected) | |
|
266 | 286 | |
|
267 | 287 | with pytest.raises(GuardRejection): |
|
268 | 288 | guarded_eval(bad, context) |
@@ -534,7 +554,7 def test_unbind_method(): | |||
|
534 | 554 | def test_assumption_instance_attr_do_not_matter(): |
|
535 | 555 | """This is semi-specified in Python documentation. |
|
536 | 556 | |
|
537 | However, since the specification says 'not guaranted | |
|
557 | However, since the specification says 'not guaranteed | |
|
538 | 558 | to work' rather than 'is forbidden to work', future |
|
539 | 559 | versions could invalidate this assumptions. This test |
|
540 | 560 | is meant to catch such a change if it ever comes true. |
General Comments 0
You need to be logged in to leave comments.
Login now