Skip to content

Commit 805900d

Browse files
authored
Move tons of stuff to pre-pep.rst (#50)
1 parent 454a987 commit 805900d

File tree

2 files changed

+336
-192
lines changed

2 files changed

+336
-192
lines changed

pre-pep.rst

Lines changed: 302 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
PEP: 9999
2-
Title: Type Manipulation!
2+
Title: Type Manipulation
33
Author: Michael J. Sullivan <sully@msully.net>, Daniel W. Park <dnwpark@protonmail.com>, Yury Selivanov <yury@edgedb.com>
44
Sponsor: <name of sponsor>
55
PEP-Delegate: <PEP delegate's name>
66
Discussions-To: Pending
77
Status: Draft
88
Type: Standards Track
99
Topic: Typing
10-
Requires: 0000
1110
Created: <date created on, in dd-mmm-yyyy format>
1211
Python-Version: 3.15 or 3.16
1312
Post-History: Pending
@@ -585,6 +584,286 @@ to be imported, and it could also be used qualified as
585584
if <type-bool>
586585

587586

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+
588867
.. _rt-support:
589868

590869

@@ -708,15 +987,32 @@ worse. Supporting filtering while mapping would make it even more bad
708987

709988
We can explore other options too if needed.
710989

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+
7111008

7121009
Open Issues
7131010
===========
7141011

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``?
7171015

718-
* How to deal with situations where we are building new *nominal*
719-
types and might want to reference them?
7201016

7211017
[Any points that are still being decided/discussed.]
7221018

0 commit comments

Comments
 (0)