##// END OF EJS Templates
Support for `Self`, custom types, type aliases...
krassowski -
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, 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):
@@ -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, 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(
@@ -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, 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:
@@ -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.10'",
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