|
1 | 1 | PEP: 9999 |
2 | | -Title: Type Manipulation! |
| 2 | +Title: Type Manipulation |
3 | 3 | Author: Michael J. Sullivan <sully@msully.net>, Daniel W. Park <dnwpark@protonmail.com>, Yury Selivanov <yury@edgedb.com> |
4 | 4 | Sponsor: <name of sponsor> |
5 | 5 | PEP-Delegate: <PEP delegate's name> |
6 | 6 | Discussions-To: Pending |
7 | 7 | Status: Draft |
8 | 8 | Type: Standards Track |
9 | 9 | Topic: Typing |
10 | | -Requires: 0000 |
11 | 10 | Created: <date created on, in dd-mmm-yyyy format> |
12 | 11 | Python-Version: 3.15 or 3.16 |
13 | 12 | Post-History: Pending |
@@ -585,6 +584,286 @@ to be imported, and it could also be used qualified as |
585 | 584 | if <type-bool> |
586 | 585 |
|
587 | 586 |
|
| 587 | +TODO: explain conditional types and iteration |
| 588 | + |
| 589 | + |
| 590 | +Type operators |
| 591 | +-------------- |
| 592 | + |
| 593 | +In some sections below we write things like ``Literal[int]]`` to mean |
| 594 | +"a literal that is of type ``int``". I don't think I'm really |
| 595 | +proposing to add that as a notion, but we could. |
| 596 | + |
| 597 | +Boolean types |
| 598 | +''''''''''''' |
| 599 | + |
| 600 | +* ``IsSub[T, S]``: What we would **want** is that it returns a boolean |
| 601 | + literal type indicating whether ``T`` is a subtype of ``S``. |
| 602 | + To support runtime checking, we probably need something weaker. |
| 603 | + |
| 604 | + |
| 605 | +Basic operators |
| 606 | +''''''''''''''' |
| 607 | + |
| 608 | +* ``GetArg[T, Base, Idx: Literal[int]]``: returns the type argument |
| 609 | + number ``Idx`` to ``T`` when interpreted as ``Base``, or ``Never`` |
| 610 | + if it cannot be. (That is, if we have ``class A(B[C]): ...``, then |
| 611 | + ``GetArg[A, B, 0] == C`` while ``GetArg[A, A, 0] == Never``). |
| 612 | + |
| 613 | + N.B: *Unfortunately* ``Base`` must be a proper class, *not* a |
| 614 | + protocol. So, for example, ``GetArg[Ty, Iterable, 0]]`` to get the |
| 615 | + type of something iterable *won't* work. This is because we can't do |
| 616 | + protocol checks at runtime in general. Special forms unfortunately |
| 617 | + require some special handling: the arguments list of a ``Callable`` |
| 618 | + will be packed in a tuple, and a ``...`` will become |
| 619 | + ``SpecialFormEllipsis``. |
| 620 | + |
| 621 | + |
| 622 | +* ``GetArgs[T, Base]``: returns a tuple containing all of the type |
| 623 | + arguments of ``T`` when interpreted as ``Base``, or ``Never`` if it |
| 624 | + cannot be. |
| 625 | + |
| 626 | + |
| 627 | +* ``GetAttr[T, S: Literal[str]]``: Extract the type of the member |
| 628 | + named ``S`` from the class ``T``. |
| 629 | + |
| 630 | +* ``Length[T: tuple]`` - get the length of a tuple as an int literal |
| 631 | + (or ``Literal[None]`` if it is unbounded) |
| 632 | + |
| 633 | + |
| 634 | +All of the operators in this section are "lifted" over union types. |
| 635 | + |
| 636 | +Union processing |
| 637 | +'''''''''''''''' |
| 638 | + |
| 639 | +* ``FromUnion[T]``: returns a tuple containing all of the union |
| 640 | + elements, or a 1-ary tuple containing T if it is not a union. |
| 641 | + |
| 642 | + |
| 643 | + |
| 644 | +Object inspection |
| 645 | +''''''''''''''''' |
| 646 | + |
| 647 | +* ``Members[T]``: produces a ``tuple`` of ``Member`` types describing |
| 648 | + the members (attributes and methods) of class ``T``. |
| 649 | + |
| 650 | + In order to allow typechecking time and runtime evaluation coincide |
| 651 | + more closely, **only members with explicit type annotations are included**. |
| 652 | + |
| 653 | +* ``Attrs[T]``: like ``Members[T]`` but only returns attributes (not |
| 654 | + methods). |
| 655 | + |
| 656 | +* ``Member[N: Literal[str], T, Q: MemberQuals, Init, D]``: ``Member``, |
| 657 | + is a simple type, not an operator, that is used to describe members |
| 658 | + of classes. Its type parameters encode the information about each |
| 659 | + member. |
| 660 | + |
| 661 | + * ``N`` is the name, as a literal string type |
| 662 | + * ``T`` is the type |
| 663 | + * ``Q`` is a union of qualifiers (see ``MemberQuals`` below) |
| 664 | + * ``Init`` is the literal type of the attribute initializer in the |
| 665 | + class (see :ref:`InitField <init-field>`) |
| 666 | + * ``D`` is the defining class of the member. (That is, which class |
| 667 | + the member is inherited from.) |
| 668 | + |
| 669 | +* ``MemberQuals = Literal['ClassVar', 'Final']`` - ``MemberQuals`` is |
| 670 | + the type of "qualifiers" that can apply to a member; currently |
| 671 | + ``ClassVar`` and ``Final`` |
| 672 | + |
| 673 | + |
| 674 | +Methods are returned as callables using the new ``Param`` based |
| 675 | +extended callables, and carrying the ``ClassVar`` |
| 676 | +qualifier. ``staticmethod`` and ``classmethod`` will return |
| 677 | +``staticmethod`` and ``classmethod`` types, which are subscriptable as |
| 678 | +of 3.14. |
| 679 | + |
| 680 | +TODO: What do we do about decorators in general, *at runtime*... This |
| 681 | +seems pretty cursed. We can probably sometimes evaluate them, if there |
| 682 | +are annotations at runtime, but in general that would require full |
| 683 | +subtype checking, which we can't do. |
| 684 | + |
| 685 | +We also have helpers for extracting the fields of ``Members``; they |
| 686 | +are all definable in terms of ``GetArg``. (Some of them are shared |
| 687 | +with ``Param``, discussed below.) |
| 688 | + |
| 689 | +* ``GetName[T: Member | Param]`` |
| 690 | +* ``GetType[T: Member | Param]`` |
| 691 | +* ``GetQuals[T: Member | Param]`` |
| 692 | +* ``GetInit[T: Member]`` |
| 693 | +* ``GetDefiner[T: Member]`` |
| 694 | + |
| 695 | + |
| 696 | + |
| 697 | +Object creation |
| 698 | +''''''''''''''' |
| 699 | + |
| 700 | +* ``NewProtocol[*Ps: Member]`` |
| 701 | + |
| 702 | +* ``NewProtocolWithBases[Bases, Ps: tuple[Member]]`` - A variant that |
| 703 | + allows specifying bases too. (UNIMPLEMENTED) - OR MAYBE SHOULD NOT EXIST |
| 704 | + |
| 705 | +* ``NewTypedDict[*Ps: Member]`` -- TODO: Needs fleshing out; will work |
| 706 | + similarly to ``NewProtocol`` but has different flags |
| 707 | + |
| 708 | + |
| 709 | + |
| 710 | +.. _init-field: |
| 711 | + |
| 712 | +InitField |
| 713 | +''''''''' |
| 714 | + |
| 715 | +We want to be able to support transforming types based on |
| 716 | +dataclasses/attrs/pydantic style field descriptors. In order to do |
| 717 | +that, we need to be able to consume things like calls to ``Field``. |
| 718 | + |
| 719 | +Our strategy for this is to introduce a new type |
| 720 | +``InitField[KwargDict]`` that collects arguments defined by a |
| 721 | +``KwargDict: TypedDict``:: |
| 722 | + |
| 723 | + class InitField[KwargDict: BaseTypedDict]: |
| 724 | + def __init__(self, **kwargs: typing.Unpack[KwargDict]) -> None: |
| 725 | + ... |
| 726 | + |
| 727 | + def _get_kwargs(self) -> KwargDict: |
| 728 | + ... |
| 729 | + |
| 730 | +When ``InitField`` or (more likely) a subtype of it is instantiated |
| 731 | +inside a class body, we infer a *more specific* type for it, based on |
| 732 | +``Literal`` types for all the arguments passed. |
| 733 | + |
| 734 | +So if we write:: |
| 735 | + |
| 736 | + class A: |
| 737 | + foo: int = InitField(default=0) |
| 738 | + |
| 739 | +then we would infer the type ``InitField[TypedDict('...', {'default': |
| 740 | +Literal[0]})]`` for the initializer, and that would be made available |
| 741 | +as the ``Init`` field of the ``Member``. |
| 742 | + |
| 743 | + |
| 744 | +Annotated |
| 745 | +''''''''' |
| 746 | + |
| 747 | +This could maybe be dropped? |
| 748 | + |
| 749 | +Libraries like FastAPI use annotations heavily, and we would like to |
| 750 | +be able to use annotations to drive type-level computation decision |
| 751 | +making. |
| 752 | + |
| 753 | +We understand that this may be controversial, as currently ``Annotated`` |
| 754 | +may be fully ignored by typecheckers. The operations proposed are: |
| 755 | + |
| 756 | +* ``GetAnnotations[T]`` - Fetch the annotations of a potentially |
| 757 | + Annotated type, as Literals. Examples:: |
| 758 | + |
| 759 | + GetAnnotations[Annotated[int, 'xxx']] = Literal['xxx'] |
| 760 | + GetAnnotations[Annotated[int, 'xxx', 5]] = Literal['xxx', 5] |
| 761 | + GetAnnotations[int] = Never |
| 762 | + |
| 763 | + |
| 764 | +* ``DropAnnotations[T]`` - Drop the annotations of a potentially |
| 765 | + Annotated type. Examples:: |
| 766 | + |
| 767 | + DropAnnotations[Annotated[int, 'xxx']] = int |
| 768 | + DropAnnotations[Annotated[int, 'xxx', 5]] = int |
| 769 | + DropAnnotations[int] = int |
| 770 | + |
| 771 | + |
| 772 | +Callable inspection and creation |
| 773 | +'''''''''''''''''''''''''''''''' |
| 774 | + |
| 775 | +``Callable`` types always have their arguments exposed in the extended |
| 776 | +Callable format discussed above. |
| 777 | + |
| 778 | +The names, type, and qualifiers share getter operations with |
| 779 | +``Member``. |
| 780 | + |
| 781 | +TODO: Should we make ``GetInit`` be literal types of default parameter |
| 782 | +values too? |
| 783 | + |
| 784 | +Generic Callable |
| 785 | +'''''''''''''''' |
| 786 | + |
| 787 | +Two possibilities for creating parameterized functions/types. They are kind of more syntax than functions exactly. I like the lambda one more. |
| 788 | + |
| 789 | +* ``GenericCallable[Vs, Ty]``: A generic callable. ``Vs`` are a tuple |
| 790 | + type of unbound type variables and ``Ty`` should be a ``Callable``, |
| 791 | + ``staticmethod``, or ``classmethod`` that has access to the |
| 792 | + variables in ``Vs`` |
| 793 | + |
| 794 | +This is kind of unsatisfying but we at least need some way to return |
| 795 | +existing generic methods and put them back into a new protocol. |
| 796 | + |
| 797 | + |
| 798 | +String manipulation |
| 799 | +''''''''''''''''''' |
| 800 | + |
| 801 | +String manipulation operations for string ``Literal`` types. |
| 802 | +We can put more in, but this is what typescript has. |
| 803 | +``Slice`` and ``Concat`` are a poor man's literal template. |
| 804 | +We can actually implement the case functions in terms of them and a |
| 805 | +bunch of conditionals, but shouldn't (especially if we want it to work |
| 806 | +for all unicode!). |
| 807 | + |
| 808 | + |
| 809 | +* ``Slice[S: Literal[str] | tuple, Start: Literal[int | None], End: Literal[int | None]]``: |
| 810 | + Slices a ``str`` or a tuple type. |
| 811 | + |
| 812 | +* ``Concat[S1: Literal[str], S2: Literal[str]]``: concatenate two strings |
| 813 | + |
| 814 | +* ``Uppercase[S: Literal[str]]``: uppercase a string literal |
| 815 | +* ``Lowercase[S: Literal[str]]``: lowercase a string literal |
| 816 | +* ``Capitalize[S: Literal[str]]``: capitalize a string literal |
| 817 | +* ``Uncapitalize[S: Literal[str]]``: uncapitalize a string literal |
| 818 | + |
| 819 | +All of the operators in this section are "lifted" over union types. |
| 820 | + |
| 821 | +Raise error |
| 822 | +''''''''''' |
| 823 | + |
| 824 | +* ``RaiseError[S: Literal[str]]``: If this type needs to be evaluated |
| 825 | + to determine some actual type, generate a type error with the |
| 826 | + provided message. |
| 827 | + |
| 828 | +Update class |
| 829 | +'''''''''''' |
| 830 | + |
| 831 | +TODO: This is kind of sketchy but it is I think needed for defining |
| 832 | +base classes and type decorators that do ``dataclass`` like things. |
| 833 | + |
| 834 | +* ``UpdateClass[*Ps: Member]``: A special form that *updates* an |
| 835 | + existing nominal class with new members (possibly overriding old |
| 836 | + ones, or removing them by making them have type ``Never``). |
| 837 | + |
| 838 | + This can only be used in the return type of a type decorator |
| 839 | + or as the return type of ``__init_subclass__``. |
| 840 | + |
| 841 | +One snag here: it introduces type-evaluation-order dependence; if the |
| 842 | +``UpdateClass`` return type for some ``__init_subclass__`` inspects |
| 843 | +some unrelated class's ``Members`` , and that class also has an |
| 844 | +``__init_subclass__``, then the results might depend on what order they |
| 845 | +are evaluated. |
| 846 | + |
| 847 | +This does actually exactly mirror a potential **runtime** |
| 848 | +evaluation-order dependence, though. |
| 849 | + |
| 850 | +.. _lifting: |
| 851 | + |
| 852 | +Lifting over Unions |
| 853 | +------------------- |
| 854 | + |
| 855 | +Many of the builtin operations are "lifted" over ``Union``. |
| 856 | + |
| 857 | +For example:: |
| 858 | + |
| 859 | + Concat[Literal['a'] | Literal['b'], Literal['c'] | Literal['d']] = ( |
| 860 | + Literal['ac'] | Literal['ad'] | Literal['bc'] | Literal['bd'] |
| 861 | + ) |
| 862 | + |
| 863 | + |
| 864 | +TODO: EXPLAIN |
| 865 | + |
| 866 | + |
588 | 867 | .. _rt-support: |
589 | 868 |
|
590 | 869 |
|
@@ -708,15 +987,32 @@ worse. Supporting filtering while mapping would make it even more bad |
708 | 987 |
|
709 | 988 | We can explore other options too if needed. |
710 | 989 |
|
| 990 | +Make the type-level operations more "strictly-typed" |
| 991 | +---------------------------------------------------- |
| 992 | + |
| 993 | +This proposal is less "strictly-typed" than typescript |
| 994 | +(strictly-kinded, maybe?). |
| 995 | + |
| 996 | +Typescript has better typechecking at the alias definition site: |
| 997 | +For ``P[K]``, ``K`` needs to have ``keyof P``... |
| 998 | + |
| 999 | +We could do potentially better but it would require more meachinery. |
| 1000 | + |
| 1001 | +* ``KeyOf[T]`` - literal keys of ``T`` |
| 1002 | +* ``Member[T]``, when statically checking a type alias, could be |
| 1003 | + treated as having some type like ``tuple[Member[KeyOf[T], object, |
| 1004 | + str, ..., ...], ...]`` |
| 1005 | +* ``GetAttr[T, S: KeyOf[T]]`` - but this isn't supported yet. TS supports it. |
| 1006 | +* We would also need to do context sensitive type bound inference |
| 1007 | + |
711 | 1008 |
|
712 | 1009 | Open Issues |
713 | 1010 | =========== |
714 | 1011 |
|
715 | | -* What is the best way to type base-class driven transformations using |
716 | | - ``__init_subclass__`` or (*shudder* metaclasses). |
| 1012 | +* Should we support building new nominal types?? |
| 1013 | + |
| 1014 | +* What invalid operations should be errors and what should return ``Never``? |
717 | 1015 |
|
718 | | -* How to deal with situations where we are building new *nominal* |
719 | | - types and might want to reference them? |
720 | 1016 |
|
721 | 1017 | [Any points that are still being decided/discussed.] |
722 | 1018 |
|
|
0 commit comments