Skip to content

Commit 4a459f2

Browse files
authored
Merge pull request #95 from KotlinIsland/typeform
`TypeForm`
2 parents 03edccd + 3e31afa commit 4a459f2

20 files changed

+272
-152
lines changed

.idea/basedtyping.iml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/watcherTasks.xml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

basedtyping/__init__.py

Lines changed: 122 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22

33
from __future__ import annotations
44

5-
import contextlib
65
import sys
7-
from typing import (
6+
from typing import ( # type: ignore[attr-defined]
87
TYPE_CHECKING,
98
Any,
109
Callable,
@@ -17,33 +16,20 @@
1716
Type,
1817
TypeVar,
1918
Union,
19+
_GenericAlias,
20+
_remove_dups_flatten,
2021
_SpecialForm,
22+
_tp_cache,
23+
_type_check,
2124
cast,
2225
)
2326

2427
import typing_extensions
25-
from typing_extensions import Never, ParamSpec, TypeAlias, TypeGuard, TypeVarTuple
28+
from typing_extensions import Never, ParamSpec, Self, TypeAlias, TypeGuard, TypeVarTuple
2629

2730
from basedtyping.runtime_only import OldUnionType
2831

29-
if TYPE_CHECKING:
30-
from typing_extensions import override
31-
else:
32-
33-
def override(arg, /):
34-
# TODO: Remove when typing_extensions is >= 4.4
35-
with contextlib.suppress(AttributeError, TypeError):
36-
# Skip the attribute silently if it is not writable.
37-
# AttributeError happens if the object has __slots__ or a
38-
# read-only property, TypeError if it's a builtin class.
39-
arg.__override__ = True
40-
return arg
41-
42-
4332
if not TYPE_CHECKING:
44-
# TODO: remove the TYPE_CHECKING block once these are typed in basedtypeshed
45-
from typing import _GenericAlias, _remove_dups_flatten, _tp_cache, _type_check
46-
4733
if sys.version_info >= (3, 11):
4834
from typing import _collect_parameters
4935
else:
@@ -64,20 +50,47 @@ def override(arg, /):
6450
"issubform",
6551
"Untyped",
6652
"Intersection",
53+
"TypeForm",
6754
)
6855

69-
if not TYPE_CHECKING:
56+
if TYPE_CHECKING:
57+
_tp_cache_typed: Callable[[T], T]
58+
else:
59+
_tp_cache_typed = _tp_cache
60+
61+
62+
class _BasedSpecialForm(_SpecialForm, _root=True): # type: ignore[misc]
63+
_name: str
64+
65+
def __init_subclass__(cls, _root=False): # noqa: FBT002
66+
super().__init_subclass__(_root=_root) # type: ignore[call-arg]
67+
68+
def __init__(self, *args: object, **kwargs: object):
69+
self.alias = kwargs.pop("alias", _BasedGenericAlias)
70+
super().__init__(*args, **kwargs)
71+
72+
def __repr__(self) -> str:
73+
return "basedtyping." + self._name
74+
75+
def __and__(self, other: object) -> object:
76+
return Intersection[self, other]
7077

71-
class _BasedSpecialForm(_SpecialForm, _root=True):
72-
def __repr__(self):
73-
return "basedtyping." + self._name
78+
def __rand__(self, other: object) -> object:
79+
return Intersection[other, self]
7480

75-
if sys.version_info < (3, 9):
81+
if sys.version_info < (3, 9):
7682

77-
def __getitem__(self, item):
78-
if self._name == "Intersection":
79-
return _IntersectionGenericAlias(self, item)
80-
return None
83+
@_tp_cache_typed
84+
def __getitem__(self, item: object) -> object:
85+
return self.alias(self, item) # type: ignore[operator]
86+
87+
88+
class _BasedGenericAlias(_GenericAlias, _root=True):
89+
def __and__(self, other: object) -> object:
90+
return Intersection[self, other]
91+
92+
def __rand__(self, other: object) -> object:
93+
return Intersection[other, self]
8194

8295

8396
if TYPE_CHECKING:
@@ -202,7 +215,7 @@ def _raise_generics_not_reified(cls) -> NoReturn:
202215
f" to instantiate a reified class: {cls._orig_type_vars}"
203216
)
204217

205-
def _check_generics_reified(cls) -> None:
218+
def _check_generics_reified(cls):
206219
if not cls._generics_are_reified() or cls._has_non_reified_type_vars():
207220
cls._raise_generics_not_reified()
208221

@@ -221,7 +234,6 @@ def _is_subclass(cls, subclass: object) -> TypeGuard[_ReifiedGenericMetaclass]:
221234
cast(_ReifiedGenericMetaclass, subclass)._orig_class(),
222235
)
223236

224-
@override
225237
def __subclasscheck__(cls, subclass: object) -> bool:
226238
if not cls._is_subclass(subclass):
227239
return False
@@ -241,7 +253,6 @@ def __subclasscheck__(cls, subclass: object) -> bool:
241253
subclass._check_generics_reified()
242254
return cls._type_var_check(subclass.__reified_generics__)
243255

244-
@override
245256
def __instancecheck__(cls, instance: object) -> bool:
246257
if not cls._is_subclass(type(instance)):
247258
return False
@@ -252,7 +263,6 @@ def __instancecheck__(cls, instance: object) -> bool:
252263
)
253264

254265
# need the generic here for pyright. see https://github.com/microsoft/pyright/issues/5488
255-
@override
256266
def __call__(cls: type[T], *args: object, **kwargs: object) -> T:
257267
"""A placeholder ``__call__`` method that gets called when the class is
258268
instantiated directly, instead of first supplying the type parameters.
@@ -317,7 +327,7 @@ class ReifiedGeneric(Generic[T], metaclass=_ReifiedGenericMetaclass):
317327
__type_vars__: tuple[TypeVar, ...]
318328
"""``TypeVar``\\s that have not yet been reified. so this Tuple should always be empty by the time the ``ReifiedGeneric`` is instantiated"""
319329

320-
@_tp_cache # type: ignore[name-defined, misc]
330+
@_tp_cache # type: ignore[no-any-expr, misc]
321331
def __class_getitem__( # type: ignore[no-any-decorated]
322332
cls, item: GenericItems
323333
) -> type[ReifiedGeneric[T]]:
@@ -375,8 +385,7 @@ def __class_getitem__( # type: ignore[no-any-decorated]
375385
ReifiedGenericCopy._can_do_instance_and_subclass_checks_without_generics = False
376386
return ReifiedGenericCopy
377387

378-
@override
379-
def __init_subclass__(cls) -> None:
388+
def __init_subclass__(cls):
380389
cls._can_do_instance_and_subclass_checks_without_generics = True
381390
super().__init_subclass__()
382391

@@ -435,16 +444,17 @@ def issubform(form: _Forms, forminfo: _Forms) -> bool:
435444
Untyped: TypeAlias = Any # type: ignore[no-any-explicit]
436445
elif sys.version_info >= (3, 9):
437446

438-
@_SpecialForm # `_SpecialForm`s init isn't typed
439-
def Untyped(self: _SpecialForm, parameters: object) -> NoReturn: # noqa: ARG001
447+
@_BasedSpecialForm
448+
def Untyped(
449+
self: _BasedSpecialForm, parameters: object # noqa: ARG001
450+
) -> NoReturn:
440451
"""Special type indicating that something isn't typed.
441452
442453
This is more specialized than ``Any`` and can help with gradually typing modules.
443454
"""
444455
raise TypeError(f"{self} is not subscriptable")
445456

446457
else:
447-
# old version had the doc argument
448458
Untyped: Final = _BasedSpecialForm(
449459
"Untyped",
450460
doc=(
@@ -453,75 +463,96 @@ def Untyped(self: _SpecialForm, parameters: object) -> NoReturn: # noqa: ARG001
453463
),
454464
)
455465

456-
if not TYPE_CHECKING:
457466

458-
class _IntersectionGenericAlias(_GenericAlias, _root=True):
459-
def copy_with(self, args):
460-
return Intersection[args]
467+
class _IntersectionGenericAlias(_BasedGenericAlias, _root=True):
468+
def copy_with(self, args: object) -> Self: # type: ignore[override] # TODO: put in the overloads
469+
return cast(Self, Intersection[args])
461470

462-
def __eq__(self, other):
463-
if not isinstance(other, _IntersectionGenericAlias):
464-
return NotImplemented
465-
return set(self.__args__) == set(other.__args__)
471+
def __eq__(self, other: object) -> bool:
472+
if not isinstance(other, _IntersectionGenericAlias):
473+
return NotImplemented
474+
return set(self.__args__) == set(other.__args__)
466475

467-
def __hash__(self):
468-
return hash(frozenset(self.__args__))
476+
def __hash__(self) -> int:
477+
return hash(frozenset(self.__args__))
469478

470-
def __instancecheck__(self, obj):
471-
return self.__subclasscheck__(type(obj))
479+
def __instancecheck__(self, obj: object) -> bool:
480+
return self.__subclasscheck__(type(obj))
472481

473-
def __subclasscheck__(self, cls):
474-
return any(issubclass(cls, arg) for arg in self.__args__)
482+
def __subclasscheck__(self, cls: type[object]) -> bool:
483+
return all(issubclass(cls, arg) for arg in self.__args__)
475484

476-
def __reduce__(self):
477-
func, (_, args) = super().__reduce__()
478-
return func, (Intersection, args)
485+
def __reduce__(self) -> (object, object):
486+
func, (_, args) = super().__reduce__() # type: ignore[no-any-expr, misc]
487+
return func, (Intersection, args)
479488

480-
if sys.version_info > (3, 9):
481489

482-
@_BasedSpecialForm
483-
def Intersection(self, parameters):
484-
"""Intersection type; Intersection[X, Y] means both X and Y.
490+
if sys.version_info > (3, 9):
485491

486-
To define an intersection:
487-
- If using __future__.annotations, shortform can be used e.g. A & B
488-
- otherwise the fullform must be used e.g. Intersection[A, B].
492+
@_BasedSpecialForm
493+
def Intersection(self: _BasedSpecialForm, parameters: object) -> object:
494+
"""Intersection type; Intersection[X, Y] means both X and Y.
489495
490-
Details:
491-
- The arguments must be types and there must be at least one.
492-
- None as an argument is a special case and is replaced by
493-
type(None).
494-
- Intersections of intersections are flattened, e.g.::
496+
To define an intersection:
497+
- If using __future__.annotations, shortform can be used e.g. A & B
498+
- otherwise the fullform must be used e.g. Intersection[A, B].
495499
496-
Intersection[Intersection[int, str], float] == Intersection[int, str, float]
500+
Details:
501+
- The arguments must be types and there must be at least one.
502+
- None as an argument is a special case and is replaced by
503+
type(None).
504+
- Intersections of intersections are flattened, e.g.::
497505
498-
- Intersections of a single argument vanish, e.g.::
506+
Intersection[Intersection[int, str], float] == Intersection[int, str, float]
499507
500-
Intersection[int] == int # The constructor actually returns int
508+
- Intersections of a single argument vanish, e.g.::
501509
502-
- Redundant arguments are skipped, e.g.::
510+
Intersection[int] == int # The constructor actually returns int
503511
504-
Intersection[int, str, int] == Intersection[int, str]
512+
- Redundant arguments are skipped, e.g.::
505513
506-
- When comparing intersections, the argument order is ignored, e.g.::
514+
Intersection[int, str, int] == Intersection[int, str]
507515
508-
Intersection[int, str] == Intersection[str, int]
516+
- When comparing intersections, the argument order is ignored, e.g.::
509517
510-
- You cannot subclass or instantiate an intersection.
511-
"""
512-
if parameters == ():
513-
raise TypeError("Cannot take an Intersection of no types.")
514-
if not isinstance(parameters, tuple):
515-
parameters = (parameters,)
516-
msg = "Intersection[arg, ...]: each arg must be a type."
517-
parameters = tuple(_type_check(p, msg) for p in parameters)
518-
parameters = _remove_dups_flatten(parameters)
519-
if len(parameters) == 1:
520-
return parameters[0]
521-
return _IntersectionGenericAlias(self, parameters)
518+
Intersection[int, str] == Intersection[str, int]
519+
520+
- You cannot subclass or instantiate an intersection.
521+
"""
522+
if parameters == ():
523+
raise TypeError("Cannot take an Intersection of no types.")
524+
if not isinstance(parameters, tuple):
525+
parameters = (parameters,)
526+
msg = "Intersection[arg, ...]: each arg must be a type."
527+
parameters = tuple(_type_check(p, msg) for p in parameters) # type: ignore[no-any-expr]
528+
parameters = _remove_dups_flatten(parameters) # type: ignore[no-any-expr]
529+
if len(parameters) == 1: # type: ignore[no-any-expr]
530+
return parameters[0] # type: ignore[no-any-expr]
531+
return _IntersectionGenericAlias(self, parameters) # type: ignore[arg-type, no-any-expr]
522532

523-
else:
524-
# old version had the doc argument
525-
Intersection = _BasedSpecialForm("Intersection", doc="")
526533
else:
527-
Intersection: _SpecialForm
534+
Intersection = _BasedSpecialForm(
535+
"Intersection", doc="", alias=_IntersectionGenericAlias
536+
)
537+
538+
539+
class _TypeFormForm(_BasedSpecialForm, _root=True): # type: ignore[misc]
540+
def __init__(self, doc: str):
541+
self._name = "TypeForm"
542+
self._doc = self.__doc__ = doc
543+
544+
def __getitem__(self, parameters: object | tuple[object]) -> _BasedGenericAlias:
545+
if not isinstance(parameters, tuple):
546+
parameters = (parameters,)
547+
548+
return _BasedGenericAlias(self, parameters) # type: ignore[arg-type]
549+
550+
551+
TypeForm = _TypeFormForm(doc="""\
552+
A type that can be used to represent a ``builtins.type`` or a ``SpecialForm``.
553+
For example:
554+
555+
def f[T](t: TypeForm[T]) -> T: ...
556+
557+
reveal_type(f(int | str)) # int | str
558+
""")

basedtyping/typetime_only.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,5 @@ class assert_type(Generic[T]):
2525

2626
# TODO: make this use ReifiedGeneric so that it can check at runtime
2727
# None return type on __new__ is supported in pyright but not mypy
28-
def __new__(cls, _value: T) -> None: # type: ignore[misc]
28+
def __new__(cls, _value: T): # type: ignore[empty-body]
2929
pass

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ requires = ["poetry-core>=1.0.8"]
2323

2424
[tool.mypy]
2525
python_version = 3.8
26+
# we can't use override until we bump the minimum typing_extensions or something
27+
disable_error_code = ["explicit-override"]
2628

2729
[tool.black]
2830
target-version = ["py38"]

0 commit comments

Comments
 (0)