Show More
@@ -1,3 +1,4 b'' | |||||
|
1 | from inspect import signature, Signature | |||
1 | from typing import ( |
|
2 | from typing import ( | |
2 | Any, |
|
3 | Any, | |
3 | Callable, |
|
4 | Callable, | |
@@ -335,6 +336,7 b' class _IdentitySubscript:' | |||||
335 |
|
336 | |||
336 | IDENTITY_SUBSCRIPT = _IdentitySubscript() |
|
337 | IDENTITY_SUBSCRIPT = _IdentitySubscript() | |
337 | SUBSCRIPT_MARKER = "__SUBSCRIPT_SENTINEL__" |
|
338 | SUBSCRIPT_MARKER = "__SUBSCRIPT_SENTINEL__" | |
|
339 | UNKNOWN_SIGNATURE = Signature() | |||
338 |
|
340 | |||
339 |
|
341 | |||
340 | class GuardRejection(Exception): |
|
342 | class GuardRejection(Exception): | |
@@ -415,6 +417,10 b' 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 | def _find_dunder(node_op, dunders) -> Union[Tuple[str, ...], None]: |
|
424 | def _find_dunder(node_op, dunders) -> Union[Tuple[str, ...], None]: | |
419 | dunder = None |
|
425 | dunder = None | |
420 | for op, candidate_dunder in dunders.items(): |
|
426 | for op, candidate_dunder in dunders.items(): | |
@@ -584,6 +590,27 b' def eval_node(node: Union[ast.AST, None], context: EvaluationContext):' | |||||
584 | if policy.can_call(func) and not node.keywords: |
|
590 | if policy.can_call(func) and not node.keywords: | |
585 | args = [eval_node(arg, context) for arg in node.args] |
|
591 | args = [eval_node(arg, context) for arg in node.args] | |
586 | return func(*args) |
|
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 | raise GuardRejection( |
|
614 | raise GuardRejection( | |
588 | "Call for", |
|
615 | "Call for", | |
589 | func, # not joined to avoid calling `repr` |
|
616 | func, # not joined to avoid calling `repr` |
@@ -253,16 +253,36 b' def test_method_descriptor():' | |||||
253 | assert guarded_eval("list.copy.__name__", context) == "copy" |
|
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 | @pytest.mark.parametrize( |
|
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], |
|
273 | [[1, 2, 3], "data.index(2)", "data.append(4)", 1, True], | |
260 | [{"a": 1}, "data.keys().isdisjoint({})", "data.update()", 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 | context = limited(data=data) |
|
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 | with pytest.raises(GuardRejection): |
|
287 | with pytest.raises(GuardRejection): | |
268 | guarded_eval(bad, context) |
|
288 | guarded_eval(bad, context) | |
@@ -534,7 +554,7 b' def test_unbind_method():' | |||||
534 | def test_assumption_instance_attr_do_not_matter(): |
|
554 | def test_assumption_instance_attr_do_not_matter(): | |
535 | """This is semi-specified in Python documentation. |
|
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 | to work' rather than 'is forbidden to work', future |
|
558 | to work' rather than 'is forbidden to work', future | |
539 | versions could invalidate this assumptions. This test |
|
559 | versions could invalidate this assumptions. This test | |
540 | is meant to catch such a change if it ever comes true. |
|
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