Show More
@@ -2,14 +2,21 b' 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 | |
@@ -20,15 +27,8 b' 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 | |
@@ -557,7 +557,7 b' def eval_node(node: Union[ast.AST, None], context: EvaluationContext):' | |||||
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, |
|
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): | |
@@ -583,14 +583,12 b' def eval_node(node: Union[ast.AST, None], context: EvaluationContext):' | |||||
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, |
|
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( | |
@@ -601,9 +599,7 b' def eval_node(node: Union[ast.AST, None], context: EvaluationContext):' | |||||
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. | |
@@ -618,20 +614,27 b' def _eval_return_type(' | |||||
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, |
|
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 |
|
|
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, |
|
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: | |
@@ -647,7 +650,17 b' def _eval_node_name(node_id: str, policy: EvaluationPolicy, context: EvaluationC' | |||||
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. |
@@ -1,5 +1,5 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, | |
@@ -7,6 +7,10 b' from IPython.core.guarded_eval import (' | |||||
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 | |||
@@ -286,6 +290,35 b' class 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 | [ | |
@@ -300,6 +333,13 b' class StringAnnotation:' | |||||
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 | ], |
@@ -32,7 +32,7 b' dependencies = [' | |||||
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.1 |
|
35 | "typing_extensions; python_version<'3.12'", | |
36 | ] |
|
36 | ] | |
37 | dynamic = ["authors", "license", "version"] |
|
37 | dynamic = ["authors", "license", "version"] | |
38 |
|
38 |
General Comments 0
You need to be logged in to leave comments.
Login now