Skip to content

Commit b3ae449

Browse files
authored
Merge pull request #67 from KotlinIsland/intersection
Intersection
2 parents 2774ad6 + baad281 commit b3ae449

File tree

3 files changed

+106
-3
lines changed

3 files changed

+106
-3
lines changed

basedtyping/__init__.py

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,25 @@
2424

2525
if not TYPE_CHECKING:
2626
# TODO: remove the TYPE_CHECKING block once these are typed in basedtypeshed
27-
from typing import _tp_cache
27+
from typing import _GenericAlias, _remove_dups_flatten, _tp_cache, _type_check
2828

2929
if sys.version_info >= (3, 11):
3030
from typing import _collect_parameters
3131
else:
3232
from typing import _collect_type_vars as _collect_parameters
3333

34+
if not TYPE_CHECKING:
35+
36+
class _BasedSpecialForm(_SpecialForm, _root=True):
37+
def __repr__(self):
38+
return "basedtyping." + self._name
39+
40+
if sys.version_info < (3, 9):
41+
42+
def __getitem__(self, item):
43+
if self._name == "Intersection":
44+
return _IntersectionGenericAlias(self, item)
45+
3446

3547
if TYPE_CHECKING:
3648
Function = Callable[..., object] # type: ignore[no-any-explicit]
@@ -404,10 +416,84 @@ def Untyped(self: _SpecialForm, parameters: object) -> NoReturn:
404416
raise TypeError(f"{self} is not subscriptable")
405417

406418
else:
407-
Untyped: Final = _SpecialForm(
419+
Untyped: Final = _BasedSpecialForm(
408420
"Untyped",
409421
doc=(
410422
"Special type indicating that something isn't typed.\nThis is more"
411423
" specialized than ``Any`` and can help with gradually typing modules."
412424
),
413425
)
426+
427+
if not TYPE_CHECKING:
428+
429+
class _IntersectionGenericAlias(_GenericAlias, _root=True):
430+
def copy_with(self, params):
431+
return Intersection[params]
432+
433+
def __eq__(self, other):
434+
if not isinstance(other, _IntersectionGenericAlias):
435+
return NotImplemented
436+
return set(self.__args__) == set(other.__args__)
437+
438+
def __hash__(self):
439+
return hash(frozenset(self.__args__))
440+
441+
def __instancecheck__(self, obj):
442+
return self.__subclasscheck__(type(obj))
443+
444+
def __subclasscheck__(self, cls):
445+
for arg in self.__args__:
446+
if issubclass(cls, arg):
447+
return True
448+
449+
def __reduce__(self):
450+
func, (origin, args) = super().__reduce__()
451+
return func, (Intersection, args)
452+
453+
if sys.version_info > (3, 9):
454+
455+
@_BasedSpecialForm
456+
def Intersection(self, parameters):
457+
"""Intersection type; Intersection[X, Y] means both X and Y.
458+
459+
To define an intersection:
460+
- If using __future__.annotations, shortform can be used e.g. A & B
461+
- otherwise the fullform must be used e.g. Intersection[A, B].
462+
463+
Details:
464+
- The arguments must be types and there must be at least one.
465+
- None as an argument is a special case and is replaced by
466+
type(None).
467+
- Intersections of intersections are flattened, e.g.::
468+
469+
Intersection[Intersection[int, str], float] == Intersection[int, str, float]
470+
471+
- Intersections of a single argument vanish, e.g.::
472+
473+
Intersection[int] == int # The constructor actually returns int
474+
475+
- Redundant arguments are skipped, e.g.::
476+
477+
Intersection[int, str, int] == Intersection[int, str]
478+
479+
- When comparing intersections, the argument order is ignored, e.g.::
480+
481+
Intersection[int, str] == Intersection[str, int]
482+
483+
- You cannot subclass or instantiate an intersection.
484+
"""
485+
if parameters == ():
486+
raise TypeError("Cannot take an Intersection of no types.")
487+
if not isinstance(parameters, tuple):
488+
parameters = (parameters,)
489+
msg = "Intersection[arg, ...]: each arg must be a type."
490+
parameters = tuple(_type_check(p, msg) for p in parameters)
491+
parameters = _remove_dups_flatten(parameters)
492+
if len(parameters) == 1:
493+
return parameters[0]
494+
return _IntersectionGenericAlias(self, parameters)
495+
496+
else:
497+
Intersection = _BasedSpecialForm("Intersection", doc="")
498+
else:
499+
Intersection: _SpecialForm

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ authors = [
55
]
66
description = "Utilities for basedmypy"
77
name = "basedtyping"
8-
version = "0.0.2"
8+
version = "0.0.3"
99

1010
[tool.poetry.dependencies]
1111
python = "^3.7.2"

tests/test_intersection.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from basedtyping import Intersection
2+
3+
4+
class A:
5+
x: int
6+
7+
8+
class B:
9+
y: int
10+
11+
12+
def test_intersection() -> None:
13+
assert (
14+
str(Intersection[A, B])
15+
== f"basedtyping.Intersection[{A.__module__}.{A.__qualname__},"
16+
f" {B.__module__}.{B.__qualname__}]"
17+
)

0 commit comments

Comments
 (0)