From d75a8a513fd0782bdd4a6b8cde18fb07ea9d54f8 Mon Sep 17 00:00:00 2001 From: jorenham Date: Thu, 22 Feb 2024 09:13:11 +0100 Subject: [PATCH 01/26] export `optype.Slice` --- optype/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/optype/__init__.py b/optype/__init__.py index efdb956b..37fbcd1b 100644 --- a/optype/__init__.py +++ b/optype/__init__.py @@ -87,6 +87,7 @@ 'HasWeakCallableProxy', 'HasWeakProxy', 'HasWeakReference', + 'Slice', '__version__', ) @@ -168,6 +169,7 @@ CanRepr, CanStr, ) +from ._slice import Slice from ._specialattrs import ( HasAnnotations, HasDict, From 7f7ff9a74ecea7809172dbec790434b099f7bd58 Mon Sep 17 00:00:00 2001 From: jorenham Date: Thu, 22 Feb 2024 09:14:08 +0100 Subject: [PATCH 02/26] some initial documentation --- README.md | 218 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 212 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5967f318..92658ddd 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,214 @@ -[![license](https://img.shields.io/github/license/jorenham/optype?style=flat-square)](https://github.com/jorenham/optype/blob/master/LICENSE?) -[![PyPI](https://img.shields.io/pypi/v/optype?style=flat-square)](https://pypi.org/project/optype/) -[![versions](https://img.shields.io/pypi/pyversions/optype?style=flat-square)](https://github.com/jorenham/optype) -[![Checked with pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)](https://microsoft.github.io/pyright/) +

optype

-# optype +

+ Building blocks for precise & flexible type hints. +

-Typing Protocols for Precise Type Hints in Python 3.12+. +

+ + Continuous Integration + + + PyPI + + + Python Versions + + + License + + + Ruff + + + Checked with pyright + +

+ +--- + +> [!WARNING] +> The API is not stable; use at your own risk. + + +## Installation + +Optype is available as [`optype`](https://pypi.org/project/optype/) on PyPI: + +```shell +pip install optype +``` + +## Getting started + +*Coming soon* + + +## Reference + +### Elementary interfaces for the special methods + +Single-method `typing.Protocol` definitions for each of the "special methods", +also known as "magic"- or "dunder"- methods. See the [python documentation +](https://docs.python.org/3/reference/datamodel.html#special-method-names) for +details. + + +#### + +#### Comparisons + +Generally these methods return a `bool`. But in theory, anything can be +returned (even if it doesn't implement `__bool__`). + + +| Type | Signature | Expression | Expr. Reflected | +| -------------- | -------------------------- | ----------- | --------------- | +| `CanLt[X, Y]` | `__lt__(self, x: X) -> Y` | `self < x` | `x > self` | +| `CanLe[X, Y]` | `__le__(self, x: X) -> Y` | `self <= x` | `x >= self` | +| `CanGe[X, Y]` | `__ge__(self, x: X) -> Y` | `self >= x` | `x <= self` | +| `CanGt[X, Y]` | `__gt__(self, x: X) -> Y` | `self > x` | `x < self` | +| `CanEq[X, Y]` | `__eq__(self, x: X) -> Y` | `self == x` | `x == self` | +| `CanNe[X, Y]` | `__ne__(self, x: X) -> Y` | `self != x` | `x != self` | + + +#### Arithmetic and bitwise operators + +**Unary:** + +| Type | Signature | Expression | +| -------------- | ----------------------- | ------------------ | +| `CanPos[Y]` | `__pos__(self) -> Y` | `+self` | +| `CanNeg[Y]` | `__neg__(self) -> Y` | `-self` | +| `CanInvert[Y]` | `__invert__(self) -> Y` | `~self` | +| `CanAbs[Y]` | `__abs__(self) -> Y` | `abs(self)` | +| `CanRound0[Y]` | `__round__(self) -> Y` | `round(self)` | +| `CanTrunc[Y]` | `__trunc__(self) -> Y` | `math.trunc(self)` | +| `CanFloor[Y]` | `__floor__(self) -> Y` | `math.floor(self)` | +| `CanCeil[Y]` | `__ceil__(self) -> Y` | `math.ceil(self)` | + + +**Binary:** + +| Type | Signature | Expression | +| ------------------- | ------------------------------- | ----------------- | +| `CanAdd[X, Y]` | `__add__(self, x: X) -> Y` | `self + x` | +| `CanSub[X, Y]` | `__sub__(self, x: X) -> Y` | `self - x` | +| `CanMul[X, Y]` | `__mul__(self, x: X) -> Y` | `self * x` | +| `CanMatmul[X, Y]` | `__matmul__(self, x: X) -> Y` | `self @ x` | +| `CanTruediv[X, Y]` | `__truediv__(self, x: X) -> Y` | `self / x` | +| `CanFloordiv[X, Y]` | `__floordiv__(self, x: X) -> Y` | `self // x` | +| `CanMod[X, Y]` | `__mod__(self, x: X) -> Y` | `self % x` | +| `CanDivmod[X, Y]` | `__divmod__(self, x: X) -> Y` | `divmod(self, x)` | +| `CanPow[X, Y]` | `__pow__(self, x: X) -> Y` | `self ** x` | +| `CanLshift[X, Y]` | `__lshift__(self, x: X) -> Y` | `self << x` | +| `CanRshift[X, Y]` | `__rshift__(self, x: X) -> Y` | `self >> x` | +| `CanAnd[X, Y]` | `__and__(self, x: X) -> Y` | `self & x` | +| `CanXor[X, Y]` | `__xor__(self, x: X) -> Y` | `self ^ x` | +| `CanOr[X, Y]` | `__or__(self, x: X) -> Y` | `self \| x` | + + + + +**Binary (reflected):** + + +| Type | Signature | Expression | +| -------------------- | -------------------------------- | ----------------- | +| `CanRAdd[X, Y]` | `__radd__(self, x: X) -> Y` | `x + self` | +| `CanRSub[X, Y]` | `__rsub__(self, x: X) -> Y` | `x - self` | +| `CanRMul[X, Y]` | `__rmul__(self, x: X) -> Y` | `x * self` | +| `CanRMatmul[X, Y]` | `__rmatmul__(self, x: X) -> Y` | `x @ self` | +| `CanRTruediv[X, Y]` | `__rtruediv__(self, x: X) -> Y` | `x / self` | +| `CanRFloordiv[X, Y]` | `__rfloordiv__(self, x: X) -> Y` | `x // self` | +| `CanRMod[X, Y]` | `__rmod__(self, x: X) -> Y` | `x % self` | +| `CanRDivmod[X, Y]` | `__rdivmod__(self, x: X) -> Y` | `divmod(x, self)` | +| `CanRPow[X, Y]` | `__rpow__(self, x: X) -> Y` | `x ** self` | +| `CanRLshift[X, Y]` | `__rlshift__(self, x: X) -> Y` | `x << self` | +| `CanRRshift[X, Y]` | `__rrshift__(self, x: X) -> Y` | `x >> self` | +| `CanRAnd[X, Y]` | `__rand__(self, x: X) -> Y` | `x & self` | +| `CanRXor[X, Y]` | `__rxor__(self, x: X) -> Y` | `x ^ self` | +| `CanROr[X, Y]` | `__ror__(self, x: X) -> Y` | `x \| self` | + + +**Binary (augmented / in-place):** + +| Type | Signature | Expression | +| -------------------- | -------------------------------- | ------------ | +| `CanIAdd[X, Y]` | `__iadd__(self, x: X) -> Y` | `self += x` | +| `CanISub[X, Y]` | `__isub__(self, x: X) -> Y` | `self -= x` | +| `CanIMul[X, Y]` | `__imul__(self, x: X) -> Y` | `self *= x` | +| `CanIMatmul[X, Y]` | `__imatmul__(self, x: X) -> Y` | `self @= x` | +| `CanITruediv[X, Y]` | `__itruediv__(self, x: X) -> Y` | `self /= x` | +| `CanIFloordiv[X, Y]` | `__ifloordiv__(self, x: X) -> Y` | `self //= x` | +| `CanIMod[X, Y]` | `__imod__(self, x: X) -> Y` | `self %= x` | +| `CanIPow[X, Y]` | `__ipow__(self, x: X) -> Y` | `self **= x` | +| `CanILshift[X, Y]` | `__ilshift__(self, x: X) -> Y` | `self <<= x` | +| `CanIRshift[X, Y]` | `__irshift__(self, x: X) -> Y` | `self >>= x` | +| `CanIAnd[X, Y]` | `__iand__(self, x: X) -> Y` | `self &= x` | +| `CanIXor[X, Y]` | `__ixor__(self, x: X) -> Y` | `self ^= x` | +| `CanIOr[X, Y]` | `__ior__(self, x: X) -> Y` | `self \|= x` | + + +**Ternary** + + +... + +### Containers + + + + +### Iteration + +**Sync** + + +... + +**Async** + + +... + + + +### Generic interfaces for builtins + +#### `optype.Slice[A, B, S]` + +A generic interface of the builin +[`slice`](https://docs.python.org/3/library/functions.html#slice) object. + +**Signatures**: + +- `(B) -> Slice[None, B, None]` +- `(A, B) -> Slice[A, B, None]` +- `(A, B, S) -> Slice[A, B, S]` + +these are valid for the `slice(start?, stop, step?)` constructor, +and for the extended indexing syntax `_[start? : stop? : step?]` (the `?` +denotes an optional parameter). + +**Decorators**: +- `@typing.runtime_checkable` +- `@typing.final` From 29160497a185cb532c628949c74f978cd526b3fe Mon Sep 17 00:00:00 2001 From: jorenham Date: Thu, 22 Feb 2024 09:16:41 +0100 Subject: [PATCH 03/26] export `CanFormat` --- optype/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/optype/__init__.py b/optype/__init__.py index 37fbcd1b..3f17d158 100644 --- a/optype/__init__.py +++ b/optype/__init__.py @@ -16,6 +16,7 @@ 'CanFloat', 'CanFloor', 'CanFloordiv', + 'CanFormat', 'CanGe', 'CanGetitem', 'CanGt', @@ -111,6 +112,7 @@ CanXor, ) from ._binops_i import ( + CanFormat, CanIAdd, CanIAnd, CanIFloordiv, From 976dad5d49c42bcef30bb75e3766c910967d98b1 Mon Sep 17 00:00:00 2001 From: jorenham Date: Thu, 22 Feb 2024 23:07:07 +0100 Subject: [PATCH 04/26] prepare for v0.1.0 release --- pyproject.toml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 675c5f4d..052fbe2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "optype" -version = "0.0.0" +version = "0.1.0" description = "Typing Protocols for Precise Type Hints in Python 3.12+" authors = ["Joren Hammudoglu "] license = "BSD-3-Clause" @@ -39,7 +39,6 @@ skip = """\ """ - [tool.pyright] include = ["optype", "tests"] stubPath = "." @@ -163,10 +162,6 @@ select = [ "FURB", # refurb "RUF", # ruff ] -extend-safe-fixes = [ - # pyupgrade - "UP040", # non-pep695-type-alias -] extend-ignore = [ # flake8-annotations "ANN001", # missing-type-function-argument (deprecated) From 7081c433134937ed007e43f04f5c2b16a1320d1a Mon Sep 17 00:00:00 2001 From: jorenham Date: Fri, 23 Feb 2024 00:54:40 +0100 Subject: [PATCH 05/26] Fixed some naming issues --- optype/__init__.py | 12 ++++++------ optype/_containers.py | 4 ++-- optype/_nullops.py | 5 ++--- optype/_unops.py | 30 +++++++++++++++--------------- 4 files changed, 25 insertions(+), 26 deletions(-) diff --git a/optype/__init__.py b/optype/__init__.py index 3f17d158..e0383103 100644 --- a/optype/__init__.py +++ b/optype/__init__.py @@ -1,6 +1,6 @@ __all__ = ( - 'CanAIter', - 'CanANext', + 'CanAiter', + 'CanAnext', 'CanAbs', 'CanAdd', 'CanAnd', @@ -40,7 +40,7 @@ 'CanIter', 'CanLe', 'CanLen', - 'CanLenHint', + 'CanLengthHint', 'CanLshift', 'CanLt', 'CanMatmul', @@ -167,7 +167,7 @@ CanIndex, CanInt, CanLen, - CanLenHint, + CanLengthHint, CanRepr, CanStr, ) @@ -184,8 +184,8 @@ HasWeakReference, ) from ._unops import ( - CanAIter, - CanANext, + CanAiter, + CanAnext, CanAbs, CanCeil, CanDir, diff --git a/optype/_containers.py b/optype/_containers.py index 5349953a..acdf6d87 100644 --- a/optype/_containers.py +++ b/optype/_containers.py @@ -3,12 +3,12 @@ @_tp.runtime_checkable -class CanContains[X](_tp.Protocol): +class CanContains[K](_tp.Protocol): # vibrantly generic """ `other in self` """ - def __contains__(self, __other: X) -> bool: ... + def __contains__(self, __other: K) -> bool: ... @_tp.runtime_checkable class CanGetitem[K, V](_tp.Protocol): diff --git a/optype/_nullops.py b/optype/_nullops.py index d600ef35..3d5d71ab 100644 --- a/optype/_nullops.py +++ b/optype/_nullops.py @@ -5,7 +5,6 @@ `typing.Supports`. But the problem with those is, that they are also metaclasses, and that I (@jorenham) apparently am turing into a typing-purist. """ -import types as _ts import typing as _tp # type conversion @@ -79,13 +78,13 @@ class CanLen(_tp.Protocol): def __len__(self) -> int: ... @_tp.runtime_checkable -class CanLenHint(_tp.Protocol): +class CanLengthHint(_tp.Protocol): """ - approximation of `len(self)` - purely for optimization purposes - must be `>=0` or `NotImplemented` """ - def __len__(self) -> int | _ts.NotImplementedType: ... + def __length_hint__(self) -> int: ... # fingerprinting diff --git a/optype/_unops.py b/optype/_unops.py index cb3630b5..3de5a8e6 100644 --- a/optype/_unops.py +++ b/optype/_unops.py @@ -83,19 +83,11 @@ def __ceil__(self) -> Y: ... # iteration @_tp.runtime_checkable -class CanReversed[Y](_tp.Protocol): - """ - `reversed(self)` - """ - def __reversed__(self) -> Y: ... - - -@_tp.runtime_checkable -class CanNext[Y](_tp.Protocol): +class CanNext[V](_tp.Protocol): """ `next(self)` """ - def __next__(self) -> Y: ... + def __next__(self) -> V: ... @_tp.runtime_checkable @@ -106,18 +98,25 @@ class CanIter[Y: CanNext[_tp.Any]](_tp.Protocol): def __iter__(self) -> Y: ... +@_tp.runtime_checkable +class CanReversed[Y](_tp.Protocol): + """ + `reversed(self)` + """ + def __reversed__(self) -> Y: ... + # async iteration @_tp.runtime_checkable -class CanANext[Y](_tp.Protocol): +class CanAnext[V](_tp.Protocol): """ `anext(self)` """ - def __anext__(self) -> Y: ... + def __anext__(self) -> V: ... @_tp.runtime_checkable -class CanAIter[Y: CanANext[_tp.Any]](_tp.Protocol): +class CanAiter[Y: CanAnext[_tp.Any]](_tp.Protocol): """ `aiter(self)` """ @@ -127,5 +126,6 @@ def __aiter__(self) -> Y: ... # introspection @_tp.runtime_checkable -class CanDir[T: _abc.Iterable[_tp.Any]](_tp.Protocol): - def __dir__(self) -> T: ... +class CanDir[Vs: _abc.Iterable[_tp.Any]](_tp.Protocol): + # TODO: don't use collections.abc + def __dir__(self) -> Vs: ... From 5986b2bd374ff4a52ce65a8ccb678ad3b81600f5 Mon Sep 17 00:00:00 2001 From: jorenham Date: Fri, 23 Feb 2024 00:55:05 +0100 Subject: [PATCH 06/26] documented the remaining types --- README.md | 94 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 82 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 92658ddd..a50190fd 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ --- + + > [!WARNING] > The API is not stable; use at your own risk. @@ -57,13 +59,30 @@ Optype is available as [`optype`](https://pypi.org/project/optype/) on PyPI: pip install optype ``` + ## Getting started -*Coming soon* + +... ## Reference +All of the types here live in the root `optype` namespace. +They are runtime checkable, so that you can do e.g. +`isinstance('snail', optype.CanAdd)`, in case you want to check whether +`snail` implements `__add__`. + +> [!NOTE] +> It is bad practise to use a `typing.Protocol` as base class for your +> implementation. Because of `@typing.runtime_checkable`, you can use +> `isinstance` either way. + +Unlike e.g. `collections.abc`, the `optype` protocols aren't abstract. +This makes it easier to create sub-protocols, and provides a clearer +distinction between *interface* and *implementation*. + + ### Elementary interfaces for the special methods Single-method `typing.Protocol` definitions for each of the "special methods", @@ -71,10 +90,38 @@ also known as "magic"- or "dunder"- methods. See the [python documentation ](https://docs.python.org/3/reference/datamodel.html#special-method-names) for details. +#### Strict type conversion + +The return type of these special methods is *invariant*. Python will raise an +error if some other (sub)type is returned. +This is why these `optype` interfaces don't accept generic type arguments. + +**Builtin type constructors:** + +| Type | Signature | Expression | +| ------------ | ------------------------------ | ------------------ | +| `CanBool` | `__bool__(self) -> bool` | `bool(self)` | +| `CanInt` | `__int__(self) -> int` | `int(self)` | +| `CanFloat` | `__float__(self) -> float` | `float(self)` | +| `CanComplex` | `__complex__(self) -> complex` | `complex(self)` | +| `CanBytes` | `__bytes__(self) -> bytes` | `bytes(self)` | +| `CanStr` | `__str__(self) -> str` | `str(self)` | + +**Other builtin functions:** + +| Type | Signature | Expression | +| --------------- | ------------------------------ | ------------ | +| `CanRepr` | `__repr__(self) -> str` | `repr(self)` | +| `CanHash` | `__hash__(self) -> int` | `hash(self)` | +| `CanLen` | `__len__(self) -> int` | `len(self)` | +| `CanLengthHint` | `__length_hint__(self) -> int` | [docs](LH) | +| `CanIndex` | `__index__(self) -> int` | [docs](IX) | + -#### +[LH]: https://docs.python.org/3/reference/datamodel.html#object.__length_hint__ +[IX]: https://docs.python.org/3/reference/datamodel.html#object.__index__ -#### Comparisons +#### Comparisons operators Generally these methods return a `bool`. But in theory, anything can be returned (even if it doesn't implement `__bool__`). @@ -167,29 +214,43 @@ returned (even if it doesn't implement `__bool__`). | `CanIXor[X, Y]` | `__ixor__(self, x: X) -> Y` | `self ^= x` | | `CanIOr[X, Y]` | `__ior__(self, x: X) -> Y` | `self \|= x` | - -**Ternary** - -... + ### Containers - +| Type | Signature | Expression | +| ------------------ | -------------------------------------- | ------------- | +| `CanContains[K]` | `__contains__(self, k: K) -> bool` | `x in self` | +| `CanDelitem[K]` | `__delitem__(self, k: K) -> None` | `del self[k]` | +| `CanGetitem[K, V]` | `__getitem__(self, k: K) -> V` | `self[k]` | +| `CanMissing[K, V]` | `__missing__(self, k: K) -> V` | [docs](GM) | +| `CanSetitem[K, V]` | `__setitem__(self, k: K, v: V) -> None`| `self[k] = v` | +[GM]: https://docs.python.org/3/reference/datamodel.html#object.__missing__ + ### Iteration **Sync** - -... +| Type | Signature | Expression | +| -------------------------- | ------------------------- | ---------------- | +| `CanNext[V]` | `__next__(self) -> V` | `next(self)` | +| `CanIter[Y: CanNext[Any]]` | `__iter__(self) -> Y` | `iter(self)` | +| `CanReversed[Y]` (*) | `__reversed__(self) -> Y` | `reversed(self)` | + +(*) Although not strictly required, `Y@CanReversed` should be iterable. **Async** - -... +| Type | Signature | Expression | +| ---------------------------- | ----------------------- | ---------------- | +| `CanAnext[V]` (**) | `__anext__(self) -> V` | `anext(self)` | +| `CanAiter[Y: CanAnext[Any]]` | `__aiter__(self) -> Y` | `aiter(self)` | + +(**) Although not strictly required, `V@CanAnext` should be an `Awaitable`. ### Generic interfaces for builtins @@ -212,3 +273,12 @@ denotes an optional parameter). **Decorators**: - `@typing.runtime_checkable` - `@typing.final` + + +## Roadmap + +- Single-method protocols for descriptors +- Build a replacement for the `operator` standard library, with + runtime-accessible type annotations +- Protocols for numpy's dunder methods +- Backport to Python 3.11 and 3.10 From 145404603e72c2d0a6a1db4143ad66d4b8350771 Mon Sep 17 00:00:00 2001 From: jorenham Date: Fri, 23 Feb 2024 03:58:42 +0100 Subject: [PATCH 07/26] fix order of `__all__` --- optype/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/optype/__init__.py b/optype/__init__.py index e0383103..a26a385e 100644 --- a/optype/__init__.py +++ b/optype/__init__.py @@ -1,9 +1,9 @@ __all__ = ( - 'CanAiter', - 'CanAnext', 'CanAbs', 'CanAdd', + 'CanAiter', 'CanAnd', + 'CanAnext', 'CanBool', 'CanBytes', 'CanCeil', @@ -184,9 +184,9 @@ HasWeakReference, ) from ._unops import ( + CanAbs, CanAiter, CanAnext, - CanAbs, CanCeil, CanDir, CanFloor, From e6e87980d2c71f24520dd946427ccbfaf9b6c285 Mon Sep 17 00:00:00 2001 From: jorenham Date: Fri, 23 Feb 2024 03:59:24 +0100 Subject: [PATCH 08/26] de-inlined urls --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a50190fd..70f8a9ee 100644 --- a/README.md +++ b/README.md @@ -53,12 +53,13 @@ ## Installation -Optype is available as [`optype`](https://pypi.org/project/optype/) on PyPI: +Optype is available as [`optype`](OPTYPE) on PyPI: ```shell pip install optype ``` +[OPTYPE]: https://pypi.org/project/optype/ ## Getting started @@ -86,10 +87,11 @@ distinction between *interface* and *implementation*. ### Elementary interfaces for the special methods Single-method `typing.Protocol` definitions for each of the "special methods", -also known as "magic"- or "dunder"- methods. See the [python documentation -](https://docs.python.org/3/reference/datamodel.html#special-method-names) for +also known as "magic"- or "dunder"- methods. See the [Python docs](SM) for details. +[SM]: https://docs.python.org/3/reference/datamodel.html#special-method-names + #### Strict type conversion The return type of these special methods is *invariant*. Python will raise an From b522a244b9007c6c19560c54bcecd876c2105b5c Mon Sep 17 00:00:00 2001 From: jorenham Date: Fri, 23 Feb 2024 04:44:16 +0100 Subject: [PATCH 09/26] exclude some directories from pyright --- pyproject.toml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 052fbe2d..c680634f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,16 @@ skip = """\ [tool.pyright] include = ["optype", "tests"] +exclude = [ + "**/__pycache__", + ".venv", + ".git", + ".github", + ".pytest_cache", + ".ruff_cache", + ".vscode", + "dist", +] stubPath = "." venvPath = "." venv = ".venv" From 46ac0ea42f68e232887217cf772cccf872a85bce Mon Sep 17 00:00:00 2001 From: jorenham Date: Fri, 23 Feb 2024 08:02:11 +0100 Subject: [PATCH 10/26] Structured the protocols analogous to the python datamodel docs --- optype/__init__.py | 129 +++---- optype/_binops.py | 127 ------- optype/_binops_i.py | 115 ------- optype/_binops_r.py | 109 ------ optype/_can.py | 495 +++++++++++++++++++++++++++ optype/_cmpops.py | 51 --- optype/_containers.py | 39 --- optype/{_specialattrs.py => _has.py} | 63 ++-- optype/_nullops.py | 104 ------ optype/_slice.py | 7 +- optype/_unops.py | 131 ------- pyproject.toml | 8 +- 12 files changed, 604 insertions(+), 774 deletions(-) delete mode 100644 optype/_binops.py delete mode 100644 optype/_binops_i.py delete mode 100644 optype/_binops_r.py create mode 100644 optype/_can.py delete mode 100644 optype/_cmpops.py delete mode 100644 optype/_containers.py rename optype/{_specialattrs.py => _has.py} (57%) delete mode 100644 optype/_nullops.py delete mode 100644 optype/_unops.py diff --git a/optype/__init__.py b/optype/__init__.py index a26a385e..86da3889 100644 --- a/optype/__init__.py +++ b/optype/__init__.py @@ -9,6 +9,8 @@ 'CanCeil', 'CanComplex', 'CanContains', + 'CanDel', + 'CanDelattr', 'CanDelitem', 'CanDir', 'CanDivmod', @@ -18,6 +20,8 @@ 'CanFloordiv', 'CanFormat', 'CanGe', + 'CanGetattr', + 'CanGetattribute', 'CanGetitem', 'CanGt', 'CanHash', @@ -53,7 +57,8 @@ 'CanOr', 'CanPos', 'CanPow', - 'CanPow0', + 'CanPow2', + 'CanPow3', 'CanRAdd', 'CanRAnd', 'CanRDivmod', @@ -71,8 +76,10 @@ 'CanRepr', 'CanReversed', 'CanRound', - 'CanRound0', + 'CanRound1', + 'CanRound2', 'CanRshift', + 'CanSetattr', 'CanSetitem', 'CanStr', 'CanSub', @@ -94,25 +101,33 @@ from importlib import metadata as _metadata -from ._binops import ( +from ._can import ( + CanAbs, CanAdd, + CanAiter, CanAnd, + CanAnext, + CanBool, + CanBytes, + CanCeil, + CanComplex, + CanContains, + CanDel, + CanDelattr, + CanDelitem, + CanDir, CanDivmod, + CanEq, + CanFloat, + CanFloor, CanFloordiv, - CanLshift, - CanMatmul, - CanMod, - CanMul, - CanOr, - CanPow, - CanPow0, - CanRshift, - CanSub, - CanTruediv, - CanXor, -) -from ._binops_i import ( CanFormat, + CanGe, + CanGetattr, + CanGetattribute, + CanGetitem, + CanGt, + CanHash, CanIAdd, CanIAnd, CanIFloordiv, @@ -126,8 +141,27 @@ CanISub, CanITruediv, CanIXor, -) -from ._binops_r import ( + CanIndex, + CanInt, + CanInvert, + CanIter, + CanLe, + CanLen, + CanLengthHint, + CanLshift, + CanLt, + CanMatmul, + CanMissing, + CanMod, + CanMul, + CanNe, + CanNeg, + CanNext, + CanOr, + CanPos, + CanPow, + CanPow2, + CanPow3, CanRAdd, CanRAnd, CanRDivmod, @@ -142,37 +176,21 @@ CanRSub, CanRTruediv, CanRXor, -) -from ._cmpops import ( - CanEq, - CanGe, - CanGt, - CanLe, - CanLt, - CanNe, -) -from ._containers import ( - CanContains, - CanDelitem, - CanGetitem, - CanMissing, - CanSetitem, -) -from ._nullops import ( - CanBool, - CanBytes, - CanComplex, - CanFloat, - CanHash, - CanIndex, - CanInt, - CanLen, - CanLengthHint, CanRepr, + CanReversed, + CanRound, + CanRound1, + CanRound2, + CanRshift, + CanSetattr, + CanSetitem, CanStr, + CanSub, + CanTruediv, + CanTrunc, + CanXor, ) -from ._slice import Slice -from ._specialattrs import ( +from ._has import ( HasAnnotations, HasDict, HasDoc, @@ -183,22 +201,7 @@ HasWeakProxy, HasWeakReference, ) -from ._unops import ( - CanAbs, - CanAiter, - CanAnext, - CanCeil, - CanDir, - CanFloor, - CanInvert, - CanIter, - CanNeg, - CanNext, - CanPos, - CanReversed, - CanRound, - CanRound0, - CanTrunc, -) +from ._slice import Slice + __version__: str = _metadata.version(__package__ or __file__.split('/')[-1]) diff --git a/optype/_binops.py b/optype/_binops.py deleted file mode 100644 index 9e69491d..00000000 --- a/optype/_binops.py +++ /dev/null @@ -1,127 +0,0 @@ -"""Interfaces for individual binary operations.""" -import typing as _tp - -# arithmetic operations - - -@_tp.runtime_checkable -class CanAdd[X, Y](_tp.Protocol): - """ - `self + other` - """ - def __add__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanSub[X, Y](_tp.Protocol): - """ - `self - other` - """ - def __sub__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanMul[X, Y](_tp.Protocol): - """ - `self * other` - """ - def __mul__(self, __other: X) -> Y: ... - -@_tp.runtime_checkable -class CanMatmul[X, Y](_tp.Protocol): - """ - `self @ other` - """ - def __matmul__(self, __other: X) -> Y: ... - -@_tp.runtime_checkable -class CanTruediv[X, Y](_tp.Protocol): - """ - `self / other` - """ - def __truediv__(self, __other: X) -> Y: ... - -@_tp.runtime_checkable -class CanFloordiv[X, Y](_tp.Protocol): - """ - `self // other` - """ - def __floordiv__(self, __other: X) -> Y: ... - -@_tp.runtime_checkable -class CanMod[X, Y](_tp.Protocol): - """ - `self % other` - """ - def __mod__(self, __other: X) -> Y: ... - -@_tp.runtime_checkable -class CanDivmod[X, Y](_tp.Protocol): - """ - `divmod(self, other)` - """ - def __pow__(self, __other: X) -> Y: ... - -@_tp.runtime_checkable -class CanPow0[X, Y](_tp.Protocol): - """ - - `self ** other` or - - `pow(self, other)` - """ - def __pow__(self, __other: X) -> Y: ... - -@_tp.runtime_checkable -class CanPow[X, M, Y, YM](_tp.Protocol): - """ - Implements - - - `self ** other` and - - `pow(self, other[, modulo])`, - - via `__pow__` with overloaded signatures - - - `(Self, X) -> Y` and - - `(Self, X, M) -> YM`. - - Note that there is no `__rpow__` with modulo (the official docs are wrong). - """ - @_tp.overload - def __pow__(self, __other: X) -> Y: ... - @_tp.overload - def __pow__(self, __other: X, __modulo: M) -> YM: ... - - -# bitwise operations - -@_tp.runtime_checkable -class CanLshift[X, Y](_tp.Protocol): - """ - `self << other` - """ - def __lshift__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanRshift[X, Y](_tp.Protocol): - """ - `self >> other` - """ - def __rshift__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanAnd[X, Y](_tp.Protocol): - """ - `self & other` - """ - def __and__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanXor[X, Y](_tp.Protocol): - """ - `self ^ other` - """ - def __xor__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanOr[X, Y](_tp.Protocol): - """ - `self | other` - """ - def __or__(self, __other: X, /) -> Y: ... diff --git a/optype/_binops_i.py b/optype/_binops_i.py deleted file mode 100644 index 9f47ae49..00000000 --- a/optype/_binops_i.py +++ /dev/null @@ -1,115 +0,0 @@ -# ruff: noqa: PYI034 -"""Interfaces for the augmented / in-place binary operation variants.""" -import typing as _tp - -# arithmetic operations - - -@_tp.runtime_checkable -class CanIAdd[X, Y](_tp.Protocol): - """ - `_ += other` - """ - def __iadd__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanISub[X, Y](_tp.Protocol): - """ - `_ -= other` - """ - def __isub__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanIMul[X, Y](_tp.Protocol): - """ - `_ *= other` - """ - def __imul__(self, __other: X) -> Y: ... - -@_tp.runtime_checkable -class CanIMatmul[X, Y](_tp.Protocol): - """ - `_ @= other` - """ - def __imatmul__(self, __other: X) -> Y: ... - -@_tp.runtime_checkable -class CanITruediv[X, Y](_tp.Protocol): - """ - `_ /= other` - """ - def __itruediv__(self, __other: X) -> Y: ... - -@_tp.runtime_checkable -class CanIFloordiv[X, Y](_tp.Protocol): - """ - `_ //= other` - """ - def __ifloordiv__(self, __other: X) -> Y: ... - -@_tp.runtime_checkable -class CanIMod[X, Y](_tp.Protocol): - """ - `_ %= other` - """ - def __imod__(self, __other: X) -> Y: ... - -# no __idivmod__ - -@_tp.runtime_checkable -class CanIPow[X, Y](_tp.Protocol): - """ - `_ **= other` - """ - def __ipow__(self, __other: X) -> Y: ... - - -# bitwise operations (augmented) - -@_tp.runtime_checkable -class CanILshift[X, Y](_tp.Protocol): - """ - `_ <<= other` - """ - def __ilshift__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanIRshift[X, Y](_tp.Protocol): - """ - `_ >>= other` - """ - def __irshift__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanIAnd[X, Y](_tp.Protocol): - """ - `_ &= other` - """ - def __iand__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanIXor[X, Y](_tp.Protocol): - """ - `_ ^= other` - """ - def __ixor__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanIOr[X, Y](_tp.Protocol): - """ - `_ |= other` - """ - def __ior__(self, __other: X, /) -> Y: ... - - -# formatting - -@_tp.runtime_checkable -class CanFormat[X: str, Y: str](_tp.Protocol): - """ - `format(self[, format_spec])` - - note that both the `format_spec` and the returned value can be subclasses - of `str`. - """ - def __format__(self, __format_spec: X) -> Y: ... # type: ignore[override] diff --git a/optype/_binops_r.py b/optype/_binops_r.py deleted file mode 100644 index 044fa230..00000000 --- a/optype/_binops_r.py +++ /dev/null @@ -1,109 +0,0 @@ -"""Interfaces for the reflected variants of thebinary operation.""" -import typing as _tp - -# arithmetic operations - - -@_tp.runtime_checkable -class CanRAdd[X, Y](_tp.Protocol): - """ - `other + self` - """ - def __radd__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanRSub[X, Y](_tp.Protocol): - """ - `other - self` - """ - def __rsub__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanRMul[X, Y](_tp.Protocol): - """ - `other * self` - """ - def __rmul__(self, __other: X) -> Y: ... - -@_tp.runtime_checkable -class CanRMatmul[X, Y](_tp.Protocol): - """ - `other @ self` - """ - def __rmatmul__(self, __other: X) -> Y: ... - -@_tp.runtime_checkable -class CanRTruediv[X, Y](_tp.Protocol): - """ - `other / self` - """ - def __rtruediv__(self, __other: X) -> Y: ... - -@_tp.runtime_checkable -class CanRFloordiv[X, Y](_tp.Protocol): - """ - `other // self` - """ - def __rfloordiv__(self, __other: X) -> Y: ... - -@_tp.runtime_checkable -class CanRMod[X, Y](_tp.Protocol): - """ - `other % self` - """ - def __rmod__(self, __other: X) -> Y: ... - -@_tp.runtime_checkable -class CanRDivmod[X, Y](_tp.Protocol): - """ - `divmod(other, self)` - """ - def __rdivmod__(self, __other: X) -> Y: ... - -@_tp.runtime_checkable -class CanRPow[X, Y](_tp.Protocol): - """ - `other ** self` or - `pow(other, self)` - - note that `pow(a, b, modulo)` will not be reflected - """ - def __rpow__(self, __other: X) -> Y: ... - - -# bitwise operations - -@_tp.runtime_checkable -class CanRLshift[X, Y](_tp.Protocol): - """ - `other << self` - """ - def __rlshift__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanRRshift[X, Y](_tp.Protocol): - """ - `other >> self` - """ - def __rrshift__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanRAnd[X, Y](_tp.Protocol): - """ - `other & self ` - """ - def __rand__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanRXor[X, Y](_tp.Protocol): - """ - `other ^ self` - """ - def __rxor__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanROr[X, Y](_tp.Protocol): - """ - `other | self` - """ - def __ror__(self, __other: X, /) -> Y: ... diff --git a/optype/_can.py b/optype/_can.py new file mode 100644 index 00000000..d96d5eb5 --- /dev/null +++ b/optype/_can.py @@ -0,0 +1,495 @@ +# ruff: noqa: PYI034 +from typing import Any, Protocol, overload, override, runtime_checkable + + +# Iterator types +# https://docs.python.org/3/library/stdtypes.html#iterator-types + +@runtime_checkable +class CanNext[V](Protocol): + def __next__(self) -> V: ... + +@runtime_checkable +class CanIter[Y: CanNext[Any]](Protocol): + def __iter__(self) -> Y: ... + + +# 3.3.1. Basic customization +# https://docs.python.org/3/reference/datamodel.html#basic-customization + +# TODO: __new__ +# TODO: __init__ + +@runtime_checkable +class CanDel[Y: str](Protocol): + def __del__(self) -> Any: ... + +@runtime_checkable +class CanRepr[Y: str](Protocol): + @override + def __repr__(self) -> Y: ... + +@runtime_checkable +class CanStr[Y: str](Protocol): + """By default, each `object` has a `__str__` method.""" + @override + def __str__(self) -> Y: ... + +@runtime_checkable +class CanBytes[Y: bytes](Protocol): + def __bytes__(self) -> Y: ... + +@runtime_checkable +class CanFormat[X: str, Y: str](Protocol): + # typeshed's object.__format__ stub is unnecessarily restrictive + def __format__(self, __x: X) -> Y: ... # type: ignore[override] + +@runtime_checkable +class CanLt[X, Y](Protocol): + def __lt__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanLe[X, Y](Protocol): + def __le__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanEq[X, Y](Protocol): + def __eq__(self, __x: X, /) -> Y: ... # type: ignore[override] + +@runtime_checkable +class CanNe[X, Y](Protocol): + def __ne__(self, __x: X, /) -> Y: ... # type: ignore[override] + +@runtime_checkable +class CanGt[X, Y](Protocol): + def __gt__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanGe[X, Y](Protocol): + def __ge__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanHash(Protocol): + @override + def __hash__(self) -> int: ... + +@runtime_checkable +class CanBool(Protocol): + def __bool__(self) -> bool: ... + + +# 3.3.2. Customizing attribute access +# https://docs.python.org/3/reference/datamodel.html#customizing-attribute-access + +@runtime_checkable +class CanGetattr[K: str, V](Protocol): + def __getattr__(self, __k: K) -> V: ... + +@runtime_checkable +class CanGetattribute[K: str, V](Protocol): + """Note that `isinstance(x, CanGetattribute)` is always true.""" + @override + def __getattribute__(self, __k: K) -> V: ... # type: ignore[override] + +@runtime_checkable +class CanSetattr[K: str, V](Protocol): + """Note that `isinstance(x, CanSetattr)` is always true.""" + @override + def __setattr__(self, __k: K, __v: V) -> Any: ... # type: ignore[override] + +@runtime_checkable +class CanDelattr[K: str](Protocol): + @override + def __delattr__(self, __k: K) -> Any: ... # type: ignore[override] + +@runtime_checkable +class CanDir[Vs: CanIter[Any]](Protocol): + @override + def __dir__(self) -> Vs: ... + + +# 3.3.2.2. Implementing Descriptors +# https://docs.python.org/3/reference/datamodel.html#implementing-descriptors + +# TODO: CanGet +# TODO: CanSet +# TODO: CanDelete +# TODO: HasObjclass + +# 3.3.2.4. `__slots__` +# https://docs.python.org/3/reference/datamodel.html#slots + +# TODO HasSlots + + +# 3.3.3. Customizing class creation +# https://docs.python.org/3/reference/datamodel.html#customizing-class-creation + +# TODO: CanInitSubclass +# TODO: CanSetName + +# 3.3.3.2. Resolving MRO entries +# https://docs.python.org/3/reference/datamodel.html#resolving-mro-entries + +# TODO: CanMroEntries + +# 3.3.3.4. Preparing the class namespace +# https://docs.python.org/3/reference/datamodel.html#preparing-the-class-namespace + +# TODO: CanPrepare + +# 3.3.3.6. Creating the class object +# https://docs.python.org/3/reference/datamodel.html#creating-the-class-object + +# TODO: HasClass + + +# 3.3.4. Customizing instance and subclass checks +# https://docs.python.org/3/reference/datamodel.html#customizing-instance-and-subclass-checks + +# TODO: CanInstancecheck +# TODO: CanSubclasscheck + + +# 3.3.5. Emulating generic types +# https://docs.python.org/3/reference/datamodel.html#emulating-generic-types + +# TODO: CanClassGetItem + + +# 3.3.6. Emulating callable objects +# https://docs.python.org/3/reference/datamodel.html#emulating-callable-objects + +# TODO: CanCall + + +# 3.3.7. Emulating container types +# https://docs.python.org/3/reference/datamodel.html#emulating-container-types + +@runtime_checkable +class CanLen(Protocol): + def __len__(self) -> int: ... + +@runtime_checkable +class CanLengthHint(Protocol): + def __length_hint__(self) -> int: ... + +@runtime_checkable +class CanGetitem[K, V](Protocol): + def __getitem__(self, __k: K) -> V: ... + +@runtime_checkable +class CanSetitem[K, V](Protocol): + def __setitem__(self, __k: K, __v: V) -> None: ... + +@runtime_checkable +class CanDelitem[K](Protocol): + def __delitem__(self, __k: K) -> None: ... + +@runtime_checkable +class CanMissing[K, V](Protocol): + def __missing__(self, __k: K) -> V: ... + +@runtime_checkable +class CanReversed[Y](Protocol): + def __reversed__(self) -> Y: ... + +@runtime_checkable +class CanContains[K](Protocol): + def __contains__(self, __k: K) -> bool: ... + + +# 3.3.8. Emulating numeric types +# https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types + +@runtime_checkable +class CanAdd[X, Y](Protocol): + def __add__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanSub[X, Y](Protocol): + def __sub__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanMul[X, Y](Protocol): + def __mul__(self, __x: X) -> Y: ... + +@runtime_checkable +class CanMatmul[X, Y](Protocol): + def __matmul__(self, __x: X) -> Y: ... + +@runtime_checkable +class CanTruediv[X, Y](Protocol): + def __truediv__(self, __x: X) -> Y: ... + +@runtime_checkable +class CanFloordiv[X, Y](Protocol): + def __floordiv__(self, __x: X) -> Y: ... + +@runtime_checkable +class CanMod[X, Y](Protocol): + def __mod__(self, __x: X) -> Y: ... + +@runtime_checkable +class CanDivmod[X, Y](Protocol): + def __divmod__(self, __x: X) -> Y: ... + +@runtime_checkable +class CanPow2[X, Y2](Protocol): + def __pow__(self, __x: X) -> Y2: ... + +@runtime_checkable +class CanPow3[X, M, Y3](Protocol): + def __pow__(self, __x: X, __m: M) -> Y3: ... + +@runtime_checkable +class CanPow[X, M, Y2, Y3](CanPow2[X, Y2], CanPow3[X, M, Y3], Protocol): + @overload + def __pow__(self, __x: X) -> Y2: ... + @overload + def __pow__(self, __x: X, __m: M) -> Y3: ... + +@runtime_checkable +class CanLshift[X, Y](Protocol): + def __lshift__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanRshift[X, Y](Protocol): + def __rshift__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanAnd[X, Y](Protocol): + def __and__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanXor[X, Y](Protocol): + def __xor__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanOr[X, Y](Protocol): + def __or__(self, __x: X, /) -> Y: ... + + +@runtime_checkable +class CanRAdd[X, Y](Protocol): + def __radd__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanRSub[X, Y](Protocol): + def __rsub__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanRMul[X, Y](Protocol): + def __rmul__(self, __x: X) -> Y: ... + +@runtime_checkable +class CanRMatmul[X, Y](Protocol): + def __rmatmul__(self, __x: X) -> Y: ... + +@runtime_checkable +class CanRTruediv[X, Y](Protocol): + def __rtruediv__(self, __x: X) -> Y: ... + +@runtime_checkable +class CanRFloordiv[X, Y](Protocol): + def __rfloordiv__(self, __x: X) -> Y: ... + +@runtime_checkable +class CanRMod[X, Y](Protocol): + def __rmod__(self, __x: X) -> Y: ... + +@runtime_checkable +class CanRDivmod[X, Y](Protocol): + def __rdivmod__(self, __x: X) -> Y: ... + +@runtime_checkable +class CanRPow[X, Y](Protocol): + def __rpow__(self, __x: X) -> Y: ... + +@runtime_checkable +class CanRLshift[X, Y](Protocol): + def __rlshift__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanRRshift[X, Y](Protocol): + def __rrshift__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanRAnd[X, Y](Protocol): + def __rand__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanRXor[X, Y](Protocol): + def __rxor__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanROr[X, Y](Protocol): + def __ror__(self, __x: X, /) -> Y: ... + + +@runtime_checkable +class CanIAdd[X, Y](Protocol): + def __iadd__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanISub[X, Y](Protocol): + def __isub__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanIMul[X, Y](Protocol): + def __imul__(self, __x: X) -> Y: ... + +@runtime_checkable +class CanIMatmul[X, Y](Protocol): + def __imatmul__(self, __x: X) -> Y: ... + +@runtime_checkable +class CanITruediv[X, Y](Protocol): + def __itruediv__(self, __x: X) -> Y: ... + +@runtime_checkable +class CanIFloordiv[X, Y](Protocol): + def __ifloordiv__(self, __x: X) -> Y: ... + +@runtime_checkable +class CanIMod[X, Y](Protocol): + def __imod__(self, __x: X) -> Y: ... + +@runtime_checkable +class CanIPow[X, Y](Protocol): + # no augmented pow/3 exists + def __ipow__(self, __x: X) -> Y: ... + +@runtime_checkable +class CanILshift[X, Y](Protocol): + def __ilshift__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanIRshift[X, Y](Protocol): + def __irshift__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanIAnd[X, Y](Protocol): + def __iand__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanIXor[X, Y](Protocol): + def __ixor__(self, __x: X, /) -> Y: ... + +@runtime_checkable +class CanIOr[X, Y](Protocol): + def __ior__(self, __x: X, /) -> Y: ... + + +@runtime_checkable +class CanNeg[Y](Protocol): + def __neg__(self) -> Y: ... + +@runtime_checkable +class CanPos[Y](Protocol): + def __pos__(self) -> Y: ... + +@runtime_checkable +class CanAbs[Y](Protocol): + def __abs__(self) -> Y: ... + +@runtime_checkable +class CanInvert[Y](Protocol): + def __invert__(self) -> Y: ... + + +@runtime_checkable +class CanComplex(Protocol): + def __complex__(self) -> complex: ... + +@runtime_checkable +class CanFloat(Protocol): + def __float__(self) -> float: ... + +@runtime_checkable +class CanInt(Protocol): + def __int__(self) -> int: ... + +@runtime_checkable +class CanIndex(Protocol): + def __index__(self) -> int: ... + + +@runtime_checkable +class CanRound1[Y](Protocol): + def __round__(self) -> Y: ... + +@runtime_checkable +class CanRound2[N, Y](Protocol): + def __round__(self, __n: N) -> Y: ... + +@runtime_checkable +class CanRound[N, Y1, Y2](CanRound1[Y1], CanRound2[N, Y2], Protocol): + @overload + def __round__(self) -> Y1: ... + @overload + def __round__(self, __n: N) -> Y2: ... + +@runtime_checkable +class CanTrunc[Y](Protocol): + def __trunc__(self) -> Y: ... + +@runtime_checkable +class CanFloor[Y](Protocol): + def __floor__(self) -> Y: ... + +@runtime_checkable +class CanCeil[Y](Protocol): + def __ceil__(self) -> Y: ... + + +# 3.3.9. With Statement Context Managers +# https://docs.python.org/3/reference/datamodel.html#with-statement-context-managers + +# TODO: CanEnter +# TODO: CanExit +# TODO: CanWith = CanEnter & CanExit + +# 3.3.10. Customizing positional arguments in class pattern matching +# https://docs.python.org/3/reference/datamodel.html#customizing-positional-arguments-in-class-pattern-matching + +# TODO: HasMatchArgs + + +# 3.3.11. Emulating buffer types +# https://docs.python.org/3/reference/datamodel.html#emulating-buffer-types + +# TODO: CanBuffer +# TODO: CanReleaseBuffer + + +# 3.4.1. Awaitable Objects +# https://docs.python.org/3/reference/datamodel.html#awaitable-objects + +# TODO: CanAwait + + +# 3.4.2. Coroutine Objects +# https://docs.python.org/3/reference/datamodel.html#coroutine-objects + +# TODO: .send(_), .throw(_), .throw(_, _), .throw(_, _, _), .close() ??? + + +# 3.4.3. Asynchronous Iterators +# https://docs.python.org/3/reference/datamodel.html#asynchronous-iterators + +@runtime_checkable +class CanAnext[V](Protocol): + def __anext__(self) -> V: ... + +@runtime_checkable +class CanAiter[Y: CanAnext[Any]](Protocol): + def __aiter__(self) -> Y: ... + + +# 3.4.4. Asynchronous Context Managers +# https://docs.python.org/3/reference/datamodel.html#asynchronous-context-managers + +# TODO: CanAenter +# TODO: CanAexit +# TODO: CanAsyncWith = CanAenter & CanAexit diff --git a/optype/_cmpops.py b/optype/_cmpops.py deleted file mode 100644 index b47c3e4b..00000000 --- a/optype/_cmpops.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Interfaces for the "rich comparison" methods (auto-reflective).""" -import typing as _tp - - -@_tp.runtime_checkable -class CanLt[X, Y](_tp.Protocol): - """ - - `self < other` - - `other > self` - """ - def __lt__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanGt[X, Y](_tp.Protocol): - """ - - `self > other` - - `other < self` - """ - def __gt__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanLe[X, Y](_tp.Protocol): - """ - - `self <= other` - - `other >= self` - """ - def __le__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanGe[X, Y](_tp.Protocol): - """ - - `self >= other` - - `other <= self` - """ - def __ge__(self, __other: X, /) -> Y: ... - -@_tp.runtime_checkable -class CanEq[X, Y](_tp.Protocol): - """ - - `self == other` - - `other == self` - """ - def __eq__(self, __other: X, /) -> Y: ... # type: ignore[override] - -@_tp.runtime_checkable -class CanNe[X, Y](_tp.Protocol): - """ - - `self != other` - - `other != self` - """ - def __ne__(self, __other: X, /) -> Y: ... # type: ignore[override] diff --git a/optype/_containers.py b/optype/_containers.py deleted file mode 100644 index acdf6d87..00000000 --- a/optype/_containers.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Generic interfaces for elementary container operations.""" -import typing as _tp - - -@_tp.runtime_checkable -class CanContains[K](_tp.Protocol): - # vibrantly generic - """ - `other in self` - """ - def __contains__(self, __other: K) -> bool: ... - -@_tp.runtime_checkable -class CanGetitem[K, V](_tp.Protocol): - """ - `self[key]` - """ - def __getitem__(self, __key: K) -> V: ... - -@_tp.runtime_checkable -class CanSetitem[K, V](_tp.Protocol): - """ - `self[key] = value` - """ - def __setitem__(self, __key: K, __value: V) -> None: ... - -@_tp.runtime_checkable -class CanDelitem[K](_tp.Protocol): - """ - `self[key] = value` - """ - def __delitem__(self, __key: K) -> None: ... - -@_tp.runtime_checkable -class CanMissing[K, V](_tp.Protocol): - """ - fallback for `self[key]` - """ - def __missing__(self, __key: K) -> V: ... diff --git a/optype/_specialattrs.py b/optype/_has.py similarity index 57% rename from optype/_specialattrs.py rename to optype/_has.py index 9a33a6d2..0d31c286 100644 --- a/optype/_specialattrs.py +++ b/optype/_has.py @@ -2,41 +2,41 @@ """ Elementary interfaces for special "dunder" attributes. """ -import typing as _tp +from typing import TYPE_CHECKING, Any, Protocol, Self, runtime_checkable -if _tp.TYPE_CHECKING: + +if TYPE_CHECKING: import weakref as _weakref # special attributes -@_tp.runtime_checkable -class HasDict[V](_tp.Protocol): +@runtime_checkable +class HasDict[V](Protocol): __dict__: dict[str, V] -@_tp.runtime_checkable -class _HasDocAttr(_tp.Protocol): +@runtime_checkable +class _HasDocAttr(Protocol): __doc__: str | None -@_tp.runtime_checkable -class _HasDocProp(_tp.Protocol): +@runtime_checkable +class _HasDocProp(Protocol): @property def __doc__(self) -> str | None: ... # type: ignore[override] type HasDoc = _HasDocAttr | _HasDocProp -"""Note that with (c)python's `-OO` flag, any generated `__doc__` is `None`.""" -@_tp.runtime_checkable -class _HasNameAttr(_tp.Protocol): +@runtime_checkable +class _HasNameAttr(Protocol): __name__: str -@_tp.runtime_checkable -class _HasNameProp(_tp.Protocol): +@runtime_checkable +class _HasNameProp(Protocol): @property def __name__(self) -> str: ... @@ -44,17 +44,18 @@ def __name__(self) -> str: ... type HasName = _HasNameAttr | _HasNameProp -@_tp.runtime_checkable -class HasQualname(_tp.Protocol): +@runtime_checkable +class HasQualname(Protocol): __qualname__: str -@_tp.runtime_checkable -class _HasModuleAttr(_tp.Protocol): +@runtime_checkable +class _HasModuleAttr(Protocol): __module__: str -class _HasModuleProp(_tp.Protocol): +@runtime_checkable +class _HasModuleProp(Protocol): @property def __module__(self) -> str: ... # type: ignore[override] @@ -62,31 +63,33 @@ def __module__(self) -> str: ... # type: ignore[override] type HasModule = _HasModuleAttr | _HasModuleProp -@_tp.runtime_checkable -class HasAnnotations[V](_tp.Protocol): +@runtime_checkable +class HasAnnotations[V](Protocol): """Note that the `V` type is hard to accurately define; blame PEP 563.""" __annotations__: dict[str, V] -@_tp.runtime_checkable -class HasWeakReference(_tp.Protocol): +# weakref + +@runtime_checkable +class HasWeakReference(Protocol): """An object referenced by a `weakref.ReferenceType[Self]`.""" - __weakref__: '_weakref.ReferenceType[_tp.Self]' + __weakref__: '_weakref.ReferenceType[Self]' -@_tp.runtime_checkable -class HasWeakCallableProxy[**Xs, Y](_tp.Protocol): +@runtime_checkable +class HasWeakCallableProxy[**Xs, Y](Protocol): """A callable referenced by a `weakref.CallableProxyType[Self]`.""" - __weakref__: '_weakref.CallableProxyType[_tp.Self]' + __weakref__: '_weakref.CallableProxyType[Self]' def __call__(self, *__args: Xs.args, **__kwargs: Xs.kwargs) -> Y: ... -@_tp.runtime_checkable -class _HasWeakProxy(_tp.Protocol): - __weakref__: '_weakref.ProxyType[_tp.Self]' +@runtime_checkable +class _HasWeakProxy(Protocol): + __weakref__: '_weakref.ProxyType[Self]' -type HasWeakProxy = HasWeakCallableProxy[..., _tp.Any] | _HasWeakProxy +type HasWeakProxy = HasWeakCallableProxy[..., Any] | _HasWeakProxy """An object referenced by a `weakref.proxy` (not the proxy itself).""" diff --git a/optype/_nullops.py b/optype/_nullops.py deleted file mode 100644 index 3d5d71ab..00000000 --- a/optype/_nullops.py +++ /dev/null @@ -1,104 +0,0 @@ -""" -Interfaces for the "nullary" ops (w.r.t. arity of the type parameter). - -Note that this might not seem DRY, since many of the protocols are already in -`typing.Supports`. But the problem with those is, that they are also -metaclasses, and that I (@jorenham) apparently am turing into a typing-purist. -""" -import typing as _tp - -# type conversion - - -@_tp.runtime_checkable -class CanBool(_tp.Protocol): - """ - `bool(self)` - """ - def __bool__(self) -> bool: ... - -@_tp.runtime_checkable -class CanInt(_tp.Protocol): - """ - `int(self)` - """ - def __int__(self) -> int: ... - -@_tp.runtime_checkable -class CanFloat(_tp.Protocol): - """ - `float(self)` - """ - def __float__(self) -> float: ... - -@_tp.runtime_checkable -class CanComplex(_tp.Protocol): - """ - `complex(self)` - """ - def __complex__(self) -> float: ... - -@_tp.runtime_checkable -class CanBytes(_tp.Protocol): - """ - `bytes(self)` - """ - def __bytes__(self) -> str: ... - -@_tp.runtime_checkable -class CanStr(_tp.Protocol): - """ - `str(self)` - """ - def __str__(self) -> str: ... - - -# display methods - -@_tp.runtime_checkable -class CanRepr(_tp.Protocol): - """ - `repr(self)` - """ - def __repr__(self) -> str: ... - - -# size methods - -@_tp.runtime_checkable -class CanLen(_tp.Protocol): - """ - `len(self)` - - some notes: - - must be non-negative. - - (cpython) must not exceed `sys.maxsize` - - (cpython) without `__bool__` cpython will use `bool(len(self))` instead - """ - def __len__(self) -> int: ... - -@_tp.runtime_checkable -class CanLengthHint(_tp.Protocol): - """ - - approximation of `len(self)` - - purely for optimization purposes - - must be `>=0` or `NotImplemented` - """ - def __length_hint__(self) -> int: ... - - -# fingerprinting - -@_tp.runtime_checkable -class CanHash(_tp.Protocol): - """ - `hash(self)` - """ - def __hash__(self) -> int: ... - -@_tp.runtime_checkable -class CanIndex(_tp.Protocol): - """ - `hash(self)` - """ - def __index__(self) -> int: ... diff --git a/optype/_slice.py b/optype/_slice.py index 20c7f519..0a00c58e 100644 --- a/optype/_slice.py +++ b/optype/_slice.py @@ -3,10 +3,11 @@ import typing as _tp + if _tp.TYPE_CHECKING: import types as _ts - from ._nullops import CanIndex as _CanIndex + from ._can import CanIndex as _CanIndex @_tp.final @@ -22,9 +23,9 @@ def __new__(cls, __stop: B) -> Slice[None, B, None]: ... def __new__(cls, __start: A, __stop: B) -> Slice[A, B, None]: ... @_tp.overload def __new__(cls, __start: A, __stop: B, __step: S) -> Slice[A, B, S]: ... - + @_tp.override def __eq__(self, __other: object) -> bool: ... - + @_tp.override def __ne__(self, __other: object) -> bool: ... def __lt__(self, __other: object) -> bool | _ts.NotImplementedType: ... diff --git a/optype/_unops.py b/optype/_unops.py deleted file mode 100644 index 3de5a8e6..00000000 --- a/optype/_unops.py +++ /dev/null @@ -1,131 +0,0 @@ -"""Interfaces for the (generic) unary operations.""" - -import collections.abc as _abc -import typing as _tp - -# arithmetic operations - - -@_tp.runtime_checkable -class CanNeg[Y](_tp.Protocol): - """ - `-self` - """ - def __neg__(self) -> Y: ... - -@_tp.runtime_checkable -class CanPos[Y](_tp.Protocol): - """ - `+self` - """ - def __pos__(self) -> Y: ... - -@_tp.runtime_checkable -class CanInvert[Y](_tp.Protocol): - """ - `~self` - """ - def __invert__(self) -> Y: ... - -@_tp.runtime_checkable -class CanAbs[Y](_tp.Protocol): - """ - `abs(self)` - """ - def __abs__(self) -> Y: ... - - -# rounding - -@_tp.runtime_checkable -class CanRound0[Y](_tp.Protocol): - """ - `round(self) -> Y0` - """ - def __round__(self) -> Y: ... - -@_tp.runtime_checkable -class CanRound[N, Y, YN](_tp.Protocol): - """ - Implements `round(self[, ndigits])` through `__pow__` with overloaded - signatures: - - - `(Self) -> Y` - - `(Self, N) -> YN` - """ - @_tp.overload - def __round__(self) -> Y: ... - @_tp.overload - def __round__(self, __ndigits: N) -> YN: ... - -@_tp.runtime_checkable -class CanTrunc[Y](_tp.Protocol): - """ - `math.trunc(self)` - """ - def __trunc__(self) -> Y: ... - -@_tp.runtime_checkable -class CanFloor[Y](_tp.Protocol): - """ - `math.floor(self)` - """ - def __floor__(self) -> Y: ... - - -@_tp.runtime_checkable -class CanCeil[Y](_tp.Protocol): - """ - `math.ceil(self)` - """ - def __ceil__(self) -> Y: ... - -# iteration - -@_tp.runtime_checkable -class CanNext[V](_tp.Protocol): - """ - `next(self)` - """ - def __next__(self) -> V: ... - - -@_tp.runtime_checkable -class CanIter[Y: CanNext[_tp.Any]](_tp.Protocol): - """ - `iter(self)` - """ - def __iter__(self) -> Y: ... - - -@_tp.runtime_checkable -class CanReversed[Y](_tp.Protocol): - """ - `reversed(self)` - """ - def __reversed__(self) -> Y: ... - -# async iteration - -@_tp.runtime_checkable -class CanAnext[V](_tp.Protocol): - """ - `anext(self)` - """ - def __anext__(self) -> V: ... - - -@_tp.runtime_checkable -class CanAiter[Y: CanAnext[_tp.Any]](_tp.Protocol): - """ - `aiter(self)` - """ - def __aiter__(self) -> Y: ... - - -# introspection - -@_tp.runtime_checkable -class CanDir[Vs: _abc.Iterable[_tp.Any]](_tp.Protocol): - # TODO: don't use collections.abc - def __dir__(self) -> Vs: ... diff --git a/pyproject.toml b/pyproject.toml index c680634f..cb2fc8b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,12 +43,12 @@ skip = """\ include = ["optype", "tests"] exclude = [ "**/__pycache__", + "**/.vscode", ".venv", ".git", ".github", ".pytest_cache", ".ruff_cache", - ".vscode", "dist", ] stubPath = "." @@ -88,6 +88,7 @@ reportUnusedCoroutine = "error" reportUnusedExpression = "warning" reportUnnecessaryTypeIgnoreComment = "error" reportMatchNotExhaustive = "error" +reportImplicitOverride = "warning" reportShadowedImports = "error" @@ -170,12 +171,13 @@ select = [ "FLY", # flynt "PERF", # perflint, "FURB", # refurb - "RUF", # ruff + "RUF", # ruff ] extend-ignore = [ # flake8-annotations "ANN001", # missing-type-function-argument (deprecated) "ANN002", # missing-type-args (deprecated) + "ANN401", # any-type (unreasonable) # pylint "PLW1641", # eq-without-hash (buggy; doesn't consider super) @@ -200,6 +202,8 @@ case-sensitive = true combine-as-imports = true force-wrap-aliases = true known-first-party = ["optype"] +lines-between-types = 0 +lines-after-imports = 2 no-lines-before = ["future", "local-folder"] [tool.ruff.format] From 6aead56d9898b36ca1ab613d6a98a4c548ac43bd Mon Sep 17 00:00:00 2001 From: jorenham Date: Fri, 23 Feb 2024 08:15:22 +0100 Subject: [PATCH 11/26] improved the docs --- README.md | 91 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 70f8a9ee..24df13bf 100644 --- a/README.md +++ b/README.md @@ -45,10 +45,8 @@ --- - - > [!WARNING] -> The API is not stable; use at your own risk. +> The API is not stable; use it at your own risk. ## Installation @@ -69,29 +67,34 @@ pip install optype ## Reference -All of the types here live in the root `optype` namespace. -They are runtime checkable, so that you can do e.g. +All [typing protocols](PC) here live in the root `optype` namespace. +They are [runtime-checkable](RC) so that you can do e.g. `isinstance('snail', optype.CanAdd)`, in case you want to check whether `snail` implements `__add__`. > [!NOTE] -> It is bad practise to use a `typing.Protocol` as base class for your -> implementation. Because of `@typing.runtime_checkable`, you can use +> It is bad practice to use a [`typing.Protocol`](PC) as base class for your +> implementation. Because of [`@typing.runtime_checkable`](RC), you can use > `isinstance` either way. -Unlike e.g. `collections.abc`, the `optype` protocols aren't abstract. -This makes it easier to create sub-protocols, and provides a clearer -distinction between *interface* and *implementation*. +Unlike`collections.abc`, `optype`'s protocols aren't abstract base classes, +i.e. they don't extend `abc.ABC`, only `typing.Protocol`. +This allows the `optype` protocols to be used as building blocks for `.pyi` +type stubs. + +[PC]: https://typing.readthedocs.io/en/latest/spec/protocol.html +[RC]: https://typing.readthedocs.io/en/latest/spec/protocol.html#runtime-checkable-decorator-and-narrowing-types-by-isinstance ### Elementary interfaces for the special methods -Single-method `typing.Protocol` definitions for each of the "special methods", -also known as "magic"- or "dunder"- methods. See the [Python docs](SM) for -details. +Single-method [`typing.Protocol`](PC) definitions for each of the "special +methods", also known as "magic"- or "dunder" methods. +See the [Python docs](SM) for details. [SM]: https://docs.python.org/3/reference/datamodel.html#special-method-names + #### Strict type conversion The return type of these special methods is *invariant*. Python will raise an @@ -100,14 +103,14 @@ This is why these `optype` interfaces don't accept generic type arguments. **Builtin type constructors:** -| Type | Signature | Expression | -| ------------ | ------------------------------ | ------------------ | -| `CanBool` | `__bool__(self) -> bool` | `bool(self)` | -| `CanInt` | `__int__(self) -> int` | `int(self)` | -| `CanFloat` | `__float__(self) -> float` | `float(self)` | -| `CanComplex` | `__complex__(self) -> complex` | `complex(self)` | -| `CanBytes` | `__bytes__(self) -> bytes` | `bytes(self)` | -| `CanStr` | `__str__(self) -> str` | `str(self)` | +| Type | Signature | Expression | +| ------------ | ------------------------------ | --------------- | +| `CanBool` | `__bool__(self) -> bool` | `bool(self)` | +| `CanInt` | `__int__(self) -> int` | `int(self)` | +| `CanFloat` | `__float__(self) -> float` | `float(self)` | +| `CanComplex` | `__complex__(self) -> complex` | `complex(self)` | +| `CanBytes` | `__bytes__(self) -> bytes` | `bytes(self)` | +| `CanStr` | `__str__(self) -> str` | `str(self)` | **Other builtin functions:** @@ -123,12 +126,12 @@ This is why these `optype` interfaces don't accept generic type arguments. [LH]: https://docs.python.org/3/reference/datamodel.html#object.__length_hint__ [IX]: https://docs.python.org/3/reference/datamodel.html#object.__index__ + #### Comparisons operators Generally these methods return a `bool`. But in theory, anything can be returned (even if it doesn't implement `__bool__`). - | Type | Signature | Expression | Expr. Reflected | | -------------- | -------------------------- | ----------- | --------------- | | `CanLt[X, Y]` | `__lt__(self, x: X) -> Y` | `self < x` | `x > self` | @@ -139,6 +142,30 @@ returned (even if it doesn't implement `__bool__`). | `CanNe[X, Y]` | `__ne__(self, x: X) -> Y` | `self != x` | `x != self` | +#### Rounding + +| Type | Signature | Expression | +| ------------------ | ----------------------------- | --------------------- | +| `CanRound1[Y1]` | `__round__(self) -> Y1` | `round(self)` | +| `CanRound2[N, Y2]` | `__round__(self, n: N) -> Y2` | `round(self, n: N)` | + +For convenience, `optype` also provides their intersection type +`CanRound[N, Y1, Y2] =: CanRound1[Y1] & CanRound2[N, Y2]`, whose signature +overloads those of the `CanRound1` and `CanRound2`. + +For instance, `float` is a `CanRound[int, int, float]` and `int` a +`CanRound[int, int, int]`. + +| Type | Signature | Expression | +| -------------- | ----------------------- | ------------------ | +| `CanTrunc[Y]` | `__trunc__(self) -> Y` | `math.trunc(self)` | +| `CanFloor[Y]` | `__floor__(self) -> Y` | `math.floor(self)` | +| `CanCeil[Y]` | `__ceil__(self) -> Y` | `math.ceil(self)` | + +Note that the type parameter `Y` is unbounded, because technically these +methods can return any type. + + #### Arithmetic and bitwise operators **Unary:** @@ -149,10 +176,6 @@ returned (even if it doesn't implement `__bool__`). | `CanNeg[Y]` | `__neg__(self) -> Y` | `-self` | | `CanInvert[Y]` | `__invert__(self) -> Y` | `~self` | | `CanAbs[Y]` | `__abs__(self) -> Y` | `abs(self)` | -| `CanRound0[Y]` | `__round__(self) -> Y` | `round(self)` | -| `CanTrunc[Y]` | `__trunc__(self) -> Y` | `math.trunc(self)` | -| `CanFloor[Y]` | `__floor__(self) -> Y` | `math.floor(self)` | -| `CanCeil[Y]` | `__ceil__(self) -> Y` | `math.ceil(self)` | **Binary:** @@ -167,19 +190,20 @@ returned (even if it doesn't implement `__bool__`). | `CanFloordiv[X, Y]` | `__floordiv__(self, x: X) -> Y` | `self // x` | | `CanMod[X, Y]` | `__mod__(self, x: X) -> Y` | `self % x` | | `CanDivmod[X, Y]` | `__divmod__(self, x: X) -> Y` | `divmod(self, x)` | -| `CanPow[X, Y]` | `__pow__(self, x: X) -> Y` | `self ** x` | +| `CanPow2[X, Y]` | `__pow__(self, x: X) -> Y` | `self ** x` | +| `CanPow3[X, M, Y]` | `__pow__(self, x: X, m: M) -> Y`| `pow(self, x, m)` | | `CanLshift[X, Y]` | `__lshift__(self, x: X) -> Y` | `self << x` | | `CanRshift[X, Y]` | `__rshift__(self, x: X) -> Y` | `self >> x` | | `CanAnd[X, Y]` | `__and__(self, x: X) -> Y` | `self & x` | | `CanXor[X, Y]` | `__xor__(self, x: X) -> Y` | `self ^ x` | | `CanOr[X, Y]` | `__or__(self, x: X) -> Y` | `self \| x` | - - +Additionally, there is the intersection type +`CanPow[X, M, Y2, Y3] =: CanPow2[X, Y2] & CanPow3[X, M, Y3]`, whose signature +overloads those of the `CanPow2` and `CanPow3`. **Binary (reflected):** - | Type | Signature | Expression | | -------------------- | -------------------------------- | ----------------- | | `CanRAdd[X, Y]` | `__radd__(self, x: X) -> Y` | `x + self` | @@ -232,6 +256,7 @@ returned (even if it doesn't implement `__bool__`). [GM]: https://docs.python.org/3/reference/datamodel.html#object.__missing__ + ### Iteration **Sync** @@ -246,10 +271,9 @@ returned (even if it doesn't implement `__bool__`). **Async** - | Type | Signature | Expression | | ---------------------------- | ----------------------- | ---------------- | -| `CanAnext[V]` (**) | `__anext__(self) -> V` | `anext(self)` | +| `CanAnext[V]` (**) | `__anext__(self) -> V` | `anext(self)` | | `CanAiter[Y: CanAnext[Any]]` | `__aiter__(self) -> Y` | `aiter(self)` | (**) Although not strictly required, `V@CanAnext` should be an `Awaitable`. @@ -277,9 +301,8 @@ denotes an optional parameter). - `@typing.final` -## Roadmap +## Future plans -- Single-method protocols for descriptors - Build a replacement for the `operator` standard library, with runtime-accessible type annotations - Protocols for numpy's dunder methods From bb5e20e339acb567e2c26f5056ce815bbb78a3f5 Mon Sep 17 00:00:00 2001 From: jorenham Date: Fri, 23 Feb 2024 15:31:26 +0100 Subject: [PATCH 12/26] ignore buggy ruff rule --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index cb2fc8b3..177d9855 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -179,6 +179,9 @@ extend-ignore = [ "ANN002", # missing-type-args (deprecated) "ANN401", # any-type (unreasonable) + # flake8-pyi + "PYI036", # bad-exit-annotation (FP with more precise overloads) + # pylint "PLW1641", # eq-without-hash (buggy; doesn't consider super) ] From 080f5402692d444cd11683889fb237a2820e8e6a Mon Sep 17 00:00:00 2001 From: jorenham Date: Fri, 23 Feb 2024 15:32:26 +0100 Subject: [PATCH 13/26] Added several more dunder protocols --- optype/__init__.py | 26 ++++++++ optype/_can.py | 150 ++++++++++++++++++++++++++++++++++++++------- 2 files changed, 153 insertions(+), 23 deletions(-) diff --git a/optype/__init__.py b/optype/__init__.py index 86da3889..6092b6d8 100644 --- a/optype/__init__.py +++ b/optype/__init__.py @@ -4,22 +4,30 @@ 'CanAiter', 'CanAnd', 'CanAnext', + 'CanAwait', 'CanBool', + 'CanBuffer', 'CanBytes', + 'CanCall', 'CanCeil', 'CanComplex', 'CanContains', 'CanDel', 'CanDelattr', + 'CanDelete', 'CanDelitem', 'CanDir', 'CanDivmod', + 'CanEnter', 'CanEq', + 'CanExit', 'CanFloat', 'CanFloor', 'CanFloordiv', 'CanFormat', 'CanGe', + 'CanGet', + 'CanGetBound', 'CanGetattr', 'CanGetattribute', 'CanGetitem', @@ -39,6 +47,7 @@ 'CanITruediv', 'CanIXor', 'CanIndex', + 'CanInitSubclass', 'CanInt', 'CanInvert', 'CanIter', @@ -73,18 +82,22 @@ 'CanRSub', 'CanRTruediv', 'CanRXor', + 'CanReleaseBuffer', 'CanRepr', 'CanReversed', 'CanRound', 'CanRound1', 'CanRound2', 'CanRshift', + 'CanSet', + 'CanSetName', 'CanSetattr', 'CanSetitem', 'CanStr', 'CanSub', 'CanTruediv', 'CanTrunc', + 'CanWith', 'CanXor', 'HasAnnotations', 'HasDict', @@ -107,22 +120,30 @@ CanAiter, CanAnd, CanAnext, + CanAwait, CanBool, + CanBuffer, CanBytes, + CanCall, CanCeil, CanComplex, CanContains, CanDel, CanDelattr, + CanDelete, CanDelitem, CanDir, CanDivmod, + CanEnter, CanEq, + CanExit, CanFloat, CanFloor, CanFloordiv, CanFormat, CanGe, + CanGet, + CanGetBound, CanGetattr, CanGetattribute, CanGetitem, @@ -142,6 +163,7 @@ CanITruediv, CanIXor, CanIndex, + CanInitSubclass, CanInt, CanInvert, CanIter, @@ -176,18 +198,22 @@ CanRSub, CanRTruediv, CanRXor, + CanReleaseBuffer, CanRepr, CanReversed, CanRound, CanRound1, CanRound2, CanRshift, + CanSet, + CanSetName, CanSetattr, CanSetitem, CanStr, CanSub, CanTruediv, CanTrunc, + CanWith, CanXor, ) from ._has import ( diff --git a/optype/_can.py b/optype/_can.py index d96d5eb5..78f42436 100644 --- a/optype/_can.py +++ b/optype/_can.py @@ -1,5 +1,13 @@ # ruff: noqa: PYI034 -from typing import Any, Protocol, overload, override, runtime_checkable +from collections.abc import Generator +from types import TracebackType +from typing import ( + Any, + Protocol, + overload, + override, + runtime_checkable, +) # Iterator types @@ -41,32 +49,31 @@ def __bytes__(self) -> Y: ... @runtime_checkable class CanFormat[X: str, Y: str](Protocol): - # typeshed's object.__format__ stub is unnecessarily restrictive def __format__(self, __x: X) -> Y: ... # type: ignore[override] @runtime_checkable class CanLt[X, Y](Protocol): - def __lt__(self, __x: X, /) -> Y: ... + def __lt__(self, __x: X) -> Y: ... @runtime_checkable class CanLe[X, Y](Protocol): - def __le__(self, __x: X, /) -> Y: ... + def __le__(self, __x: X) -> Y: ... @runtime_checkable class CanEq[X, Y](Protocol): - def __eq__(self, __x: X, /) -> Y: ... # type: ignore[override] + def __eq__(self, __x: X) -> Y: ... # type: ignore[override] @runtime_checkable class CanNe[X, Y](Protocol): - def __ne__(self, __x: X, /) -> Y: ... # type: ignore[override] + def __ne__(self, __x: X) -> Y: ... # type: ignore[override] @runtime_checkable class CanGt[X, Y](Protocol): - def __gt__(self, __x: X, /) -> Y: ... + def __gt__(self, __x: X) -> Y: ... @runtime_checkable class CanGe[X, Y](Protocol): - def __ge__(self, __x: X, /) -> Y: ... + def __ge__(self, __x: X) -> Y: ... @runtime_checkable class CanHash(Protocol): @@ -111,9 +118,25 @@ def __dir__(self) -> Vs: ... # 3.3.2.2. Implementing Descriptors # https://docs.python.org/3/reference/datamodel.html#implementing-descriptors -# TODO: CanGet -# TODO: CanSet -# TODO: CanDelete +@runtime_checkable +class CanGetBound[T, V](Protocol): + def __get__(self, __obj: T, __cls: type[T] | None = ...) -> V: ... + +@runtime_checkable +class CanGet[T, U, V](CanGetBound[T, V], Protocol): + @overload + def __get__(self, __obj: None, __cls: type[T]) -> U: ... + @overload + def __get__(self, __obj: T, __cls: type[T] | None = ...) -> V: ... + +@runtime_checkable +class CanSet[T, V](Protocol): + def __set__(self, __obj: T, __v: V) -> Any: ... + +@runtime_checkable +class CanDelete[T](Protocol): + def __delete__(self, __obj: T) -> Any: ... + # TODO: HasObjclass # 3.3.2.4. `__slots__` @@ -125,8 +148,21 @@ def __dir__(self) -> Vs: ... # 3.3.3. Customizing class creation # https://docs.python.org/3/reference/datamodel.html#customizing-class-creation -# TODO: CanInitSubclass -# TODO: CanSetName +@runtime_checkable +class CanInitSubclass[**Ps](Protocol): + # no positional-only are allowed + # cannot `Unpack` type args: https://github.com/python/typing/issues/1399 + # workaround: use `**Ps` instead of `Ps: TypedDict` + def __init_subclass__( + cls, + *__dont_use_these_args: Ps.args, + **__kwargs: Ps.kwargs, + ) -> Any: ... + +@runtime_checkable +class CanSetName[T](Protocol): + def __set_name__(self, __cls: type[T], __name: str) -> Any: ... + # 3.3.3.2. Resolving MRO entries # https://docs.python.org/3/reference/datamodel.html#resolving-mro-entries @@ -160,7 +196,9 @@ def __dir__(self) -> Vs: ... # 3.3.6. Emulating callable objects # https://docs.python.org/3/reference/datamodel.html#emulating-callable-objects -# TODO: CanCall +@runtime_checkable +class CanCall[**Xs, Y](Protocol): + def __call__(self, *__xa: Xs.args, **__xk: Xs.kwargs) -> Y: ... # 3.3.7. Emulating container types @@ -446,9 +484,26 @@ def __ceil__(self) -> Y: ... # 3.3.9. With Statement Context Managers # https://docs.python.org/3/reference/datamodel.html#with-statement-context-managers -# TODO: CanEnter -# TODO: CanExit -# TODO: CanWith = CanEnter & CanExit +@runtime_checkable +class CanEnter[V](Protocol): + def __enter__(self) -> V: ... + +@runtime_checkable +class CanExit(Protocol): + @overload + def __exit__(self, __tp: None, __ex: None, __tb: None) -> None: ... + @overload + def __exit__( + self, + __tp: type[BaseException], + __ex: BaseException, + __tb: TracebackType, + ) -> None: ... + +@runtime_checkable +class CanWith[V](CanEnter[V], CanExit, Protocol): + """Intersection type of `CanEnter[V] & CanExit`, i.e. a contextmanager.""" + # 3.3.10. Customizing positional arguments in class pattern matching # https://docs.python.org/3/reference/datamodel.html#customizing-positional-arguments-in-class-pattern-matching @@ -459,20 +514,39 @@ def __ceil__(self) -> Y: ... # 3.3.11. Emulating buffer types # https://docs.python.org/3/reference/datamodel.html#emulating-buffer-types -# TODO: CanBuffer -# TODO: CanReleaseBuffer + +@runtime_checkable +class CanBuffer[B: int](Protocol): + def __buffer__(self, __b: B) -> memoryview: ... + +@runtime_checkable +class CanReleaseBuffer(Protocol): + def __release_buffer__(self, __v: memoryview) -> None: ... # 3.4.1. Awaitable Objects # https://docs.python.org/3/reference/datamodel.html#awaitable-objects -# TODO: CanAwait +# This should be `None | asyncio.Future[Any]`. But that would make this +# incompatible with `collections.abc.Awaitable`, because it (annoyingly) +# uses `Any`... +type _MaybeFuture = Any -# 3.4.2. Coroutine Objects -# https://docs.python.org/3/reference/datamodel.html#coroutine-objects -# TODO: .send(_), .throw(_), .throw(_, _), .throw(_, _, _), .close() ??? +@runtime_checkable +class CanAwait[V](Protocol): + # Technically speaking, this can return any + # `CanNext[None | asyncio.Future[Any]]`. But in theory, the return value + # of generators are currently impossible to type, because the return value + # of a `yield from _` is # piggybacked using a `raise StopIteration(value)` + # from `__next__`. So that also makes `__await__` theoretically + # impossible to type. In practice, typecheckers work around that, by + # accepting the lie called `collections.abc.Generator`... + @overload + def __await__(self: 'CanAwait[None]') -> CanNext[_MaybeFuture]: ... + @overload + def __await__(self: 'CanAwait[V]') -> Generator[_MaybeFuture, None, V]: ... # 3.4.3. Asynchronous Iterators @@ -493,3 +567,33 @@ def __aiter__(self) -> Y: ... # TODO: CanAenter # TODO: CanAexit # TODO: CanAsyncWith = CanAenter & CanAexit + + +# Module `abc` +# https://docs.python.org/3/library/abc.html +# TODO: CanSubclasshook + + +# Module `copy` +# https://docs.python.org/3/library/copy.html + +# TODO: CanCopy +# TODO: CanDeepCopy +# TODO: CanReplace (py313+) + + +# Module `pickle` +# https://docs.python.org/3/library/pickle.html#pickling-class-instances + +# TODO: CanGetnewargsEx +# TODO: CanGetnewargs +# TODO: CanGetstate +# TODO: CanSetstate +# TODO: CanReduce +# TODO: CanReduceEx + + +# Module `sys` +# https://docs.python.org/3/library/sys.html + +# TODO: CanSizeof From 4d3f22b742db3c0e122acc998ae984abcd6e7b62 Mon Sep 17 00:00:00 2001 From: jorenham Date: Fri, 23 Feb 2024 21:14:20 +0100 Subject: [PATCH 14/26] more canned goods, and one more has --- optype/__init__.py | 8 ++++++++ optype/_can.py | 43 +++++++++++++++++++++++++++++++------------ optype/_has.py | 38 ++++++++++++++++---------------------- 3 files changed, 55 insertions(+), 34 deletions(-) diff --git a/optype/__init__.py b/optype/__init__.py index 6092b6d8..2a89b306 100644 --- a/optype/__init__.py +++ b/optype/__init__.py @@ -1,9 +1,12 @@ __all__ = ( 'CanAbs', 'CanAdd', + 'CanAenter', + 'CanAexit', 'CanAiter', 'CanAnd', 'CanAnext', + 'CanAsyncWith', 'CanAwait', 'CanBool', 'CanBuffer', @@ -102,6 +105,7 @@ 'HasAnnotations', 'HasDict', 'HasDoc', + 'HasMatchArgs', 'HasModule', 'HasName', 'HasQualname', @@ -117,9 +121,12 @@ from ._can import ( CanAbs, CanAdd, + CanAenter, + CanAexit, CanAiter, CanAnd, CanAnext, + CanAsyncWith, CanAwait, CanBool, CanBuffer, @@ -220,6 +227,7 @@ HasAnnotations, HasDict, HasDoc, + HasMatchArgs, HasModule, HasName, HasQualname, diff --git a/optype/_can.py b/optype/_can.py index 78f42436..43a700b6 100644 --- a/optype/_can.py +++ b/optype/_can.py @@ -18,8 +18,8 @@ class CanNext[V](Protocol): def __next__(self) -> V: ... @runtime_checkable -class CanIter[Y: CanNext[Any]](Protocol): - def __iter__(self) -> Y: ... +class CanIter[Vs: CanNext[Any]](Protocol): + def __iter__(self) -> Vs: ... # 3.3.1. Basic customization @@ -29,7 +29,7 @@ def __iter__(self) -> Y: ... # TODO: __init__ @runtime_checkable -class CanDel[Y: str](Protocol): +class CanDel(Protocol): def __del__(self) -> Any: ... @runtime_checkable @@ -51,6 +51,7 @@ def __bytes__(self) -> Y: ... class CanFormat[X: str, Y: str](Protocol): def __format__(self, __x: X) -> Y: ... # type: ignore[override] + @runtime_checkable class CanLt[X, Y](Protocol): def __lt__(self, __x: X) -> Y: ... @@ -75,6 +76,7 @@ def __gt__(self, __x: X) -> Y: ... class CanGe[X, Y](Protocol): def __ge__(self, __x: X) -> Y: ... + @runtime_checkable class CanHash(Protocol): @override @@ -505,12 +507,6 @@ class CanWith[V](CanEnter[V], CanExit, Protocol): """Intersection type of `CanEnter[V] & CanExit`, i.e. a contextmanager.""" -# 3.3.10. Customizing positional arguments in class pattern matching -# https://docs.python.org/3/reference/datamodel.html#customizing-positional-arguments-in-class-pattern-matching - -# TODO: HasMatchArgs - - # 3.3.11. Emulating buffer types # https://docs.python.org/3/reference/datamodel.html#emulating-buffer-types @@ -564,9 +560,32 @@ def __aiter__(self) -> Y: ... # 3.4.4. Asynchronous Context Managers # https://docs.python.org/3/reference/datamodel.html#asynchronous-context-managers -# TODO: CanAenter -# TODO: CanAexit -# TODO: CanAsyncWith = CanAenter & CanAexit +@runtime_checkable +class CanAenter[V](Protocol): + def __aenter__(self) -> CanAwait[V]: ... + +@runtime_checkable +class CanAexit(Protocol): + @overload + def __aexit__( + self, __tp: None, + __ex: None, + __tb: None, + ) -> CanAwait[None]: ... + @overload + def __aexit__( + self, + __tp: type[BaseException], + __ex: BaseException, + __tb: TracebackType, + ) -> CanAwait[bool | None]: ... + +@runtime_checkable +class CanAsyncWith[V](CanAenter[V], CanAexit, Protocol): + """ + Intersection type of `CanAenter[V] & CanAexit`, i.e. an async + contextmanager. + """ # Module `abc` diff --git a/optype/_has.py b/optype/_has.py index 0d31c286..90f3062a 100644 --- a/optype/_has.py +++ b/optype/_has.py @@ -27,7 +27,7 @@ class _HasDocProp(Protocol): def __doc__(self) -> str | None: ... # type: ignore[override] -type HasDoc = _HasDocAttr | _HasDocProp +HasDoc = _HasDocAttr | _HasDocProp @runtime_checkable @@ -41,7 +41,7 @@ class _HasNameProp(Protocol): def __name__(self) -> str: ... -type HasName = _HasNameAttr | _HasNameProp +HasName = _HasNameAttr | _HasNameProp @runtime_checkable @@ -60,7 +60,7 @@ class _HasModuleProp(Protocol): def __module__(self) -> str: ... # type: ignore[override] -type HasModule = _HasModuleAttr | _HasModuleProp +HasModule = _HasModuleAttr | _HasModuleProp @runtime_checkable @@ -69,7 +69,19 @@ class HasAnnotations[V](Protocol): __annotations__: dict[str, V] -# weakref +@runtime_checkable +class HasMatchArgs[Ks: tuple[str, ...] | list[str]](Protocol): + __match_args__: Ks + + +# Module `dataclasses` +# https://docs.python.org/3/library/dataclasses.html + +# TODO: HasDataclassFields + + +# Module `weakref` +# https://docs.python.org/3/library/weakref.html @runtime_checkable class HasWeakReference(Protocol): @@ -91,21 +103,3 @@ class _HasWeakProxy(Protocol): type HasWeakProxy = HasWeakCallableProxy[..., Any] | _HasWeakProxy """An object referenced by a `weakref.proxy` (not the proxy itself).""" - - -# Unary operators -# TODO: type-cast methods, e.g. `__int__` -# TODO: strict int methods: `__len__`, `__index__`, `__hash__` -# TODO: strint str methods: `__repr__` - - -# Binary operators -# TODO; `__contains__` -# TODO: `class Cannot*: ...` variants that return `NotImplementedType` - - -# TODO: __getitem__ and __missing__ -# TODO: __getattr__ - -# TODO: __init_subclass__ ? -# TODO: __class_getitem__ ? From 28464cd28bd66c871f0bac4f4a204880b6b9ea10 Mon Sep 17 00:00:00 2001 From: jorenham Date: Fri, 23 Feb 2024 21:17:53 +0100 Subject: [PATCH 15/26] better docs, and some rants --- README.md | 320 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 222 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index 24df13bf..961a4c41 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@

optype

+

+ One protocol, one method. +

+

Building blocks for precise & flexible type hints.

@@ -33,7 +37,7 @@ Ruff + /> bool` | `bool(self)` | @@ -112,97 +116,184 @@ This is why these `optype` interfaces don't accept generic type arguments. | `CanBytes` | `__bytes__(self) -> bytes` | `bytes(self)` | | `CanStr` | `__str__(self) -> str` | `str(self)` | -**Other builtin functions:** -| Type | Signature | Expression | -| --------------- | ------------------------------ | ------------ | -| `CanRepr` | `__repr__(self) -> str` | `repr(self)` | -| `CanHash` | `__hash__(self) -> int` | `hash(self)` | -| `CanLen` | `__len__(self) -> int` | `len(self)` | -| `CanLengthHint` | `__length_hint__(self) -> int` | [docs](LH) | -| `CanIndex` | `__index__(self) -> int` | [docs](IX) | +These formatting methods are allowed to return instances that are a subtype +of the `str` builtin. The same holds for the `__format__` argument. +So if you're a 10x developer that wants to hack Python's f-strings, but only +if your type hints are spot-on; `optype` is you friend. - -[LH]: https://docs.python.org/3/reference/datamodel.html#object.__length_hint__ -[IX]: https://docs.python.org/3/reference/datamodel.html#object.__index__ +| Type | Signature | Expression | +| -------------------------- | ------------------------------| -------------- | +| `CanRepr[Y: str]` | `__repr__(self) -> T` | `repr(_)` | +| `CanFormat[X: str, Y: str]`| `__format__(self, x: X) -> Y` | `format(_, x)` | -#### Comparisons operators +#### "Rich comparison" operators -Generally these methods return a `bool`. But in theory, anything can be -returned (even if it doesn't implement `__bool__`). +These special methods generally a `bool`. However, instances of any type can +be returned. | Type | Signature | Expression | Expr. Reflected | | -------------- | -------------------------- | ----------- | --------------- | | `CanLt[X, Y]` | `__lt__(self, x: X) -> Y` | `self < x` | `x > self` | | `CanLe[X, Y]` | `__le__(self, x: X) -> Y` | `self <= x` | `x >= self` | -| `CanGe[X, Y]` | `__ge__(self, x: X) -> Y` | `self >= x` | `x <= self` | -| `CanGt[X, Y]` | `__gt__(self, x: X) -> Y` | `self > x` | `x < self` | | `CanEq[X, Y]` | `__eq__(self, x: X) -> Y` | `self == x` | `x == self` | | `CanNe[X, Y]` | `__ne__(self, x: X) -> Y` | `self != x` | `x != self` | +| `CanGt[X, Y]` | `__gt__(self, x: X) -> Y` | `self > x` | `x < self` | +| `CanGe[X, Y]` | `__ge__(self, x: X) -> Y` | `self >= x` | `x <= self` | -#### Rounding +#### Attribute access + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeSignatureExpression
CanGetattr[K: str, V]__getattr__(self, k: K) -> Vv = self.k or v = getattr(self, k)
CanGetattribute[K: str, V]__getattribute__(self, k: K) -> Vv = self.k or v = getattr(self, k)
CanSetattr[K: str, V]__setattr__(self, k: K, v: V)self.k = v or setattr(self, k, v)
CanDelattr[K: str]__delattr__(self, k: K)del self.k or delattr(self, k)
CanDir[Vs: CanIter[Any]]__dir__(self) -> Vsdir(self)
+ + +#### Descriptors + + +... -| Type | Signature | Expression | -| ------------------ | ----------------------------- | --------------------- | -| `CanRound1[Y1]` | `__round__(self) -> Y1` | `round(self)` | -| `CanRound2[N, Y2]` | `__round__(self, n: N) -> Y2` | `round(self, n: N)` | -For convenience, `optype` also provides their intersection type -`CanRound[N, Y1, Y2] =: CanRound1[Y1] & CanRound2[N, Y2]`, whose signature -overloads those of the `CanRound1` and `CanRound2`. +#### Customizing class creation -For instance, `float` is a `CanRound[int, int, float]` and `int` a -`CanRound[int, int, int]`. + +... -| Type | Signature | Expression | -| -------------- | ----------------------- | ------------------ | -| `CanTrunc[Y]` | `__trunc__(self) -> Y` | `math.trunc(self)` | -| `CanFloor[Y]` | `__floor__(self) -> Y` | `math.floor(self)` | -| `CanCeil[Y]` | `__ceil__(self) -> Y` | `math.ceil(self)` | -Note that the type parameter `Y` is unbounded, because technically these -methods can return any type. +#### Callable objects + + +... -#### Arithmetic and bitwise operators +#### Iteration -**Unary:** +The operand `x` of `iter(_)` is within Python known as an *iterable*, which is +what `collections.abc.Iterable[K]` is often used for (e.g. as base class, or +for instance checking). -| Type | Signature | Expression | -| -------------- | ----------------------- | ------------------ | -| `CanPos[Y]` | `__pos__(self) -> Y` | `+self` | -| `CanNeg[Y]` | `__neg__(self) -> Y` | `-self` | -| `CanInvert[Y]` | `__invert__(self) -> Y` | `~self` | -| `CanAbs[Y]` | `__abs__(self) -> Y` | `abs(self)` | - - -**Binary:** - -| Type | Signature | Expression | -| ------------------- | ------------------------------- | ----------------- | -| `CanAdd[X, Y]` | `__add__(self, x: X) -> Y` | `self + x` | -| `CanSub[X, Y]` | `__sub__(self, x: X) -> Y` | `self - x` | -| `CanMul[X, Y]` | `__mul__(self, x: X) -> Y` | `self * x` | -| `CanMatmul[X, Y]` | `__matmul__(self, x: X) -> Y` | `self @ x` | -| `CanTruediv[X, Y]` | `__truediv__(self, x: X) -> Y` | `self / x` | -| `CanFloordiv[X, Y]` | `__floordiv__(self, x: X) -> Y` | `self // x` | -| `CanMod[X, Y]` | `__mod__(self, x: X) -> Y` | `self % x` | -| `CanDivmod[X, Y]` | `__divmod__(self, x: X) -> Y` | `divmod(self, x)` | -| `CanPow2[X, Y]` | `__pow__(self, x: X) -> Y` | `self ** x` | -| `CanPow3[X, M, Y]` | `__pow__(self, x: X, m: M) -> Y`| `pow(self, x, m)` | -| `CanLshift[X, Y]` | `__lshift__(self, x: X) -> Y` | `self << x` | -| `CanRshift[X, Y]` | `__rshift__(self, x: X) -> Y` | `self >> x` | -| `CanAnd[X, Y]` | `__and__(self, x: X) -> Y` | `self & x` | -| `CanXor[X, Y]` | `__xor__(self, x: X) -> Y` | `self ^ x` | -| `CanOr[X, Y]` | `__or__(self, x: X) -> Y` | `self \| x` | +The `optype` analogue is `CanIter[Ks]`, which as the name suggests, +also implements `__iter__`. But unlike `Iterable[K]`, its type parameter `Ks` +binds to the return type of `iter(_)`. This makes it possible to annotate the +specific type of the *iterable* that `iter(_)` returns. `Iterable[K]` is only +able to annotate the type of the iterated value. To see why that isn't +possible, see [python/typing#548](https://github.com/python/typing/issues/548). + +The `collections.abc.Iterator[K]` is even more awkward; it is a subtype of +`Iterable[K]`. For those familiar with `collections.abc` this might come as a +surprise, but an iterator only needs to implement `__next__`, `__iter__` isn't +needed. This means that the `Iterator[K]` is unnecessarily restrictive. +Apart from that being theoretically "ugly", it has significant performance +implications, because the time-complexity of `isinstance` on a +`typing.Protocol` is $O(n)$, with the $n$ referring to the amount of members. +So even if the overhead of the inheritance and the `abc.ABC` usage is ignored, +`collections.abc.Iterator` is twice as slow as it needs to be. + +That's one of the (many) reasons that `optype.CanNext[V]` and +`optype.CanNext[V]` are the better alternatives to `Iterable` and `Iterator` +from the abracadabra collections. This is how they are defined: + +| Type | Signature | Expression | +| --------------------------- | ---------------------- | -------------- | +| `CanNext[V]` | `__next__(self) -> V` | `next(self)` | +| `CanIter[Vs: CanNext[Any]]` | `__iter__(self) -> Vs` | `iter(self)` | + + + + +#### Containers + + +| Type | Signature | Expression | +| --------------------- | ---------------------------------- | -------------- | +| `CanLen` | `__len__(self) -> int` | `len(self)` | +| `CanLengthHint` | `__length_hint__(self) -> int` | [docs](LH) | +| `CanGetitem[K, V]` | `__getitem__(self, k: K) -> V` | `self[k]` | +| `CanSetitem[K, V]` | `__setitem__(self, k: K, v: V)` | `self[k] = v` | +| `CanDelitem[K]` | `__delitem__(self, k: K)` | `del self[k]` | +| `CanMissing[K, V]` | `__missing__(self, k: K) -> V` | [docs](GM) | +| `CanReversed[Y]` [^4] | `__reversed__(self) -> Y` |`reversed(self)`| +| `CanContains[K]` | `__contains__(self, k: K) -> bool` | `x in self` | + + +For indexing or locating container values, the following special methods are +relevant: + +| Type | Signature | Expression | +| --------------- | ------------------------ | ------------ | +| `CanHash` | `__hash__(self) -> int` | `hash(self)` | +| `CanIndex` | `__index__(self) -> int` | [docs](IX) | + + +[^4]: Although not strictly required, `Y@CanReversed` should be a `CanNext`. +[LH]: https://docs.python.org/3/reference/datamodel.html#object.__length_hint__ +[GM]: https://docs.python.org/3/reference/datamodel.html#object.__missing__ +[IX]: https://docs.python.org/3/reference/datamodel.html#object.__index__ + + + +#### Numeric operations + +> [!TIP] +> If you're unfamiliar with customizing operators, check out the official +> [Python docs](NT). + +| Type | Signature | Expression | +| ------------------- | ------------------------------- | ------------------ | +| `CanAdd[X, Y]` | `__add__(self, x: X) -> Y` | `self + x` | +| `CanSub[X, Y]` | `__sub__(self, x: X) -> Y` | `self - x` | +| `CanMul[X, Y]` | `__mul__(self, x: X) -> Y` | `self * x` | +| `CanMatmul[X, Y]` | `__matmul__(self, x: X) -> Y` | `self @ x` | +| `CanTruediv[X, Y]` | `__truediv__(self, x: X) -> Y` | `self / x` | +| `CanFloordiv[X, Y]` | `__floordiv__(self, x: X) -> Y` | `self // x` | +| `CanMod[X, Y]` | `__mod__(self, x: X) -> Y` | `self % x` | +| `CanDivmod[X, Y]` | `__divmod__(self, x: X) -> Y` | `divmod(self, x)` | +| `CanPow2[X, Y]` | `__pow__(self, x: X) -> Y` | `self ** x` | +| `CanPow3[X, M, Y]` | `__pow__(self, x: X, m: M) -> Y`| `pow(self, x, m)` | +| `CanLshift[X, Y]` | `__lshift__(self, x: X) -> Y` | `self << x` | +| `CanRshift[X, Y]` | `__rshift__(self, x: X) -> Y` | `self >> x` | +| `CanAnd[X, Y]` | `__and__(self, x: X) -> Y` | `self & x` | +| `CanXor[X, Y]` | `__xor__(self, x: X) -> Y` | `self ^ x` | +| `CanOr[X, Y]` | `__or__(self, x: X) -> Y` | `self \| x` | Additionally, there is the intersection type -`CanPow[X, M, Y2, Y3] =: CanPow2[X, Y2] & CanPow3[X, M, Y3]`, whose signature -overloads those of the `CanPow2` and `CanPow3`. +`CanPow[X, M, Y2, Y3] =: CanPow2[X, Y2] & CanPow3[X, M, Y3]`, overloading +both `__pow__` method signatures. Note that the `2` and `3` suffixes refer +to the *arity* (#parameters) of the operators. -**Binary (reflected):** +For the binary infix operators above, `optype` additionally provides +interfaces with reflected (swapped) operands: | Type | Signature | Expression | | -------------------- | -------------------------------- | ----------------- | @@ -221,8 +312,11 @@ overloads those of the `CanPow2` and `CanPow3`. | `CanRXor[X, Y]` | `__rxor__(self, x: X) -> Y` | `x ^ self` | | `CanROr[X, Y]` | `__ror__(self, x: X) -> Y` | `x \| self` | +Note that `CanRPow` corresponds to `CanPow2`; the 3-parameter "modulo" `pow` +does not reflect in Python. -**Binary (augmented / in-place):** +Similarly, the augmented assignment operators are described by the following +`optype` interfaces: | Type | Signature | Expression | | -------------------- | -------------------------------- | ------------ | @@ -240,43 +334,73 @@ overloads those of the `CanPow2` and `CanPow3`. | `CanIXor[X, Y]` | `__ixor__(self, x: X) -> Y` | `self ^= x` | | `CanIOr[X, Y]` | `__ior__(self, x: X) -> Y` | `self \|= x` | - +Additionally, there are the unary arithmetic operators: +| Type | Signature | Expression | +| ------------------- | ------------------------------- | ------------------ | +| `CanPos[Y]` | `__pos__(self) -> Y` | `+self` | +| `CanNeg[Y]` | `__neg__(self) -> Y` | `-self` | +| `CanInvert[Y]` | `__invert__(self) -> Y` | `~self` | +| `CanAbs[Y]` | `__abs__(self) -> Y` | `abs(self)` | -### Containers -| Type | Signature | Expression | -| ------------------ | -------------------------------------- | ------------- | -| `CanContains[K]` | `__contains__(self, k: K) -> bool` | `x in self` | -| `CanDelitem[K]` | `__delitem__(self, k: K) -> None` | `del self[k]` | -| `CanGetitem[K, V]` | `__getitem__(self, k: K) -> V` | `self[k]` | -| `CanMissing[K, V]` | `__missing__(self, k: K) -> V` | [docs](GM) | -| `CanSetitem[K, V]` | `__setitem__(self, k: K, v: V) -> None`| `self[k] = v` | +The `round` function comes in two flavors: +| Type | Signature | Expression | +| ------------------ | ----------------------------- | --------------------- | +| `CanRound1[Y1]` | `__round__(self) -> Y1` | `round(self)` | +| `CanRound2[N, Y2]` | `__round__(self, n: N) -> Y2` | `round(self, n: N)` | -[GM]: https://docs.python.org/3/reference/datamodel.html#object.__missing__ +For convenience, `optype` also provides their intersection type +`CanRound[N, Y1, Y2] =: CanRound1[Y1] & CanRound2[N, Y2]`, whose signature +overloads those of the `CanRound1` and `CanRound2`. +To illustrate; `float` is a `CanRound[int, int, float]` and `int` a +`CanRound[int, int, int]`. + +And finally, the remaining rounding functions: + +| Type | Signature | Expression | +| -------------- | ----------------------- | ------------------ | +| `CanTrunc[Y]` | `__trunc__(self) -> Y` | `math.trunc(self)` | +| `CanFloor[Y]` | `__floor__(self) -> Y` | `math.floor(self)` | +| `CanCeil[Y]` | `__ceil__(self) -> Y` | `math.ceil(self)` | + +Note that the type parameter `Y` is unbounded, because technically these +methods can return any type. + + +[NT]: https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types -### Iteration +#### Async objects -**Sync** +| Type | Signature | Expression | +| ------------- | -------------------------------------------- | ------------ | +| `CanAwait[V]` | `__await__(self) -> Generator[Any, None, V]` | `await self` | -| Type | Signature | Expression | -| -------------------------- | ------------------------- | ---------------- | -| `CanNext[V]` | `__next__(self) -> V` | `next(self)` | -| `CanIter[Y: CanNext[Any]]` | `__iter__(self) -> Y` | `iter(self)` | -| `CanReversed[Y]` (*) | `__reversed__(self) -> Y` | `reversed(self)` | -(*) Although not strictly required, `Y@CanReversed` should be iterable. +#### Async Iteration -**Async** +Yes, you guessed it right; the abracadabra collections repeated their mistakes +with their async iterablors (or something like that). +But fret not, the `optype` alternatives are right here: -| Type | Signature | Expression | -| ---------------------------- | ----------------------- | ---------------- | -| `CanAnext[V]` (**) | `__anext__(self) -> V` | `anext(self)` | -| `CanAiter[Y: CanAnext[Any]]` | `__aiter__(self) -> Y` | `aiter(self)` | +| `CanAnext[V]` | `__anext__(self) -> V` | `anext(self)` | +| `CanAiter[Vs: CanAnext]` | `__aiter__(self) -> Y` | `aiter(self)` | -(**) Although not strictly required, `V@CanAnext` should be an `Awaitable`. + +But wait, shouldn't `V` be `Awaitable`? Well, only if you don't want to get +fired... +Technically speaking, `__anext__` can return any type, and `anext` will pass +it along without nagging (instance checks are slow, now stop bothering that +liberal). +Just because something is legal, doesn't mean it's a good idea (don't eat the +yellow snow). + +#### Async context managers + + +... ### Generic interfaces for builtins From 0ed8bb81b66043da954175e8149e94431cca73c0 Mon Sep 17 00:00:00 2001 From: jorenham Date: Sat, 24 Feb 2024 15:56:32 +0100 Subject: [PATCH 16/26] remove the broken `optype.Slice` --- optype/__init__.py | 2 -- optype/_slice.py | 45 --------------------------------------------- 2 files changed, 47 deletions(-) delete mode 100644 optype/_slice.py diff --git a/optype/__init__.py b/optype/__init__.py index 2a89b306..323fbfc4 100644 --- a/optype/__init__.py +++ b/optype/__init__.py @@ -112,7 +112,6 @@ 'HasWeakCallableProxy', 'HasWeakProxy', 'HasWeakReference', - 'Slice', '__version__', ) @@ -235,7 +234,6 @@ HasWeakProxy, HasWeakReference, ) -from ._slice import Slice __version__: str = _metadata.version(__package__ or __file__.split('/')[-1]) diff --git a/optype/_slice.py b/optype/_slice.py deleted file mode 100644 index 0a00c58e..00000000 --- a/optype/_slice.py +++ /dev/null @@ -1,45 +0,0 @@ -"""But a dream of a generic `slice`.""" -from __future__ import annotations - -import typing as _tp - - -if _tp.TYPE_CHECKING: - import types as _ts - - from ._can import CanIndex as _CanIndex - - -@_tp.final -@_tp.runtime_checkable -class Slice[A, B, S](_tp.Protocol): - start: _tp.Final[A] - stop: _tp.Final[B] - step: _tp.Final[S] - - @_tp.overload - def __new__(cls, __stop: B) -> Slice[None, B, None]: ... - @_tp.overload - def __new__(cls, __start: A, __stop: B) -> Slice[A, B, None]: ... - @_tp.overload - def __new__(cls, __start: A, __stop: B, __step: S) -> Slice[A, B, S]: ... - @_tp.override - def __eq__(self, __other: object) -> bool: ... - @_tp.override - def __ne__(self, __other: object) -> bool: ... - - def __lt__(self, __other: object) -> bool | _ts.NotImplementedType: ... - - def __le__(self, __other: object) -> bool | _ts.NotImplementedType: ... - - def __ge__(self, __other: object) -> bool | _ts.NotImplementedType: ... - - def __gt__(self, __other: object) -> bool | _ts.NotImplementedType: ... - - @_tp.overload - def indices( - self: Slice[_CanIndex | None, _CanIndex | None, _CanIndex | None], - __len: _CanIndex, - ) -> tuple[int, int, int]: ... - @_tp.overload - def indices(self: Slice[A, B, S], __len: _CanIndex) -> _tp.NoReturn: ... From 8007fb3660be1e13912e5b79188bdbbe418387e9 Mon Sep 17 00:00:00 2001 From: jorenham Date: Sat, 24 Feb 2024 16:08:14 +0100 Subject: [PATCH 17/26] improved context manager interfaces --- optype/_can.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/optype/_can.py b/optype/_can.py index 43a700b6..8ac51108 100644 --- a/optype/_can.py +++ b/optype/_can.py @@ -491,19 +491,19 @@ class CanEnter[V](Protocol): def __enter__(self) -> V: ... @runtime_checkable -class CanExit(Protocol): +class CanExit[R](Protocol): @overload def __exit__(self, __tp: None, __ex: None, __tb: None) -> None: ... @overload - def __exit__( + def __exit__[E: BaseException]( self, - __tp: type[BaseException], - __ex: BaseException, + __tp: type[E], + __ex: E, __tb: TracebackType, - ) -> None: ... + ) -> R: ... @runtime_checkable -class CanWith[V](CanEnter[V], CanExit, Protocol): +class CanWith[V, R](CanEnter[V], CanExit[R], Protocol): """Intersection type of `CanEnter[V] & CanExit`, i.e. a contextmanager.""" @@ -565,23 +565,24 @@ class CanAenter[V](Protocol): def __aenter__(self) -> CanAwait[V]: ... @runtime_checkable -class CanAexit(Protocol): +class CanAexit[R](Protocol): @overload def __aexit__( - self, __tp: None, + self, + __tp: None, __ex: None, __tb: None, ) -> CanAwait[None]: ... @overload - def __aexit__( + def __aexit__[E: BaseException]( self, - __tp: type[BaseException], - __ex: BaseException, + __tp: type[E], + __ex: E, __tb: TracebackType, - ) -> CanAwait[bool | None]: ... + ) -> CanAwait[R]: ... @runtime_checkable -class CanAsyncWith[V](CanAenter[V], CanAexit, Protocol): +class CanAsyncWith[V, R](CanAenter[V], CanAexit[R], Protocol): """ Intersection type of `CanAenter[V] & CanAexit`, i.e. an async contextmanager. From e1a71db8f539cc4cb0b2aeb4727af2d142ba8bb6 Mon Sep 17 00:00:00 2001 From: jorenham Date: Sat, 24 Feb 2024 16:09:03 +0100 Subject: [PATCH 18/26] cleaner and more complete docs --- README.md | 182 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 96 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index 961a4c41..6cb61a95 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,6 @@ --- -> [!WARNING] -> The API is not stable; use it at your own risk. - - ## Installation Optype is available as [`optype`](OPTYPE) on PyPI: @@ -90,18 +86,7 @@ type stubs. [RC]: https://typing.readthedocs.io/en/latest/spec/protocol.html#runtime-checkable-decorator-and-narrowing-types-by-isinstance -### Elementary typing protocols - -A collection of `optype` single-method [`typing.Protocol`](PC) interfaces for -most of the "special methods", also known as "dunder" (double underscore) -methods. - -See the [Python docs](SM) for more info. - -[SM]: https://docs.python.org/3/reference/datamodel.html#special-method-names - - -#### Type conversion +### Type conversion The return type of these special methods is *invariant*. Python will raise an error if some other (sub)type is returned. @@ -128,7 +113,7 @@ if your type hints are spot-on; `optype` is you friend. | `CanFormat[X: str, Y: str]`| `__format__(self, x: X) -> Y` | `format(_, x)` | -#### "Rich comparison" operators +### "Rich comparison" operators These special methods generally a `bool`. However, instances of any type can be returned. @@ -143,7 +128,7 @@ be returned. | `CanGe[X, Y]` | `__ge__(self, x: X) -> Y` | `self >= x` | `x <= self` | -#### Attribute access +### Attribute access @@ -179,25 +164,7 @@ be returned.
-#### Descriptors - - -... - - -#### Customizing class creation - - -... - - -#### Callable objects - - -... - - -#### Iteration +### Iteration The operand `x` of `iter(_)` is within Python known as an *iterable*, which is what `collections.abc.Iterable[K]` is often used for (e.g. as base class, or @@ -230,9 +197,7 @@ from the abracadabra collections. This is how they are defined: | `CanIter[Vs: CanNext[Any]]` | `__iter__(self) -> Vs` | `iter(self)` | - - -#### Containers +### Containers | Type | Signature | Expression | @@ -262,30 +227,47 @@ relevant: [IX]: https://docs.python.org/3/reference/datamodel.html#object.__index__ +### Descriptors + + +... + + +### Customizing class creation + + +... + + +### Callable objects + + +... -#### Numeric operations + +### Numeric operations > [!TIP] > If you're unfamiliar with customizing operators, check out the official > [Python docs](NT). -| Type | Signature | Expression | -| ------------------- | ------------------------------- | ------------------ | -| `CanAdd[X, Y]` | `__add__(self, x: X) -> Y` | `self + x` | -| `CanSub[X, Y]` | `__sub__(self, x: X) -> Y` | `self - x` | -| `CanMul[X, Y]` | `__mul__(self, x: X) -> Y` | `self * x` | -| `CanMatmul[X, Y]` | `__matmul__(self, x: X) -> Y` | `self @ x` | -| `CanTruediv[X, Y]` | `__truediv__(self, x: X) -> Y` | `self / x` | -| `CanFloordiv[X, Y]` | `__floordiv__(self, x: X) -> Y` | `self // x` | -| `CanMod[X, Y]` | `__mod__(self, x: X) -> Y` | `self % x` | -| `CanDivmod[X, Y]` | `__divmod__(self, x: X) -> Y` | `divmod(self, x)` | -| `CanPow2[X, Y]` | `__pow__(self, x: X) -> Y` | `self ** x` | -| `CanPow3[X, M, Y]` | `__pow__(self, x: X, m: M) -> Y`| `pow(self, x, m)` | -| `CanLshift[X, Y]` | `__lshift__(self, x: X) -> Y` | `self << x` | -| `CanRshift[X, Y]` | `__rshift__(self, x: X) -> Y` | `self >> x` | -| `CanAnd[X, Y]` | `__and__(self, x: X) -> Y` | `self & x` | -| `CanXor[X, Y]` | `__xor__(self, x: X) -> Y` | `self ^ x` | -| `CanOr[X, Y]` | `__or__(self, x: X) -> Y` | `self \| x` | +| Type | Signature | Expression | +| ------------------- | -------------------------------- | ------------------ | +| `CanAdd[X, Y]` | `__add__(self, x: X) -> Y` | `self + x` | +| `CanSub[X, Y]` | `__sub__(self, x: X) -> Y` | `self - x` | +| `CanMul[X, Y]` | `__mul__(self, x: X) -> Y` | `self * x` | +| `CanMatmul[X, Y]` | `__matmul__(self, x: X) -> Y` | `self @ x` | +| `CanTruediv[X, Y]` | `__truediv__(self, x: X) -> Y` | `self / x` | +| `CanFloordiv[X, Y]` | `__floordiv__(self, x: X) -> Y` | `self // x` | +| `CanMod[X, Y]` | `__mod__(self, x: X) -> Y` | `self % x` | +| `CanDivmod[X, Y]` | `__divmod__(self, x: X) -> Y` | `divmod(self, x)` | +| `CanPow2[X, Y]` | `__pow__(self, x: X) -> Y` | `self ** x` | +| `CanPow3[X, M, Y]` | `__pow__(self, x: X, m: M) -> Y` | `pow(self, x, m)` | +| `CanLshift[X, Y]` | `__lshift__(self, x: X) -> Y` | `self << x` | +| `CanRshift[X, Y]` | `__rshift__(self, x: X) -> Y` | `self >> x` | +| `CanAnd[X, Y]` | `__and__(self, x: X) -> Y` | `self & x` | +| `CanXor[X, Y]` | `__xor__(self, x: X) -> Y` | `self ^ x` | +| `CanOr[X, Y]` | `__or__(self, x: X) -> Y` | `self \| x` | Additionally, there is the intersection type `CanPow[X, M, Y2, Y3] =: CanPow2[X, Y2] & CanPow3[X, M, Y3]`, overloading @@ -343,7 +325,6 @@ Additionally, there are the unary arithmetic operators: | `CanInvert[Y]` | `__invert__(self) -> Y` | `~self` | | `CanAbs[Y]` | `__abs__(self) -> Y` | `abs(self)` | - The `round` function comes in two flavors: | Type | Signature | Expression | @@ -368,28 +349,62 @@ And finally, the remaining rounding functions: Note that the type parameter `Y` is unbounded, because technically these methods can return any type. - [NT]: https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types -#### Async objects +### Context managers + +Support for the `with` statement. + +| Type | Signature | +| -------------- | ------------------------------------------ | +| `CanEnter[V]` | `__enter__(self) -> V` | +| `CanExit[R]` | `__exit__(self, *exc_info: *ExcInfo) -> R` | + +In case of errors, the type alias `ExcInfo` will be +`tuple[type[E], E, types.TracebackType]`, where `E` is some `BaseException`. +On the other hand, if no errors are raised (without being silenced), +then `Excinfo` will be `None` in triplicate. + +Because everyone that enters must also leave (that means you too, Barry), +`optype` provides the *intersection type* +`CanWith[V, R] = CanEnter[V] & CanExit[R]`. +If you're thinking of an insect-themed sect right now, that's ok -- +intersection types aren't real (yet..?). +To put your mind at ease, here's how it's implemented: + +```python +class CanWith[V, R](CanEnter[V], CanExit[R]): + # You won't find any bugs here :) + ... +``` + + +### Async objects + +The `optype` variant of `collections.abc.Awaitable[V]`. The only difference +is that `optype.CanAwait[V]` is a pure interface, whereas `Awaitable` is +also an abstract base class. | Type | Signature | Expression | | ------------- | -------------------------------------------- | ------------ | | `CanAwait[V]` | `__await__(self) -> Generator[Any, None, V]` | `await self` | -#### Async Iteration +### Async Iteration Yes, you guessed it right; the abracadabra collections repeated their mistakes with their async iterablors (or something like that). + But fret not, the `optype` alternatives are right here: +| Type | Signature | Expression | +| ------------------------ | ---------------------- | ------------- | | `CanAnext[V]` | `__anext__(self) -> V` | `anext(self)` | | `CanAiter[Vs: CanAnext]` | `__aiter__(self) -> Y` | `aiter(self)` | -But wait, shouldn't `V` be `Awaitable`? Well, only if you don't want to get +But wait, shouldn't `V` be a `CanAwait`? Well, only if you don't want to get fired... Technically speaking, `__anext__` can return any type, and `anext` will pass it along without nagging (instance checks are slow, now stop bothering that @@ -397,37 +412,32 @@ liberal). Just because something is legal, doesn't mean it's a good idea (don't eat the yellow snow). -#### Async context managers - - -... +### Async context managers -### Generic interfaces for builtins +Support for the `async with` statement. -#### `optype.Slice[A, B, S]` +| Type | Signature | +| -------------- | ---------------------------------------------------------- | +| `CanAenter[V]` | `__aenter__(self) -> CanAwait[V]` | +| `CanAexit[R]` | `__aexit__(self, *exc_info: *ExcInfo) -> CanAwait[R]` | -A generic interface of the builin -[`slice`](https://docs.python.org/3/library/functions.html#slice) object. +And just like `CanWith[V, R]` for sync [context managers](#context-managers), +there is the `CanAsyncWith[V, R] = CanAenter[V] & CanAexit[R]` intersection +type. -**Signatures**: +### `weakref` -- `(B) -> Slice[None, B, None]` -- `(A, B) -> Slice[A, B, None]` -- `(A, B, S) -> Slice[A, B, S]` - -these are valid for the `slice(start?, stop, step?)` constructor, -and for the extended indexing syntax `_[start? : stop? : step?]` (the `?` -denotes an optional parameter). + +... -**Decorators**: -- `@typing.runtime_checkable` -- `@typing.final` ## Future plans -- Build a replacement for the `operator` standard library, with - runtime-accessible type annotations -- Protocols for numpy's dunder methods -- Backport to Python 3.11 and 3.10 +- Build a drop-in replacement for the `operator` standard library, with + runtime-accessible type annotations, and more operators. +- More standard library protocols, e.g. `copy`, `dataclasses`, `pickle`. +- Dependency-free third-party type support, e.g. protocols for `numpy`'s array + interface. +- Support for Python versions before 3.12. From 7e6b2ed7191501edfba06c156af3ce558c4da098 Mon Sep 17 00:00:00 2001 From: jorenham Date: Sat, 24 Feb 2024 17:28:10 +0100 Subject: [PATCH 19/26] removed broken weakref protocols --- optype/_has.py | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/optype/_has.py b/optype/_has.py index 90f3062a..65fabc5f 100644 --- a/optype/_has.py +++ b/optype/_has.py @@ -2,11 +2,7 @@ """ Elementary interfaces for special "dunder" attributes. """ -from typing import TYPE_CHECKING, Any, Protocol, Self, runtime_checkable - - -if TYPE_CHECKING: - import weakref as _weakref +from typing import Protocol, runtime_checkable # special attributes @@ -78,28 +74,3 @@ class HasMatchArgs[Ks: tuple[str, ...] | list[str]](Protocol): # https://docs.python.org/3/library/dataclasses.html # TODO: HasDataclassFields - - -# Module `weakref` -# https://docs.python.org/3/library/weakref.html - -@runtime_checkable -class HasWeakReference(Protocol): - """An object referenced by a `weakref.ReferenceType[Self]`.""" - __weakref__: '_weakref.ReferenceType[Self]' - - -@runtime_checkable -class HasWeakCallableProxy[**Xs, Y](Protocol): - """A callable referenced by a `weakref.CallableProxyType[Self]`.""" - __weakref__: '_weakref.CallableProxyType[Self]' - - def __call__(self, *__args: Xs.args, **__kwargs: Xs.kwargs) -> Y: ... - -@runtime_checkable -class _HasWeakProxy(Protocol): - __weakref__: '_weakref.ProxyType[Self]' - - -type HasWeakProxy = HasWeakCallableProxy[..., Any] | _HasWeakProxy -"""An object referenced by a `weakref.proxy` (not the proxy itself).""" From e44e0502b2852cb36c6ee5d473b47c9a2cd9a468 Mon Sep 17 00:00:00 2001 From: jorenham Date: Sat, 24 Feb 2024 17:28:25 +0100 Subject: [PATCH 20/26] simplified `CanGet` --- optype/_can.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/optype/_can.py b/optype/_can.py index 8ac51108..f0e535d7 100644 --- a/optype/_can.py +++ b/optype/_can.py @@ -121,22 +121,18 @@ def __dir__(self) -> Vs: ... # https://docs.python.org/3/reference/datamodel.html#implementing-descriptors @runtime_checkable -class CanGetBound[T, V](Protocol): - def __get__(self, __obj: T, __cls: type[T] | None = ...) -> V: ... - -@runtime_checkable -class CanGet[T, U, V](CanGetBound[T, V], Protocol): +class CanGet[T, U, V](Protocol): @overload def __get__(self, __obj: None, __cls: type[T]) -> U: ... @overload def __get__(self, __obj: T, __cls: type[T] | None = ...) -> V: ... @runtime_checkable -class CanSet[T, V](Protocol): +class CanSet[T: object, V](Protocol): def __set__(self, __obj: T, __v: V) -> Any: ... @runtime_checkable -class CanDelete[T](Protocol): +class CanDelete[T: object](Protocol): def __delete__(self, __obj: T) -> Any: ... # TODO: HasObjclass @@ -200,7 +196,7 @@ def __set_name__(self, __cls: type[T], __name: str) -> Any: ... @runtime_checkable class CanCall[**Xs, Y](Protocol): - def __call__(self, *__xa: Xs.args, **__xk: Xs.kwargs) -> Y: ... + def __call__(self, *__xs: Xs.args, **__kxs: Xs.kwargs) -> Y: ... # 3.3.7. Emulating container types From 4de6042eea6bf6caf2c5daa679b0d0622f6bd8f3 Mon Sep 17 00:00:00 2001 From: jorenham Date: Sat, 24 Feb 2024 17:30:49 +0100 Subject: [PATCH 21/26] document callables and descriptors, and some cleanup --- README.md | 82 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 6cb61a95..d5da6ba2 100644 --- a/README.md +++ b/README.md @@ -191,10 +191,10 @@ That's one of the (many) reasons that `optype.CanNext[V]` and `optype.CanNext[V]` are the better alternatives to `Iterable` and `Iterator` from the abracadabra collections. This is how they are defined: -| Type | Signature | Expression | -| --------------------------- | ---------------------- | -------------- | -| `CanNext[V]` | `__next__(self) -> V` | `next(self)` | -| `CanIter[Vs: CanNext[Any]]` | `__iter__(self) -> Vs` | `iter(self)` | +| Type | Signature | Expression | +| --------------------------- | ---------------------- | ------------ | +| `CanNext[V]` | `__next__(self) -> V` | `next(self)` | +| `CanIter[Vs: CanNext[Any]]` | `__iter__(self) -> Vs` | `iter(self)` | ### Containers @@ -215,10 +215,10 @@ from the abracadabra collections. This is how they are defined: For indexing or locating container values, the following special methods are relevant: -| Type | Signature | Expression | -| --------------- | ------------------------ | ------------ | -| `CanHash` | `__hash__(self) -> int` | `hash(self)` | -| `CanIndex` | `__index__(self) -> int` | [docs](IX) | +| Type | Signature | Expression | +| ---------- | ------------------------ | ------------ | +| `CanHash` | `__hash__(self) -> int` | `hash(self)` | +| `CanIndex` | `__index__(self) -> int` | [docs](IX) | [^4]: Although not strictly required, `Y@CanReversed` should be a `CanNext`. @@ -229,27 +229,65 @@ relevant: ### Descriptors - -... - +Interfaces for [descriptors](https://docs.python.org/3/howto/descriptor.html). -### Customizing class creation + + + + + + + + + + + + + + + + + + + + + +
TypeSignature
CanGet[T: object, U, V] + __get__(self, obj: None, cls: type[T]) -> U
+ __get__(self, obj: T, cls: type[T] | None = ...) -> V +
CanSet[T: object, V]__set__(self, obj: T, v: V) -> Any
CanDelete[T: object]__delete__(self, obj: T) -> Any
CanSetName[T: object] + __set_name__(self, cls: type[T], name: str) -> Any +
- + + ... ### Callable objects - -... +Like `collections.abc.Callable`, but without esoteric hacks. + + + + + + + + + + + + +
TypeSignatureExpression
CanCall[**Xs, Y] + __call__(self, *xs: Xs.args, **kxs: Xs.kwargs) -> Y + self(*xs, **kxs)
### Numeric operations -> [!TIP] -> If you're unfamiliar with customizing operators, check out the official -> [Python docs](NT). +For describing things that act like numbers. See the [Python docs](NT) for more +info. | Type | Signature | Expression | | ------------------- | -------------------------------- | ------------------ | @@ -270,7 +308,7 @@ relevant: | `CanOr[X, Y]` | `__or__(self, x: X) -> Y` | `self \| x` | Additionally, there is the intersection type -`CanPow[X, M, Y2, Y3] =: CanPow2[X, Y2] & CanPow3[X, M, Y3]`, overloading +`CanPow[X, M, Y2, Y3] =: CanPow2[X, Y2] & CanPow3[X, M, Y3]`, that overloads both `__pow__` method signatures. Note that the `2` and `3` suffixes refer to the *arity* (#parameters) of the operators. @@ -426,12 +464,6 @@ And just like `CanWith[V, R]` for sync [context managers](#context-managers), there is the `CanAsyncWith[V, R] = CanAenter[V] & CanAexit[R]` intersection type. -### `weakref` - - -... - - ## Future plans From f7ad8f42d4236cd40fe6031ba5cac5188c80e988 Mon Sep 17 00:00:00 2001 From: jorenham Date: Sat, 24 Feb 2024 22:00:26 +0100 Subject: [PATCH 22/26] remove some useless `Has*` types --- optype/_has.py | 80 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 27 deletions(-) diff --git a/optype/_has.py b/optype/_has.py index 65fabc5f..8ebb4584 100644 --- a/optype/_has.py +++ b/optype/_has.py @@ -2,72 +2,98 @@ """ Elementary interfaces for special "dunder" attributes. """ -from typing import Protocol, runtime_checkable +from types import CodeType, ModuleType +from typing import Any, Protocol, Self, override, runtime_checkable +from ._can import CanCall, CanIter -# special attributes + +# Instances @runtime_checkable -class HasDict[V](Protocol): - __dict__: dict[str, V] +class HasMatchArgs[Ks: tuple[str, ...] | list[str]](Protocol): + __match_args__: Ks @runtime_checkable -class _HasDocAttr(Protocol): - __doc__: str | None +class HasSlots[S: str | CanIter[Any]](Protocol): + __slots__: S + + +@runtime_checkable +class HasDict[V](Protocol): + __dict__: dict[str, V] @runtime_checkable -class _HasDocProp(Protocol): +class HasClass(Protocol): @property - def __doc__(self) -> str | None: ... # type: ignore[override] + @override + def __class__(self) -> type[Self]: ... + @__class__.setter + def __class__(self, __cls: type[Self]) -> None: + """Don't.""" + +@runtime_checkable +class HasModule(Protocol): + __module__: str -HasDoc = _HasDocAttr | _HasDocProp +# __name__ and __qualname__ generally are a package deal @runtime_checkable -class _HasNameAttr(Protocol): +class HasName(Protocol): __name__: str @runtime_checkable -class _HasNameProp(Protocol): - @property - def __name__(self) -> str: ... +class HasQualname(Protocol): + __qualname__: str -HasName = _HasNameAttr | _HasNameProp +@runtime_checkable +class HasNames(HasName, HasQualname, Protocol): ... +# docs and type hints + @runtime_checkable -class HasQualname(Protocol): - __qualname__: str +class HasDoc(Protocol): + __doc__: str | None @runtime_checkable -class _HasModuleAttr(Protocol): - __module__: str +class HasAnnotations[V](Protocol): + __annotations__: dict[str, V] @runtime_checkable -class _HasModuleProp(Protocol): - @property - def __module__(self) -> str: ... # type: ignore[override] +class HasTypeParams[*Ps](Protocol): + # Note that `*Ps: (TypeVar, ParamSpec, TypeVarTuple)` should hold + __type_params__: tuple[*Ps] -HasModule = _HasModuleAttr | _HasModuleProp +# functions and methods + +@runtime_checkable +class HasFunc[**Xs, Y](Protocol): + __func__: CanCall[Xs, Y] @runtime_checkable -class HasAnnotations[V](Protocol): - """Note that the `V` type is hard to accurately define; blame PEP 563.""" - __annotations__: dict[str, V] +class HasWrapped[**Xs, Y](Protocol): + __wrapped__: CanCall[Xs, Y] @runtime_checkable -class HasMatchArgs[Ks: tuple[str, ...] | list[str]](Protocol): - __match_args__: Ks +class HasSelf[T: object | ModuleType]: + @property + def __self__(self) -> T: ... + +@runtime_checkable +class HasCode(Protocol): + __code__: CodeType # Module `dataclasses` From 204d1ef25228dd36ef2a00fc1a529bafe1451127 Mon Sep 17 00:00:00 2001 From: jorenham Date: Sat, 24 Feb 2024 22:00:43 +0100 Subject: [PATCH 23/26] Added more `Can` types --- optype/__init__.py | 28 ++++++++++++++++------------ optype/_can.py | 41 ++++++++++++++++------------------------- 2 files changed, 32 insertions(+), 37 deletions(-) diff --git a/optype/__init__.py b/optype/__init__.py index 323fbfc4..cb5829ae 100644 --- a/optype/__init__.py +++ b/optype/__init__.py @@ -15,7 +15,6 @@ 'CanCeil', 'CanComplex', 'CanContains', - 'CanDel', 'CanDelattr', 'CanDelete', 'CanDelitem', @@ -30,7 +29,6 @@ 'CanFormat', 'CanGe', 'CanGet', - 'CanGetBound', 'CanGetattr', 'CanGetattribute', 'CanGetitem', @@ -50,7 +48,6 @@ 'CanITruediv', 'CanIXor', 'CanIndex', - 'CanInitSubclass', 'CanInt', 'CanInvert', 'CanIter', @@ -103,15 +100,20 @@ 'CanWith', 'CanXor', 'HasAnnotations', + 'HasClass', + 'HasCode', 'HasDict', 'HasDoc', + 'HasFunc', 'HasMatchArgs', 'HasModule', 'HasName', + 'HasNames', 'HasQualname', - 'HasWeakCallableProxy', - 'HasWeakProxy', - 'HasWeakReference', + 'HasSelf', + 'HasSlots', + 'HasTypeParams', + 'HasWrapped', '__version__', ) @@ -134,7 +136,6 @@ CanCeil, CanComplex, CanContains, - CanDel, CanDelattr, CanDelete, CanDelitem, @@ -149,7 +150,6 @@ CanFormat, CanGe, CanGet, - CanGetBound, CanGetattr, CanGetattribute, CanGetitem, @@ -169,7 +169,6 @@ CanITruediv, CanIXor, CanIndex, - CanInitSubclass, CanInt, CanInvert, CanIter, @@ -224,15 +223,20 @@ ) from ._has import ( HasAnnotations, + HasClass, + HasCode, HasDict, HasDoc, + HasFunc, HasMatchArgs, HasModule, HasName, + HasNames, HasQualname, - HasWeakCallableProxy, - HasWeakProxy, - HasWeakReference, + HasSelf, + HasSlots, + HasTypeParams, + HasWrapped, ) diff --git a/optype/_can.py b/optype/_can.py index f0e535d7..1344191d 100644 --- a/optype/_can.py +++ b/optype/_can.py @@ -1,13 +1,7 @@ # ruff: noqa: PYI034 -from collections.abc import Generator +from collections.abc import Generator # sadge :( from types import TracebackType -from typing import ( - Any, - Protocol, - overload, - override, - runtime_checkable, -) +from typing import Any, Protocol, overload, override, runtime_checkable # Iterator types @@ -25,12 +19,9 @@ def __iter__(self) -> Vs: ... # 3.3.1. Basic customization # https://docs.python.org/3/reference/datamodel.html#basic-customization -# TODO: __new__ -# TODO: __init__ - -@runtime_checkable -class CanDel(Protocol): - def __del__(self) -> Any: ... +# @runtime_checkable +# class CanDel(Protocol): +# def __del__(self) -> Any: ... @runtime_checkable class CanRepr[Y: str](Protocol): @@ -146,16 +137,16 @@ def __delete__(self, __obj: T) -> Any: ... # 3.3.3. Customizing class creation # https://docs.python.org/3/reference/datamodel.html#customizing-class-creation -@runtime_checkable -class CanInitSubclass[**Ps](Protocol): - # no positional-only are allowed - # cannot `Unpack` type args: https://github.com/python/typing/issues/1399 - # workaround: use `**Ps` instead of `Ps: TypedDict` - def __init_subclass__( - cls, - *__dont_use_these_args: Ps.args, - **__kwargs: Ps.kwargs, - ) -> Any: ... +# @runtime_checkable +# class CanInitSubclass[**Ps](Protocol): +# # no positional-only are allowed +# # cannot `Unpack` type args: https://github.com/python/typing/issues/1399 +# # workaround: use `**Ps` instead of `Ps: TypedDict` +# def __init_subclass__( +# cls, +# *__dont_use_these_args: Ps.args, +# **__kwargs: Ps.kwargs, +# ) -> Any: ... @runtime_checkable class CanSetName[T](Protocol): @@ -587,7 +578,7 @@ class CanAsyncWith[V, R](CanAenter[V], CanAexit[R], Protocol): # Module `abc` # https://docs.python.org/3/library/abc.html -# TODO: CanSubclasshook +# TODO: CanSubclasshook? # Module `copy` From 8cca800f432f2223d42048506bcd8abd713f0b76 Mon Sep 17 00:00:00 2001 From: jorenham Date: Sat, 24 Feb 2024 22:02:54 +0100 Subject: [PATCH 24/26] avoid table wrapping --- README.md | 93 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 64 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index d5da6ba2..fbed8164 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ So if you're a 10x developer that wants to hack Python's f-strings, but only if your type hints are spot-on; `optype` is you friend. | Type | Signature | Expression | -| -------------------------- | ------------------------------| -------------- | +| -------------------------- | ----------------------------- | -------------- | | `CanRepr[Y: str]` | `__repr__(self) -> T` | `repr(_)` | | `CanFormat[X: str, Y: str]`| `__format__(self, x: X) -> Y` | `format(_, x)` | @@ -139,22 +139,34 @@ be returned. CanGetattr[K: str, V] __getattr__(self, k: K) -> V - v = self.k or v = getattr(self, k) + + v = self.k or
+ v = getattr(self, k) + CanGetattribute[K: str, V] __getattribute__(self, k: K) -> V - v = self.k or v = getattr(self, k) + + v = self.k or
+ v = getattr(self, k) + CanSetattr[K: str, V] __setattr__(self, k: K, v: V) - self.k = v or setattr(self, k, v) + + self.k = v or
+ setattr(self, k, v) + CanDelattr[K: str] __delattr__(self, k: K) - del self.k or delattr(self, k) + + del self.k or
+ delattr(self, k) + CanDir[Vs: CanIter[Any]] @@ -199,7 +211,6 @@ from the abracadabra collections. This is how they are defined: ### Containers - | Type | Signature | Expression | | --------------------- | ---------------------------------- | -------------- | | `CanLen` | `__len__(self) -> int` | `len(self)` | @@ -211,7 +222,6 @@ from the abracadabra collections. This is how they are defined: | `CanReversed[Y]` [^4] | `__reversed__(self) -> Y` |`reversed(self)`| | `CanContains[K]` | `__contains__(self, k: K) -> bool` | `x in self` | - For indexing or locating container values, the following special methods are relevant: @@ -220,7 +230,6 @@ relevant: | `CanHash` | `__hash__(self) -> int` | `hash(self)` | | `CanIndex` | `__index__(self) -> int` | [docs](IX) | - [^4]: Although not strictly required, `Y@CanReversed` should be a `CanNext`. [LH]: https://docs.python.org/3/reference/datamodel.html#object.__length_hint__ [GM]: https://docs.python.org/3/reference/datamodel.html#object.__missing__ @@ -259,10 +268,6 @@ Interfaces for [descriptors](https://docs.python.org/3/howto/descriptor.html). - - -... - ### Callable objects @@ -356,23 +361,43 @@ Similarly, the augmented assignment operators are described by the following Additionally, there are the unary arithmetic operators: -| Type | Signature | Expression | -| ------------------- | ------------------------------- | ------------------ | -| `CanPos[Y]` | `__pos__(self) -> Y` | `+self` | -| `CanNeg[Y]` | `__neg__(self) -> Y` | `-self` | -| `CanInvert[Y]` | `__invert__(self) -> Y` | `~self` | -| `CanAbs[Y]` | `__abs__(self) -> Y` | `abs(self)` | +| Type | Signature | Expression | +| ------------------ | ----------------------------- | ----------------- | +| `CanPos[Y]` | `__pos__(self) -> Y` | `+self` | +| `CanNeg[Y]` | `__neg__(self) -> Y` | `-self` | +| `CanInvert[Y]` | `__invert__(self) -> Y` | `~self` | +| `CanAbs[Y]` | `__abs__(self) -> Y` | `abs(self)` | -The `round` function comes in two flavors: +The `round` function comes in two flavors, and their overloaded intersection: -| Type | Signature | Expression | -| ------------------ | ----------------------------- | --------------------- | -| `CanRound1[Y1]` | `__round__(self) -> Y1` | `round(self)` | -| `CanRound2[N, Y2]` | `__round__(self, n: N) -> Y2` | `round(self, n: N)` | + + + + + + + + + + + + + + + + + + + + + +
TypeSignatureExpression
CanRound1[Y1]__round__(self) -> Y1round(self)
CanRound2[N, Y2]__round__(self, n: N) -> Y2round(self, n)
CanRound[N, Y1, Y2] + __round__(self) -> Y1
+ __round__(self, n: N) -> Y2 +
round(self[, n: N])
+ +The last "double" signature denotes overloading. -For convenience, `optype` also provides their intersection type -`CanRound[N, Y1, Y2] =: CanRound1[Y1] & CanRound2[N, Y2]`, whose signature -overloads those of the `CanRound1` and `CanRound2`. To illustrate; `float` is a `CanRound[int, int, float]` and `int` a `CanRound[int, int, int]`. @@ -418,6 +443,12 @@ class CanWith[V, R](CanEnter[V], CanExit[R]): ``` +### Buffer types + + +... + + ### Async objects The `optype` variant of `collections.abc.Awaitable[V]`. The only difference @@ -441,7 +472,6 @@ But fret not, the `optype` alternatives are right here: | `CanAnext[V]` | `__anext__(self) -> V` | `anext(self)` | | `CanAiter[Vs: CanAnext]` | `__aiter__(self) -> Y` | `aiter(self)` | - But wait, shouldn't `V` be a `CanAwait`? Well, only if you don't want to get fired... Technically speaking, `__anext__` can return any type, and `anext` will pass @@ -467,9 +497,14 @@ type. ## Future plans -- Build a drop-in replacement for the `operator` standard library, with +- Support for Python versions before 3.12. +- A drop-in replacement for the `operator` standard library, with runtime-accessible type annotations, and more operators. - More standard library protocols, e.g. `copy`, `dataclasses`, `pickle`. +- Typed mixins for DRY implementation of operator, e.g. for comparison ops + `GeFromLt`, `GtFromLe`, etc as a typed alternative for + `functools.total_ordering`. Similarly for numeric types, with e.g. `__add__` + and `__neg__` a mixin could generate `__pos__` and `__sub__`, or with + `__mod__` and `__truediv__` a mixin could generate `__` - Dependency-free third-party type support, e.g. protocols for `numpy`'s array interface. -- Support for Python versions before 3.12. From 95425f363fd8f03521e4a1b91d387c4b82edf695 Mon Sep 17 00:00:00 2001 From: jorenham Date: Sun, 25 Feb 2024 04:12:16 +0100 Subject: [PATCH 25/26] "an adventure through with @typing.overload`", and other horror stories --- optype/_has.py | 13 ++-- poetry.lock | 16 ++-- pyproject.toml | 6 +- tests/test_protocols.py | 157 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 178 insertions(+), 14 deletions(-) create mode 100644 tests/test_protocols.py diff --git a/optype/_has.py b/optype/_has.py index 8ebb4584..7ed39056 100644 --- a/optype/_has.py +++ b/optype/_has.py @@ -5,7 +5,10 @@ from types import CodeType, ModuleType from typing import Any, Protocol, Self, override, runtime_checkable -from ._can import CanCall, CanIter +from ._can import ( + CanCall as _CanCall, + CanIter as _CanIter, +) # Instances @@ -16,7 +19,7 @@ class HasMatchArgs[Ks: tuple[str, ...] | list[str]](Protocol): @runtime_checkable -class HasSlots[S: str | CanIter[Any]](Protocol): +class HasSlots[S: str | _CanIter[Any]](Protocol): __slots__: S @@ -78,16 +81,16 @@ class HasTypeParams[*Ps](Protocol): @runtime_checkable class HasFunc[**Xs, Y](Protocol): - __func__: CanCall[Xs, Y] + __func__: _CanCall[Xs, Y] @runtime_checkable class HasWrapped[**Xs, Y](Protocol): - __wrapped__: CanCall[Xs, Y] + __wrapped__: _CanCall[Xs, Y] @runtime_checkable -class HasSelf[T: object | ModuleType]: +class HasSelf[T: object | ModuleType](Protocol): @property def __self__(self) -> T: ... diff --git a/poetry.lock b/poetry.lock index b44f2be6..89f07737 100644 --- a/poetry.lock +++ b/poetry.lock @@ -184,13 +184,13 @@ dev = ["twine (>=3.4.1)"] [[package]] name = "pytest" -version = "8.0.1" +version = "8.0.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.0.1-py3-none-any.whl", hash = "sha256:3e4f16fe1c0a9dc9d9389161c127c3edc5d810c38d6793042fb81d9f48a59fca"}, - {file = "pytest-8.0.1.tar.gz", hash = "sha256:267f6563751877d772019b13aacbe4e860d73fe8f651f28112e9ac37de7513ae"}, + {file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"}, + {file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"}, ] [package.dependencies] @@ -303,19 +303,19 @@ files = [ [[package]] name = "setuptools" -version = "69.1.0" +version = "69.1.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.1.0-py3-none-any.whl", hash = "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6"}, - {file = "setuptools-69.1.0.tar.gz", hash = "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401"}, + {file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"}, + {file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "virtualenv" diff --git a/pyproject.toml b/pyproject.toml index 177d9855..1fa7a7a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -188,7 +188,11 @@ extend-ignore = [ [tool.ruff.lint.per-file-ignores] "tests/*" = [ - "ANN", # flake8-annotations + # flake8-annotations + "ANN201", # missing-return-type-undocumented-public-function + + # flake8-self + "SLF001", # private-member-access ] [tool.ruff.lint.pycodestyle] diff --git a/tests/test_protocols.py b/tests/test_protocols.py new file mode 100644 index 00000000..362dcddb --- /dev/null +++ b/tests/test_protocols.py @@ -0,0 +1,157 @@ +from types import ModuleType +from typing import Protocol, cast + +import pytest + +import optype +import optype._can +import optype._has + + +def _is_protocol(cls: type) -> bool: + """Based on `typing_extensions.is_protocol`.""" + return ( + isinstance(cls, type) + and cls is not Protocol + and issubclass(cls, Protocol) + and getattr(cls, '_is_protocol', False) + ) + + +def _is_runtime_protocol(cls: type) -> bool: + """Check if `cls` is a `@runtime_checkable` `typing.Protocol`.""" + return _is_protocol(cls) and getattr(cls, '_is_runtime_protocol', False) + + +def _get_protocol_members(cls: type) -> frozenset[str]: + """ + A variant of `typing_extensions.get_protocol_members()` that doesn't + hide e.g. `__dict__` and `__annotations`, or adds `__hash__` if there's an + `__eq__` method. + Does not return method names of base classes defined in another module. + """ + assert _is_protocol(cls) + + module = cls.__module__ + members = cls.__annotations__.keys() | { + name for name, v in vars(cls).items() + if ( + callable(v) and ( + v.__module__ == module or ( + # Fun fact: Each `@overload` returns the same dummy + # function; so there's no reference your wrapped method :). + # Oh and BTW; `typing.get_overloads` only works on the + # non-overloaded method... + # Oh, you mean the one that # you shouldn't define within + # a `typing.Protocol`? + # Yes exactly! Anyway, good luck searching for the + # undocumented and ever-changing dark corner of the + # `typing` internals. I'm sure it must be there somewhere! + # Oh yea if you can't find it, try `typing_extensions`. + # Oh, still can't find it? Did you try ALL THE VERSIONS? + # + # ...anyway, the only thing we know here, is the name of + # an overloaded method. But we have no idea how many of + # them there *were*, let alone their signatures. + v.__module__.startswith('typing') + and v.__name__ == '_overload_dummy' + ) + ) + ) or ( + isinstance(v, property) + and v.fget + and v.fget.__module__ == module + ) + } + if not members: + # no idea why this happens, probably something to do with inheritance.. + # ... investigating this can of worms any further will physically harm + # my very soul, or at least, what's left of it at this point. + # ... + # anyway, this hack here is plagiarized from the (often incorrect) + # `typing_extensions.get_protocol_members`. + # Maybe the `typing.get_protocol_members` that's coming in 3.13 will + # won't be as broken. I have little hope though... + members = cast( + set[str], + getattr(cls, '__protocol_attrs__', None) or set(), + ) + + return frozenset(members) + + +def _get_protocols(module: ModuleType) -> frozenset[type]: + """Return the public protocol types within the given module.""" + return frozenset({ + cls for name in dir(module) + if not name.startswith('_') + and _is_protocol(cls := getattr(module, name)) + }) + + +def test_all_public(): + """ + Ensure all of protocols in `optype._can` and `optype._has` are in + `optype.__all__`. + """ + protocols_all = _get_protocols(optype) + protocols_can = _get_protocols(optype._can) + protocols_has = _get_protocols(optype._has) + + assert protocols_can | protocols_has == protocols_all + + +@pytest.mark.parametrize('cls', _get_protocols(optype)) +def test_instance_checkable(cls: type): + """Ensure all optype protocols are `@runtime_checkable`.""" + assert _is_runtime_protocol(cls) + + +@pytest.mark.parametrize('cls', _get_protocols(optype)) +def test_name_matches_dunder(cls: type): + """ + Ensure that each single-member optype name matches its member, + and that each multi-member optype does not have more members than it has + super optypes. + """ + prefix = cls.__module__.rsplit('.', 1)[1].removeprefix('_').title() + assert prefix in {'Can', 'Has'} + + name = cls.__name__ + assert name.startswith(prefix) + + members = _get_protocol_members(cls) + assert members + + member_count = len(members) + super_count = sum(map(_is_protocol, cls.mro()[1:-1])) + + if member_count > 1: + assert super_count == member_count + return + + # convert CamelCase to to snake_case (ignoring the first char, which + # could be an async (A), augmented (I), or reflected (R) binop name prefix) + member_expect = ''.join( + f'_{c}' if i > 1 and c.isupper() else c + for i, c in enumerate(name.removeprefix(prefix)) + ).lower() + # sanity checks (a bit out-of-scope, but humankind will probably survive) + assert member_expect.isidentifier() + assert '__' not in member_expect + assert member_expect[0] != '_' + assert member_expect[-1] != '_' + + # remove potential trailing arity digit + if member_expect[-1].isdigit(): + member_expect = member_expect[:-1] + # another misplaced check (ah well, let's hope the extinction event is fun) + assert not member_expect[-1].isdigit() + + member = next(iter(members)) + + if member[:2] == member[-2:] == '__': + # add some thunder... or was is döner...? wait, no; dunder!. + member_expect = f'__{member_expect}__' + + assert member == member_expect From b12c829b2d618b58adc48dcd1677fb096acbe9be Mon Sep 17 00:00:00 2001 From: jorenham Date: Sun, 25 Feb 2024 04:26:51 +0100 Subject: [PATCH 26/26] documented `CanBuffer` and `CanReleaseBuffer` --- README.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fbed8164..90660b44 100644 --- a/README.md +++ b/README.md @@ -56,11 +56,10 @@ Optype is available as [`optype`](OPTYPE) on PyPI: ```shell pip install optype ``` - [OPTYPE]: https://pypi.org/project/optype/ -## Getting started + ... @@ -442,12 +441,22 @@ class CanWith[V, R](CanEnter[V], CanExit[R]): ... ``` - ### Buffer types - -... +Interfaces for emulating buffer types. + +| Type | Signature | +| ------------------- | ------------------------------------------ | +| `CanBuffer[B: int]` | `__buffer__(self, flags: B) -> memoryview` | +| `CanReleaseBuffer` | `__release_buffer__(self) -> None` | + +The `flags: B` parameter accepts integers within the `[1, 1023]` interval. +Note that the `CanReleaseBuffer` isn't always needed. +See the [Python docs](BP) or [`inspect.BufferFlags`](BF) for more info. + +[BP]: https://docs.python.org/3/reference/datamodel.html#python-buffer-protocol +[BD]: https://docs.python.org/3/library/inspect.html#inspect.BufferFlags ### Async objects