Show More
@@ -1,18 +1,23 b'' | |||||
1 | from inspect import isclass, signature, Signature |
|
1 | from inspect import isclass, signature, Signature | |
2 | from typing import ( |
|
2 | from typing import ( | |
|
3 | Annotated, | |||
|
4 | AnyStr, | |||
3 | Callable, |
|
5 | Callable, | |
4 | Dict, |
|
6 | Dict, | |
5 | Literal, |
|
7 | Literal, | |
6 | NamedTuple, |
|
8 | NamedTuple, | |
7 | NewType, |
|
9 | NewType, | |
|
10 | Optional, | |||
|
11 | Protocol, | |||
8 | Set, |
|
12 | Set, | |
9 | Sequence, |
|
13 | Sequence, | |
10 | Tuple, |
|
14 | Tuple, | |
11 | Type, |
|
15 | Type, | |
12 | Protocol, |
|
16 | TypeGuard, | |
13 | Union, |
|
17 | Union, | |
14 | get_args, |
|
18 | get_args, | |
15 | get_origin, |
|
19 | get_origin, | |
|
20 | is_typeddict, | |||
16 | ) |
|
21 | ) | |
17 | import ast |
|
22 | import ast | |
18 | import builtins |
|
23 | import builtins | |
@@ -27,9 +32,9 b' from IPython.utils.decorators import undoc' | |||||
27 |
|
32 | |||
28 |
|
33 | |||
29 | if sys.version_info < (3, 11): |
|
34 | if sys.version_info < (3, 11): | |
30 | from typing_extensions import Self |
|
35 | from typing_extensions import Self, LiteralString | |
31 | else: |
|
36 | else: | |
32 | from typing import Self |
|
37 | from typing import Self, LiteralString | |
33 |
|
38 | |||
34 | if sys.version_info < (3, 12): |
|
39 | if sys.version_info < (3, 12): | |
35 | from typing_extensions import TypeAliasType |
|
40 | from typing_extensions import TypeAliasType | |
@@ -423,9 +428,37 b' UNARY_OP_DUNDERS: Dict[Type[ast.unaryop], Tuple[str, ...]] = {' | |||||
423 | } |
|
428 | } | |
424 |
|
429 | |||
425 |
|
430 | |||
426 | class Duck: |
|
431 | class ImpersonatingDuck: | |
427 | """A dummy class used to create objects of other classes without calling their ``__init__``""" |
|
432 | """A dummy class used to create objects of other classes without calling their ``__init__``""" | |
428 |
|
433 | |||
|
434 | # no-op: override __class__ to impersonate | |||
|
435 | ||||
|
436 | ||||
|
437 | class _Duck: | |||
|
438 | """A dummy class used to create objects pretending to have given attributes""" | |||
|
439 | ||||
|
440 | def __init__(self, attributes: Optional[dict] = None, items: Optional[dict] = None): | |||
|
441 | self.attributes = attributes or {} | |||
|
442 | self.items = items or {} | |||
|
443 | ||||
|
444 | def __getattr__(self, attr: str): | |||
|
445 | return self.attributes[attr] | |||
|
446 | ||||
|
447 | def __hasattr__(self, attr: str): | |||
|
448 | return attr in self.attributes | |||
|
449 | ||||
|
450 | def __dir__(self): | |||
|
451 | return [*dir(super), *self.attributes] | |||
|
452 | ||||
|
453 | def __getitem__(self, key: str): | |||
|
454 | return self.items[key] | |||
|
455 | ||||
|
456 | def __hasitem__(self, key: str): | |||
|
457 | return self.items[key] | |||
|
458 | ||||
|
459 | def _ipython_key_completions_(self): | |||
|
460 | return self.items.keys() | |||
|
461 | ||||
429 |
|
462 | |||
430 | def _find_dunder(node_op, dunders) -> Union[Tuple[str, ...], None]: |
|
463 | def _find_dunder(node_op, dunders) -> Union[Tuple[str, ...], None]: | |
431 | dunder = None |
|
464 | dunder = None | |
@@ -617,26 +650,69 b' def _eval_return_type(func: Callable, node: ast.Call, context: EvaluationContext' | |||||
617 | # if annotation was not stringized, or it was stringized |
|
650 | # if annotation was not stringized, or it was stringized | |
618 | # but resolved by signature call we know the return type |
|
651 | # but resolved by signature call we know the return type | |
619 | not_empty = sig.return_annotation is not Signature.empty |
|
652 | not_empty = sig.return_annotation is not Signature.empty | |
620 | stringized = isinstance(sig.return_annotation, str) |
|
|||
621 | if not_empty: |
|
653 | if not_empty: | |
622 | return_type = ( |
|
654 | return _resolve_annotation(sig.return_annotation, sig, func, node, context) | |
623 | _eval_node_name(sig.return_annotation, context) |
|
655 | return NOT_EVALUATED | |
624 | if stringized |
|
656 | ||
625 | else sig.return_annotation |
|
657 | ||
|
658 | def _resolve_annotation( | |||
|
659 | annotation, | |||
|
660 | sig: Signature, | |||
|
661 | func: Callable, | |||
|
662 | node: ast.Call, | |||
|
663 | context: EvaluationContext, | |||
|
664 | ): | |||
|
665 | """Resolve annotation created by user with `typing` module and custom objects.""" | |||
|
666 | annotation = ( | |||
|
667 | _eval_node_name(annotation, context) | |||
|
668 | if isinstance(annotation, str) | |||
|
669 | else annotation | |||
626 |
|
|
670 | ) | |
627 | if return_type is Self and hasattr(func, "__self__"): |
|
671 | origin = get_origin(annotation) | |
|
672 | if annotation is Self and hasattr(func, "__self__"): | |||
628 |
|
|
673 | return func.__self__ | |
629 |
|
|
674 | elif origin is Literal: | |
630 |
|
|
675 | type_args = get_args(annotation) | |
631 |
|
|
676 | if len(type_args) == 1: | |
632 |
|
|
677 | return type_args[0] | |
633 | elif isinstance(return_type, NewType): |
|
678 | elif annotation is LiteralString: | |
634 | return _eval_or_create_duck(return_type.__supertype__, node, context) |
|
679 | return "" | |
635 | elif isinstance(return_type, TypeAliasType): |
|
680 | elif annotation is AnyStr: | |
636 | return _eval_or_create_duck(return_type.__value__, node, context) |
|
681 | index = None | |
|
682 | for i, (key, value) in enumerate(sig.parameters.items()): | |||
|
683 | if value.annotation is AnyStr: | |||
|
684 | index = i | |||
|
685 | break | |||
|
686 | if index is not None and index < len(node.args): | |||
|
687 | return eval_node(node.args[index], context) | |||
|
688 | elif origin is TypeGuard: | |||
|
689 | return bool() | |||
|
690 | elif origin is Union: | |||
|
691 | attributes = [ | |||
|
692 | attr | |||
|
693 | for type_arg in get_args(annotation) | |||
|
694 | for attr in dir(_resolve_annotation(type_arg, sig, func, node, context)) | |||
|
695 | ] | |||
|
696 | return _Duck(attributes=dict.fromkeys(attributes)) | |||
|
697 | elif is_typeddict(annotation): | |||
|
698 | return _Duck( | |||
|
699 | attributes=dict.fromkeys(dir(dict())), | |||
|
700 | items={ | |||
|
701 | k: _resolve_annotation(v, sig, func, node, context) | |||
|
702 | for k, v in annotation.__annotations__.items() | |||
|
703 | }, | |||
|
704 | ) | |||
|
705 | elif hasattr(annotation, "_is_protocol"): | |||
|
706 | return _Duck(attributes=dict.fromkeys(dir(annotation))) | |||
|
707 | elif origin is Annotated: | |||
|
708 | type_arg = get_args(annotation)[0] | |||
|
709 | return _resolve_annotation(type_arg, sig, func, node, context) | |||
|
710 | elif isinstance(annotation, NewType): | |||
|
711 | return _eval_or_create_duck(annotation.__supertype__, node, context) | |||
|
712 | elif isinstance(annotation, TypeAliasType): | |||
|
713 | return _eval_or_create_duck(annotation.__value__, node, context) | |||
637 |
|
|
714 | else: | |
638 |
|
|
715 | return _eval_or_create_duck(annotation, node, context) | |
639 | return NOT_EVALUATED |
|
|||
640 |
|
716 | |||
641 |
|
717 | |||
642 | def _eval_node_name(node_id: str, context: EvaluationContext): |
|
718 | def _eval_node_name(node_id: str, context: EvaluationContext): | |
@@ -671,7 +747,7 b' def _create_duck_for_heap_type(duck_type):' | |||||
671 |
|
747 | |||
672 | Returns the duck or NOT_EVALUATED sentinel if duck could not be created. |
|
748 | Returns the duck or NOT_EVALUATED sentinel if duck could not be created. | |
673 | """ |
|
749 | """ | |
674 | duck = Duck() |
|
750 | duck = ImpersonatingDuck() | |
675 | try: |
|
751 | try: | |
676 | # this only works for heap types, not builtins |
|
752 | # this only works for heap types, not builtins | |
677 | duck.__class__ = duck_type |
|
753 | duck.__class__ = duck_type |
@@ -1,6 +1,17 b'' | |||||
1 | import sys |
|
1 | import sys | |
2 | from contextlib import contextmanager |
|
2 | from contextlib import contextmanager | |
3 | from typing import NamedTuple, Literal, NewType |
|
3 | from typing import ( | |
|
4 | Annotated, | |||
|
5 | AnyStr, | |||
|
6 | NamedTuple, | |||
|
7 | Literal, | |||
|
8 | NewType, | |||
|
9 | Optional, | |||
|
10 | Protocol, | |||
|
11 | TypeGuard, | |||
|
12 | Union, | |||
|
13 | TypedDict, | |||
|
14 | ) | |||
4 | from functools import partial |
|
15 | from functools import partial | |
5 | from IPython.core.guarded_eval import ( |
|
16 | from IPython.core.guarded_eval import ( | |
6 | EvaluationContext, |
|
17 | EvaluationContext, | |
@@ -13,9 +24,9 b' import pytest' | |||||
13 |
|
24 | |||
14 |
|
25 | |||
15 | if sys.version_info < (3, 11): |
|
26 | if sys.version_info < (3, 11): | |
16 | from typing_extensions import Self |
|
27 | from typing_extensions import Self, LiteralString | |
17 | else: |
|
28 | else: | |
18 | from typing import Self |
|
29 | from typing import Self, LiteralString | |
19 |
|
30 | |||
20 | if sys.version_info < (3, 12): |
|
31 | if sys.version_info < (3, 12): | |
21 | from typing_extensions import TypeAliasType |
|
32 | from typing_extensions import TypeAliasType | |
@@ -304,6 +315,21 b' IntTypeAlias = TypeAliasType("IntTypeAlias", int)' | |||||
304 | HeapTypeAlias = TypeAliasType("HeapTypeAlias", HeapType) |
|
315 | HeapTypeAlias = TypeAliasType("HeapTypeAlias", HeapType) | |
305 |
|
316 | |||
306 |
|
317 | |||
|
318 | class TestProtocol(Protocol): | |||
|
319 | def test_method(self) -> bool: | |||
|
320 | pass | |||
|
321 | ||||
|
322 | ||||
|
323 | class TestProtocolImplementer(TestProtocol): | |||
|
324 | def test_method(self) -> bool: | |||
|
325 | return True | |||
|
326 | ||||
|
327 | ||||
|
328 | class Movie(TypedDict): | |||
|
329 | name: str | |||
|
330 | year: int | |||
|
331 | ||||
|
332 | ||||
307 | class SpecialTyping: |
|
333 | class SpecialTyping: | |
308 | def custom_int_type(self) -> CustomIntType: |
|
334 | def custom_int_type(self) -> CustomIntType: | |
309 | return CustomIntType(1) |
|
335 | return CustomIntType(1) | |
@@ -323,12 +349,40 b' class SpecialTyping:' | |||||
323 | def literal(self) -> Literal[False]: |
|
349 | def literal(self) -> Literal[False]: | |
324 | return False |
|
350 | return False | |
325 |
|
351 | |||
|
352 | def literal_string(self) -> LiteralString: | |||
|
353 | return "test" | |||
|
354 | ||||
326 | def self(self) -> Self: |
|
355 | def self(self) -> Self: | |
327 | return self |
|
356 | return self | |
328 |
|
357 | |||
|
358 | def any_str(self, x: AnyStr) -> AnyStr: | |||
|
359 | return x | |||
|
360 | ||||
|
361 | def annotated(self) -> Annotated[float, "positive number"]: | |||
|
362 | return 1 | |||
|
363 | ||||
|
364 | def annotated_self(self) -> Annotated[Self, "self with metadata"]: | |||
|
365 | self._metadata = "test" | |||
|
366 | return self | |||
|
367 | ||||
|
368 | def int_type_guard(self, x) -> TypeGuard[int]: | |||
|
369 | return isinstance(x, int) | |||
|
370 | ||||
|
371 | def optional_float(self) -> Optional[float]: | |||
|
372 | return 1.0 | |||
|
373 | ||||
|
374 | def union_str_and_int(self) -> Union[str, int]: | |||
|
375 | return "" | |||
|
376 | ||||
|
377 | def protocol(self) -> TestProtocol: | |||
|
378 | return TestProtocolImplementer() | |||
|
379 | ||||
|
380 | def typed_dict(self) -> Movie: | |||
|
381 | return {"name": "The Matrix", "year": 1999} | |||
|
382 | ||||
329 |
|
383 | |||
330 | @pytest.mark.parametrize( |
|
384 | @pytest.mark.parametrize( | |
331 |
"data, |
|
385 | "data,code,expected,equality", | |
332 | [ |
|
386 | [ | |
333 | [[1, 2, 3], "data.index(2)", 1, True], |
|
387 | [[1, 2, 3], "data.index(2)", 1, True], | |
334 | [{"a": 1}, "data.keys().isdisjoint({})", True, True], |
|
388 | [{"a": 1}, "data.keys().isdisjoint({})", True, True], | |
@@ -348,13 +402,19 b' class SpecialTyping:' | |||||
348 | [SpecialTyping(), "data.heap_type_alias()", HeapType, False], |
|
402 | [SpecialTyping(), "data.heap_type_alias()", HeapType, False], | |
349 | [SpecialTyping(), "data.self()", SpecialTyping, False], |
|
403 | [SpecialTyping(), "data.self()", SpecialTyping, False], | |
350 | [SpecialTyping(), "data.literal()", False, True], |
|
404 | [SpecialTyping(), "data.literal()", False, True], | |
|
405 | [SpecialTyping(), "data.literal_string()", str, False], | |||
|
406 | [SpecialTyping(), "data.any_str('a')", str, False], | |||
|
407 | [SpecialTyping(), "data.any_str(b'a')", bytes, False], | |||
|
408 | [SpecialTyping(), "data.annotated()", float, False], | |||
|
409 | [SpecialTyping(), "data.annotated_self()", SpecialTyping, False], | |||
|
410 | [SpecialTyping(), "data.int_type_guard()", int, False], | |||
351 | # test cases for static methods |
|
411 | # test cases for static methods | |
352 | [HasStaticMethod, "data.static_method()", HeapType, False], |
|
412 | [HasStaticMethod, "data.static_method()", HeapType, False], | |
353 | ], |
|
413 | ], | |
354 | ) |
|
414 | ) | |
355 |
def test_evaluates_calls(data, |
|
415 | def test_evaluates_calls(data, code, expected, equality): | |
356 | context = limited(data=data, HeapType=HeapType, StringAnnotation=StringAnnotation) |
|
416 | context = limited(data=data, HeapType=HeapType, StringAnnotation=StringAnnotation) | |
357 |
value = guarded_eval( |
|
417 | value = guarded_eval(code, context) | |
358 | if equality: |
|
418 | if equality: | |
359 | assert value == expected |
|
419 | assert value == expected | |
360 | else: |
|
420 | else: | |
@@ -362,6 +422,42 b' def test_evaluates_calls(data, good, expected, equality):' | |||||
362 |
|
422 | |||
363 |
|
423 | |||
364 | @pytest.mark.parametrize( |
|
424 | @pytest.mark.parametrize( | |
|
425 | "data,code,expected_attributes", | |||
|
426 | [ | |||
|
427 | [SpecialTyping(), "data.optional_float()", ["is_integer"]], | |||
|
428 | [ | |||
|
429 | SpecialTyping(), | |||
|
430 | "data.union_str_and_int()", | |||
|
431 | ["capitalize", "as_integer_ratio"], | |||
|
432 | ], | |||
|
433 | [SpecialTyping(), "data.protocol()", ["test_method"]], | |||
|
434 | [SpecialTyping(), "data.typed_dict()", ["keys", "values", "items"]], | |||
|
435 | ], | |||
|
436 | ) | |||
|
437 | def test_mocks_attributes_of_call_results(data, code, expected_attributes): | |||
|
438 | context = limited(data=data, HeapType=HeapType, StringAnnotation=StringAnnotation) | |||
|
439 | result = guarded_eval(code, context) | |||
|
440 | for attr in expected_attributes: | |||
|
441 | assert hasattr(result, attr) | |||
|
442 | assert attr in dir(result) | |||
|
443 | ||||
|
444 | ||||
|
445 | @pytest.mark.parametrize( | |||
|
446 | "data,code,expected_items", | |||
|
447 | [ | |||
|
448 | [SpecialTyping(), "data.typed_dict()", {"year": int, "name": str}], | |||
|
449 | ], | |||
|
450 | ) | |||
|
451 | def test_mocks_items_of_call_results(data, code, expected_items): | |||
|
452 | context = limited(data=data, HeapType=HeapType, StringAnnotation=StringAnnotation) | |||
|
453 | result = guarded_eval(code, context) | |||
|
454 | ipython_keys = result._ipython_key_completions_() | |||
|
455 | for key, value in expected_items.items(): | |||
|
456 | assert isinstance(result[key], value) | |||
|
457 | assert key in ipython_keys | |||
|
458 | ||||
|
459 | ||||
|
460 | @pytest.mark.parametrize( | |||
365 | "data,bad", |
|
461 | "data,bad", | |
366 | [ |
|
462 | [ | |
367 | [[1, 2, 3], "data.append(4)"], |
|
463 | [[1, 2, 3], "data.append(4)"], |
General Comments 0
You need to be logged in to leave comments.
Login now