diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6050cb3..b5b8d81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,8 +40,8 @@ jobs: - name: ruff check run: poetry run ruff check --output-format=github . - - name: pyright - run: poetry run pyright + - name: basedpyright + run: poetry run basedpyright test: timeout-minutes: 5 diff --git a/.gitignore b/.gitignore index afb77e5..c937f25 100644 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,7 @@ build/ dist/ site/ -# Unit test -.cache -.pyright/ +# Cache .pytest_cache/ .ruff_cache/ @@ -21,7 +19,4 @@ venv/ # IntelliJ .idea/ -.run - -# VSCode settings -.vscode/settings.json +.run/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1b01609..95c2de3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,31 +2,31 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - - id: check-yaml - - id: check-toml + - id: check-yaml + - id: check-toml - repo: https://github.com/python-poetry/poetry rev: 1.8.2 hooks: - - id: poetry-check + - id: poetry-check - repo: https://github.com/codespell-project/codespell rev: v2.2.6 hooks: - - id: codespell + - id: codespell - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.4 + rev: v0.3.5 hooks: - - id: ruff - args: [--fix, --exit-non-zero-on-fix] + - id: ruff + args: [--fix, --exit-non-zero-on-fix] - - repo: https://github.com/RobertCraigie/pyright-python - rev: v1.1.355 + - repo: https://github.com/DetachHead/basedpyright + rev: b1ebff9e96a087851fc24efe1674660cfea59140 hooks: - - id: pyright + - id: basedpyright - repo: https://github.com/igorshubovych/markdownlint-cli rev: v0.39.0 hooks: - - id: markdownlint + - id: markdownlint diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..b0a62f5 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,14 @@ +{ + "recommendations": [ + "charliermarsh.ruff", + "codezombiech.gitignore", + "davidanson.vscode-markdownlint", + "detachhead.basedpyright", + "editorconfig.editorconfig", + "elagil.pre-commit-helper", + "ms-python.python", + "serhioromano.vscode-gitflow", + "zeshuaro.vscode-python-poetry" + ], + "unwantedRecommendations": ["ms-python.vscode-pylance"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4f28e25 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "git.branchProtection": ["master"], + "githubPullRequests.overrideDefaultBranch": "dev", + "python.languageServer": "None" +} diff --git a/README.md b/README.md index 3eda07b..d6ea940 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,47 @@ pip install optype [OPTYPE]: https://pypi.org/project/optype/ +## Example + +Let's say you're writing a `twice(x)` function, that evaluates `2 * x`. +Implementing it is trivial, but what about the type annotations? + +Because `twice(2) == 4`, `twice(3.14) == 6.28` and `twice('I') = 'II'`, it +might seem like a good idea to type it as `twice[T](x: T) -> T: ...`. +However, that wouldn't include cases such as `twice(True) == 2` or +`twice((42, True)) == (42, True, 42, True)`, where the input- and output types +differ. +Moreover, `twice` should accept *any* type with a custom `__rmul__` method +that accepts `2` as argument. + +This is where `optype` comes in handy, which has single-method protocols for +*all* the builtin special methods. +For `twice`, we can use `optype.CanRMul[X, Y]`, which, as the name suggests, +is a protocol with (only) the `def __rmul__(self, x: X) -> Y: ...` method. +With this, the `twice` function can written as: + +```python +import typing +import optype + +type Two = typing.Literal[2] + +def twice[Y](x: optype.CanRMul[Two, Y], /) -> Y: + return 2 * x +``` + +But what about types that implement `__add__` but not `__radd__`? +In this case, we could return `x * 2` as fallback. +Because the `optype.Can*` protocols are runtime-checkable, the revised +`twice2` function can be compactly written as: + +```python +def twice2[Y](x: optype.CanRMul[Two, Y] | optype.CanMul[Two, Y], /) -> Y: + return 2 * x if isinstance(x, optype.CanRMul) else x * 2 +``` + +See [`examples/twice.py`](examples/twice.py) for the full example. + ## Overview The API of `optype` is flat; a single `import optype` is all you need. diff --git a/examples/functor.py b/examples/functor.py index 34b9dbc..8881d15 100644 --- a/examples/functor.py +++ b/examples/functor.py @@ -1,39 +1,21 @@ -# ruff: noqa: INP001 from __future__ import annotations -from typing import ( - TYPE_CHECKING, - Any, - Generic, - TypeVar, - final, - override, -) - -import optype as opt - +from typing import TYPE_CHECKING, Any, final, override +import optype if TYPE_CHECKING: from collections.abc import Callable from types import NotImplementedType -""" -Automatic type-variance inference doesn't work here. That's no surprise, since -it is impossible to do so in all cases. So that's yet another theoretically -incorrect (and therefore broken) python-typing "feature"... -""" -T_co = TypeVar('T_co', covariant=True) - - @final # noqa: PLR0904 -class Functor(Generic[T_co]): +class Functor[T]: __match_args__ = __slots__ = ('value',) - def __init__(self, value: T_co, /) -> None: + def __init__(self, value: T, /) -> None: self.value = value - def map1[Y](self, f: Callable[[T_co], Y]) -> Functor[Y]: + def map1[Y](self, f: Callable[[T], Y]) -> Functor[Y]: """ Applies a unary operator `f` over the value of `self`, and return a new `Functor`. @@ -42,7 +24,7 @@ def map1[Y](self, f: Callable[[T_co], Y]) -> Functor[Y]: def map2[X, Y]( self, - f: Callable[[T_co, X], Y], + f: Callable[[T, X], Y], other: Functor[X] | Any, ) -> Functor[Y] | NotImplementedType: """ @@ -63,36 +45,36 @@ def __repr__(self) -> str: return f'{type(self).__name__}({self.value!r})' @override - def __hash__(self: Functor[opt.CanHash]) -> int: - return opt.do_hash(self.value) + def __hash__(self: Functor[optype.CanHash]) -> int: + return optype.do_hash(self.value) # unary prefix ops - def __neg__[Y](self: Functor[opt.CanNeg[Y]]) -> Functor[Y]: + def __neg__[Y](self: Functor[optype.CanNeg[Y]]) -> Functor[Y]: """ >>> -Functor(3.14) Functor(-3.14) """ - return self.map1(opt.do_neg) + return self.map1(optype.do_neg) - def __pos__[Y](self: Functor[opt.CanPos[Y]]) -> Functor[Y]: + def __pos__[Y](self: Functor[optype.CanPos[Y]]) -> Functor[Y]: """ >>> +Functor(True) Functor(1) """ - return self.map1(opt.do_pos) + return self.map1(optype.do_pos) - def __invert__[Y](self: Functor[opt.CanInvert[Y]]) -> Functor[Y]: + def __invert__[Y](self: Functor[optype.CanInvert[Y]]) -> Functor[Y]: """ >>> ~Functor(0) Functor(-1) """ - return self.map1(opt.do_invert) + return self.map1(optype.do_invert) # rich comparison ops def __lt__[X, Y]( - self: Functor[opt.CanLt[X, Y]], + self: Functor[optype.CanLt[X, Y]], x: Functor[X], ) -> Functor[Y]: """ @@ -101,10 +83,10 @@ def __lt__[X, Y]( >>> Functor((0, 1)) < Functor((1, -1)) Functor(True) """ - return self.map2(opt.do_lt, x) + return self.map2(optype.do_lt, x) def __le__[X, Y]( - self: Functor[opt.CanLe[X, Y]], + self: Functor[optype.CanLe[X, Y]], x: Functor[X], ) -> Functor[Y]: """ @@ -113,11 +95,11 @@ def __le__[X, Y]( >>> Functor((0, 1)) <= Functor((1, -1)) Functor(True) """ - return self.map2(opt.do_le, x) + return self.map2(optype.do_le, x) @override def __eq__[X, Y]( # pyright: ignore[reportIncompatibleMethodOverride] - self: Functor[opt.CanEq[X, Y]], + self: Functor[optype.CanEq[X, Y]], x: Functor[X], ) -> Functor[Y]: """ @@ -128,11 +110,11 @@ def __eq__[X, Y]( # pyright: ignore[reportIncompatibleMethodOverride] >>> Functor(0) == 0 False """ - return self.map2(opt.do_eq, x) + return self.map2(optype.do_eq, x) @override def __ne__[X, Y]( # pyright: ignore[reportIncompatibleMethodOverride] - self: Functor[opt.CanNe[X, Y]], + self: Functor[optype.CanNe[X, Y]], x: Functor[X], ) -> Functor[Y]: """ @@ -143,10 +125,10 @@ def __ne__[X, Y]( # pyright: ignore[reportIncompatibleMethodOverride] >>> Functor(0) != 0 True """ - return self.map2(opt.do_ne, x) + return self.map2(optype.do_ne, x) def __gt__[X, Y]( - self: Functor[opt.CanGt[X, Y]], + self: Functor[optype.CanGt[X, Y]], x: Functor[X], ) -> Functor[Y]: """ @@ -155,10 +137,10 @@ def __gt__[X, Y]( >>> Functor((0, 1)) > Functor((1, -1)) Functor(False) """ - return self.map2(opt.do_gt, x) + return self.map2(optype.do_gt, x) def __ge__[X, Y]( - self: Functor[opt.CanGe[X, Y]], + self: Functor[optype.CanGe[X, Y]], x: Functor[X], ) -> Functor[Y]: """ @@ -167,12 +149,12 @@ def __ge__[X, Y]( >>> Functor((0, 1)) >= Functor((1, -1)) Functor(False) """ - return self.map2(opt.do_ge, x) + return self.map2(optype.do_ge, x) # binary infix ops def __add__[X, Y]( - self: Functor[opt.CanAdd[X, Y]], + self: Functor[optype.CanAdd[X, Y]], x: Functor[X], ) -> Functor[Y]: """ @@ -181,96 +163,96 @@ def __add__[X, Y]( >>> Functor(('spam',)) + Functor(('ham',)) + Functor(('eggs',)) Functor(('spam', 'ham', 'eggs')) """ - return self.map2(opt.do_add, x) + return self.map2(optype.do_add, x) def __sub__[X, Y]( - self: Functor[opt.CanSub[X, Y]], + self: Functor[optype.CanSub[X, Y]], x: Functor[X], ) -> Functor[Y]: """ >>> Functor(0) - Functor(1) Functor(-1) """ - return self.map2(opt.do_sub, x) + return self.map2(optype.do_sub, x) def __mul__[X, Y]( - self: Functor[opt.CanMul[X, Y]], + self: Functor[optype.CanMul[X, Y]], x: Functor[X], ) -> Functor[Y]: """ >>> Functor(('Developers!',)) * Functor(4) Functor(('Developers!', 'Developers!', 'Developers!', 'Developers!')) """ - return self.map2(opt.do_mul, x) + return self.map2(optype.do_mul, x) def __matmul__[X, Y]( - self: Functor[opt.CanMatmul[X, Y]], + self: Functor[optype.CanMatmul[X, Y]], x: Functor[X], ) -> Functor[Y]: - return self.map2(opt.do_matmul, x) + return self.map2(optype.do_matmul, x) def __truediv__[X, Y]( - self: Functor[opt.CanTruediv[X, Y]], + self: Functor[optype.CanTruediv[X, Y]], x: Functor[X], ) -> Functor[Y]: """ >>> Functor(1) / Functor(2) Functor(0.5) """ - return self.map2(opt.do_truediv, x) + return self.map2(optype.do_truediv, x) def __floordiv__[X, Y]( - self: Functor[opt.CanFloordiv[X, Y]], + self: Functor[optype.CanFloordiv[X, Y]], x: Functor[X], ) -> Functor[Y]: """ >>> Functor(1) // Functor(2) Functor(0) """ - return self.map2(opt.do_floordiv, x) + return self.map2(optype.do_floordiv, x) def __mod__[X, Y]( - self: Functor[opt.CanMod[X, Y]], + self: Functor[optype.CanMod[X, Y]], x: Functor[X], ) -> Functor[Y]: """ >>> Functor(10) % Functor(7) Functor(3) """ - return self.map2(opt.do_mod, x) + return self.map2(optype.do_mod, x) def __pow__[X, Y]( - self: Functor[opt.CanPow2[X, Y]], + self: Functor[optype.CanPow2[X, Y]], x: Functor[X], ) -> Functor[Y]: """ >>> Functor(2) ** Functor(3) Functor(8) """ - return self.map2(opt.do_pow, x) + return self.map2(optype.do_pow, x) def __lshift__[X, Y]( - self: Functor[opt.CanLshift[X, Y]], + self: Functor[optype.CanLshift[X, Y]], x: Functor[X], ) -> Functor[Y]: """ >>> Functor(1) << Functor(10) Functor(1024) """ - return self.map2(opt.do_lshift, x) + return self.map2(optype.do_lshift, x) def __rshift__[X, Y]( - self: Functor[opt.CanRshift[X, Y]], + self: Functor[optype.CanRshift[X, Y]], x: Functor[X], ) -> Functor[Y]: """ >>> Functor(1024) >> Functor(4) Functor(64) """ - return self.map2(opt.do_rshift, x) + return self.map2(optype.do_rshift, x) def __and__[X, Y]( - self: Functor[opt.CanAnd[X, Y]], + self: Functor[optype.CanAnd[X, Y]], x: Functor[X], ) -> Functor[Y]: """ @@ -279,10 +261,10 @@ def __and__[X, Y]( >>> Functor(3) & Functor(7) Functor(3) """ - return self.map2(opt.do_and, x) + return self.map2(optype.do_and, x) def __xor__[X, Y]( - self: Functor[opt.CanXor[X, Y]], + self: Functor[optype.CanXor[X, Y]], x: Functor[X], ) -> Functor[Y]: """ @@ -291,10 +273,10 @@ def __xor__[X, Y]( >>> Functor(3) ^ Functor(7) Functor(4) """ - return self.map2(opt.do_xor, x) + return self.map2(optype.do_xor, x) def __or__[X, Y]( - self: Functor[opt.CanOr[X, Y]], + self: Functor[optype.CanOr[X, Y]], x: Functor[X], ) -> Functor[Y]: """ @@ -303,84 +285,84 @@ def __or__[X, Y]( >>> Functor(3) | Functor(7) Functor(7) """ - return self.map2(opt.do_or, x) + return self.map2(optype.do_or, x) # binary reflected infix ops def __radd__[X, Y]( - self: Functor[opt.CanRAdd[X, Y]], + self: Functor[optype.CanRAdd[X, Y]], x: Functor[X], ) -> Functor[Y]: - return self.map2(opt.do_radd, x) + return self.map2(optype.do_radd, x) def __rsub__[X, Y]( - self: Functor[opt.CanRSub[X, Y]], + self: Functor[optype.CanRSub[X, Y]], x: Functor[X], ) -> Functor[Y]: - return self.map2(opt.do_rsub, x) + return self.map2(optype.do_rsub, x) def __rmul__[X, Y]( - self: Functor[opt.CanRMul[X, Y]], + self: Functor[optype.CanRMul[X, Y]], x: Functor[X], ) -> Functor[Y]: - return self.map2(opt.do_rmul, x) + return self.map2(optype.do_rmul, x) def __rmatmul__[X, Y]( - self: Functor[opt.CanRMatmul[X, Y]], + self: Functor[optype.CanRMatmul[X, Y]], x: Functor[X], ) -> Functor[Y]: - return self.map2(opt.do_rmatmul, x) + return self.map2(optype.do_rmatmul, x) def __rtruediv__[X, Y]( - self: Functor[opt.CanRTruediv[X, Y]], + self: Functor[optype.CanRTruediv[X, Y]], x: Functor[X], ) -> Functor[Y]: - return self.map2(opt.do_rtruediv, x) + return self.map2(optype.do_rtruediv, x) def __rfloordiv__[X, Y]( - self: Functor[opt.CanRFloordiv[X, Y]], + self: Functor[optype.CanRFloordiv[X, Y]], x: Functor[X], ) -> Functor[Y]: - return self.map2(opt.do_rfloordiv, x) + return self.map2(optype.do_rfloordiv, x) def __rmod__[X, Y]( - self: Functor[opt.CanRMod[X, Y]], + self: Functor[optype.CanRMod[X, Y]], x: Functor[X], ) -> Functor[Y]: - return self.map2(opt.do_rmod, x) + return self.map2(optype.do_rmod, x) def __rpow__[X, Y]( - self: Functor[opt.CanRPow[X, Y]], + self: Functor[optype.CanRPow[X, Y]], x: Functor[X], ) -> Functor[Y]: - return self.map2(opt.do_rpow, x) + return self.map2(optype.do_rpow, x) def __rlshift__[X, Y]( - self: Functor[opt.CanRLshift[X, Y]], + self: Functor[optype.CanRLshift[X, Y]], x: Functor[X], ) -> Functor[Y]: - return self.map2(opt.do_rlshift, x) + return self.map2(optype.do_rlshift, x) def __rrshift__[X, Y]( - self: Functor[opt.CanRRshift[X, Y]], + self: Functor[optype.CanRRshift[X, Y]], x: Functor[X], ) -> Functor[Y]: - return self.map2(opt.do_rrshift, x) + return self.map2(optype.do_rrshift, x) def __rand__[X, Y]( - self: Functor[opt.CanRAnd[X, Y]], + self: Functor[optype.CanRAnd[X, Y]], x: Functor[X], ) -> Functor[Y]: - return self.map2(opt.do_rand, x) + return self.map2(optype.do_rand, x) def __rxor__[X, Y]( - self: Functor[opt.CanRXor[X, Y]], + self: Functor[optype.CanRXor[X, Y]], x: Functor[X], ) -> Functor[Y]: - return self.map2(opt.do_rxor, x) + return self.map2(optype.do_rxor, x) def __ror__[X, Y]( - self: Functor[opt.CanROr[X, Y]], + self: Functor[optype.CanROr[X, Y]], x: Functor[X], ) -> Functor[Y]: - return self.map2(opt.do_ror, x) + return self.map2(optype.do_ror, x) diff --git a/examples/twice.py b/examples/twice.py new file mode 100644 index 0000000..7255f28 --- /dev/null +++ b/examples/twice.py @@ -0,0 +1,51 @@ +import typing +import optype + +type Two = typing.Literal[2] + + +def twice[Y](x: optype.CanRMul[Two, Y], /) -> Y: + return 2 * x + + +typing.assert_type(twice(True), int) +typing.assert_type(twice(1 / 137), float) +typing.assert_type(twice(str(-1 / 12)), str) +typing.assert_type(twice([object()]), list[object]) + + +@typing.final +class RMulArgs[*Ts]: + def __init__(self, *args: *Ts) -> None: + self.args = args + + def __rmul__[Y: int](self, y: Two, /) -> 'RMulArgs[*Ts, *Ts]': + if y != 2: + return NotImplemented + return RMulArgs(*self.args, *self.args) + + +typing.assert_type(twice(RMulArgs(42, True)), RMulArgs[int, bool, int, bool]) + +### + + +def twice2[Y](x: optype.CanRMul[Two, Y] | optype.CanMul[Two, Y], /) -> Y: + return 2 * x if isinstance(x, optype.CanRMul) else x * 2 + + +class RMulThing: + def __rmul__(self, y: Two, /) -> str: + return f'{y} * _' + + +class MulThing: + def __mul__(self, y: Two, /) -> str: + return f'_ * {y}' + + +typing.assert_type(twice2(RMulThing()), str) +assert twice2(RMulThing()) == '2 * _' + +typing.assert_type(twice2(MulThing()), str) +assert twice2(MulThing()) == '_ * 2' diff --git a/optype/_can.py b/optype/_can.py index 97fb542..d65e209 100644 --- a/optype/_can.py +++ b/optype/_can.py @@ -112,7 +112,7 @@ def __le__(self, __x: X) -> Y: ... @runtime_checkable -class CanEq[X, Y](Protocol): +class CanEq[X, Y](Protocol): # noqa: PLW1641 """ Unfortunately, `typeshed` incorrectly annotates `object.__eq__` as `(Self, object) -> bool`. diff --git a/optype/_do.py b/optype/_do.py index 19767b7..390f405 100644 --- a/optype/_do.py +++ b/optype/_do.py @@ -49,13 +49,6 @@ # callables -# def do_call[**Xs, Y]( -# func: _c.CanCall[Xs, Y], -# *args: Xs.args, -# **kwargs: Xs.kwargs, -# ) -> Y: -# return func(*args, **kwargs) - do_call: _d.DoesCall = _o.call diff --git a/poetry.lock b/poetry.lock index 2d4628a..d20abc9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,19 @@ # This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +[[package]] +name = "basedpyright" +version = "1.8.0" +description = "static type checking for Python (but based)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "basedpyright-1.8.0-py3-none-any.whl", hash = "sha256:8f4c2ca09b615840a57ee0e6a440fc0bd3ed5e4b7524d2bcc11bcf4362cc23fa"}, + {file = "basedpyright-1.8.0.tar.gz", hash = "sha256:7c3177f74003e0b5938a243f416ffa5e4cfb75d7d55201078d9dfc4ce0c59cf3"}, +] + +[package.dependencies] +nodejs-bin = ">=18.4.0a4" + [[package]] name = "cfgv" version = "3.4.0" @@ -52,18 +66,18 @@ files = [ [[package]] name = "filelock" -version = "3.13.1" +version = "3.13.3" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, - {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, + {file = "filelock-3.13.3-py3-none-any.whl", hash = "sha256:5ffa845303983e7a0b7ae17636509bc97997d58afeafa72fb141a17b152284cb"}, + {file = "filelock-3.13.3.tar.gz", hash = "sha256:a79895a25bbefdf55d1a2a0a80968f7dbb28edcd6d4234a0afb3f37ecde4b546"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] typing = ["typing-extensions (>=4.8)"] [[package]] @@ -105,6 +119,25 @@ files = [ [package.dependencies] setuptools = "*" +[[package]] +name = "nodejs-bin" +version = "18.4.0a4" +description = "Node.js is an open-source, cross-platform, back-end JavaScript runtime environment that runs on the V8 engine and executes JavaScript code outside a web browser." +optional = false +python-versions = "~=3.5" +files = [ + {file = "nodejs_bin-18.4.0a4-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:16cb1abf7fe8c11c574e1e474d9f934a0df49a480290eae6e733d8bb09512e22"}, + {file = "nodejs_bin-18.4.0a4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:068ca987ed83ea1123775fafe5dc22d8f2ff920d7d31571e1bfe6fb1093833eb"}, + {file = "nodejs_bin-18.4.0a4-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:06cfeaa4d26eec94d8edb9927525ce94eb96dadc81f7d1daed42d1a7d003a4c9"}, + {file = "nodejs_bin-18.4.0a4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:431ee3529f4fb226ddcfd4f14cb37e7df31238c42dfd051f4bf8f0c21029b133"}, + {file = "nodejs_bin-18.4.0a4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:21f1f77ddc8fe05353bb6d6ee8e5a62edb3a8dcdb2740a5f9307fd8d9eef6691"}, + {file = "nodejs_bin-18.4.0a4-py3-none-win32.whl", hash = "sha256:59671fdc563dabb8be8a0b6dae4169d780482b3c9e0fba3f9aa2b7ee8d2261ac"}, + {file = "nodejs_bin-18.4.0a4-py3-none-win_amd64.whl", hash = "sha256:cbd509218b4b17f75ee7841f9c21d5cacc1626d3b823a652a6627dbad18228ec"}, +] + +[package.extras] +cmd = ["nodejs-cmd"] + [[package]] name = "packaging" version = "24.0" @@ -148,13 +181,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "3.6.2" +version = "3.7.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ - {file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"}, - {file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"}, + {file = "pre_commit-3.7.0-py2.py3-none-any.whl", hash = "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab"}, + {file = "pre_commit-3.7.0.tar.gz", hash = "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060"}, ] [package.dependencies] @@ -164,24 +197,6 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" -[[package]] -name = "pyright" -version = "1.1.355" -description = "Command line wrapper for pyright" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pyright-1.1.355-py3-none-any.whl", hash = "sha256:bf30b6728fd68ae7d09c98292b67152858dd89738569836896df786e52b5fe48"}, - {file = "pyright-1.1.355.tar.gz", hash = "sha256:dca4104cd53d6484e6b1b50b7a239ad2d16d2ffd20030bcf3111b56f44c263bf"}, -] - -[package.dependencies] -nodeenv = ">=1.6.0" - -[package.extras] -all = ["twine (>=3.4.1)"] -dev = ["twine (>=3.4.1)"] - [[package]] name = "pytest" version = "8.1.1" @@ -277,28 +292,28 @@ files = [ [[package]] name = "ruff" -version = "0.3.4" +version = "0.3.5" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:60c870a7d46efcbc8385d27ec07fe534ac32f3b251e4fc44b3cbfd9e09609ef4"}, - {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6fc14fa742e1d8f24910e1fff0bd5e26d395b0e0e04cc1b15c7c5e5fe5b4af91"}, - {file = "ruff-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3ee7880f653cc03749a3bfea720cf2a192e4f884925b0cf7eecce82f0ce5854"}, - {file = "ruff-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf133dd744f2470b347f602452a88e70dadfbe0fcfb5fd46e093d55da65f82f7"}, - {file = "ruff-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3860057590e810c7ffea75669bdc6927bfd91e29b4baa9258fd48b540a4365"}, - {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:986f2377f7cf12efac1f515fc1a5b753c000ed1e0a6de96747cdf2da20a1b369"}, - {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fd98e85869603e65f554fdc5cddf0712e352fe6e61d29d5a6fe087ec82b76c"}, - {file = "ruff-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64abeed785dad51801b423fa51840b1764b35d6c461ea8caef9cf9e5e5ab34d9"}, - {file = "ruff-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df52972138318bc7546d92348a1ee58449bc3f9eaf0db278906eb511889c4b50"}, - {file = "ruff-0.3.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:98e98300056445ba2cc27d0b325fd044dc17fcc38e4e4d2c7711585bd0a958ed"}, - {file = "ruff-0.3.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:519cf6a0ebed244dce1dc8aecd3dc99add7a2ee15bb68cf19588bb5bf58e0488"}, - {file = "ruff-0.3.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb0acfb921030d00070539c038cd24bb1df73a2981e9f55942514af8b17be94e"}, - {file = "ruff-0.3.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cf187a7e7098233d0d0c71175375c5162f880126c4c716fa28a8ac418dcf3378"}, - {file = "ruff-0.3.4-py3-none-win32.whl", hash = "sha256:af27ac187c0a331e8ef91d84bf1c3c6a5dea97e912a7560ac0cef25c526a4102"}, - {file = "ruff-0.3.4-py3-none-win_amd64.whl", hash = "sha256:de0d5069b165e5a32b3c6ffbb81c350b1e3d3483347196ffdf86dc0ef9e37dd6"}, - {file = "ruff-0.3.4-py3-none-win_arm64.whl", hash = "sha256:6810563cc08ad0096b57c717bd78aeac888a1bfd38654d9113cb3dc4d3f74232"}, - {file = "ruff-0.3.4.tar.gz", hash = "sha256:f0f4484c6541a99862b693e13a151435a279b271cff20e37101116a21e2a1ad1"}, + {file = "ruff-0.3.5-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:aef5bd3b89e657007e1be6b16553c8813b221ff6d92c7526b7e0227450981eac"}, + {file = "ruff-0.3.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:89b1e92b3bd9fca249153a97d23f29bed3992cff414b222fcd361d763fc53f12"}, + {file = "ruff-0.3.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e55771559c89272c3ebab23326dc23e7f813e492052391fe7950c1a5a139d89"}, + {file = "ruff-0.3.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dabc62195bf54b8a7876add6e789caae0268f34582333cda340497c886111c39"}, + {file = "ruff-0.3.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a05f3793ba25f194f395578579c546ca5d83e0195f992edc32e5907d142bfa3"}, + {file = "ruff-0.3.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dfd3504e881082959b4160ab02f7a205f0fadc0a9619cc481982b6837b2fd4c0"}, + {file = "ruff-0.3.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87258e0d4b04046cf1d6cc1c56fadbf7a880cc3de1f7294938e923234cf9e498"}, + {file = "ruff-0.3.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:712e71283fc7d9f95047ed5f793bc019b0b0a29849b14664a60fd66c23b96da1"}, + {file = "ruff-0.3.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a532a90b4a18d3f722c124c513ffb5e5eaff0cc4f6d3aa4bda38e691b8600c9f"}, + {file = "ruff-0.3.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:122de171a147c76ada00f76df533b54676f6e321e61bd8656ae54be326c10296"}, + {file = "ruff-0.3.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d80a6b18a6c3b6ed25b71b05eba183f37d9bc8b16ace9e3d700997f00b74660b"}, + {file = "ruff-0.3.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a7b6e63194c68bca8e71f81de30cfa6f58ff70393cf45aab4c20f158227d5936"}, + {file = "ruff-0.3.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a759d33a20c72f2dfa54dae6e85e1225b8e302e8ac655773aff22e542a300985"}, + {file = "ruff-0.3.5-py3-none-win32.whl", hash = "sha256:9d8605aa990045517c911726d21293ef4baa64f87265896e491a05461cae078d"}, + {file = "ruff-0.3.5-py3-none-win_amd64.whl", hash = "sha256:dc56bb16a63c1303bd47563c60482a1512721053d93231cf7e9e1c6954395a0e"}, + {file = "ruff-0.3.5-py3-none-win_arm64.whl", hash = "sha256:faeeae9905446b975dcf6d4499dc93439b131f1443ee264055c5716dd947af55"}, + {file = "ruff-0.3.5.tar.gz", hash = "sha256:a067daaeb1dc2baf9b82a32dae67d154d95212080c80435eb052d95da647763d"}, ] [[package]] @@ -340,4 +355,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "8edd41441e33d08bc99baf4e572c8a12b2dd3598532bb0a079c67787660e4981" +content-hash = "063b874693be70ee83c46c80b32a7e120e228a3fb5d07954301e27bf7e6ee740" diff --git a/pyproject.toml b/pyproject.toml index 93b1d63..2d61811 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,8 +5,8 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "optype" -version = "0.3.0" -description = "Building blocks for precise & flexible Python type hints" +version = "0.3.1" +description = "Building blocks for precise type hints in Python 3.12+" authors = ["Joren Hammudoglu "] license = "BSD-3-Clause" readme = "README.md" @@ -15,12 +15,12 @@ readme = "README.md" python = "^3.12" [tool.poetry.group.dev.dependencies] -pre-commit = '^3.6.2' +pre-commit = '^3.7.0' [tool.poetry.group.lint.dependencies] codespell = "^2.2.6" -pyright = "^1.1.355" -ruff = "^0.3.4" +basedpyright = "^1.8.0" +ruff = "^0.3.5" [tool.poetry.group.test.dependencies] pytest = "^8.1.1" @@ -34,66 +34,33 @@ pytest-github-actions-annotate-failures = ">=0.2.0,<1.0" [tool.codespell] context = 2 ignore-words-list = "cange" # CanGe -skip = """\ - ./.venv,\ - ./.vscode,\ - *.lock,\ - *.pyc,\ - *.js\ -""" -[tool.pyright] -include = ["optype", "examples", "tests"] +[tool.basedpyright] +include = [ + "optype", + "examples", + "tests" +] exclude = [ "**/__pycache__", - "**/.vscode", - ".venv", ".git", ".github", ".pytest_cache", ".ruff_cache", - "dist", + ".venv", + ".vscode", + "dist" ] stubPath = "." venvPath = "." venv = ".venv" pythonVersion = "3.12" pythonPlatform = "All" -typeCheckingMode = "strict" +typeCheckingMode = "all" useLibraryCodeForTypes = false - -strictListInference = true -strictDictionaryInference = true -strictSetInference = true -deprecateTypingAliases = true -disableBytesTypePromotions = true -reportPropertyTypeMismatch = "error" -reportMissingTypeStubs = "error" -reportImportCycles = "error" -reportUnusedImport = "none" # already covered by ruff -reportUnusedClass = "warning" -reportUnusedFunction = "warning" -reportUnusedVariable = "warning" -reportConstantRedefinition = "error" -reportInconsistentConstructor = "error" -reportMissingTypeArgument = "error" -reportUninitializedInstanceVariable = "error" -reportCallInDefaultInitializer = "error" -reportUnnecessaryIsInstance = "warning" -reportUnnecessaryCast = "warning" -reportUnnecessaryComparison = "warning" -reportUnnecessaryContains = "warning" -reportImplicitStringConcatenation = "none" -reportInvalidStubStatement = "error" -reportIncompleteStub = "error" -# reportUnusedCallResult = "warning" -reportUnusedCoroutine = "error" -reportUnusedExpression = "warning" -reportUnnecessaryTypeIgnoreComment = "error" -reportMatchNotExhaustive = "error" -reportImplicitOverride = "warning" -reportShadowedImports = "error" +reportAny = false +reportUnusedCallResult = false [tool.pytest.ini_options] @@ -102,10 +69,6 @@ testpaths = ["optype", "examples", "tests"] addopts = [ "-ra", "--doctest-modules", - "--exitfirst", - "--showlocals", - "--strict-config", - "--strict-markers", ] doctest_optionflags = [ "NORMALIZE_WHITESPACE", @@ -130,13 +93,16 @@ ignore-init-module-imports = true preview = true select = [ "F", # pyflakes - "E", # pycodestyle error - "W", # pycodestyle warning + "E", # pycodestyle: error + "W", # pycodestyle: warning + "C90", # mccabe "I", # isort "N", # pep8-naming "UP", # pyupgrade "YTT", # flake8-2020 "ANN", # flake8-annotations + "ASYNC", # flake8-async + "TRIO", # flake8-trio "BLE", # flake8-blind-except "B", # flake8-bugbear "A", # flake8-builtins @@ -144,6 +110,7 @@ select = [ "C4", # flake8-comprehensions "DTZ", # flake8-datetimez "T10", # flake8-debugger + "EM", # flake8-errmsg "EXE", # flake8-executable "FA", # flake8-future-annotations "ISC", # flake8-implicit-str-concat @@ -164,13 +131,19 @@ select = [ "TCH", # flake8-type-checking "ARG", # flake8-unused-arguments "PTH", # flake8-use-pathlib - # "ERA", # eradicate # (rebellion!) + "TD", # flake8-todos + "FIX", # flake8-fixme + "ERA", # eradicate + "PD", # pandas-vet "PGH", # pygrep-hooks "PL", # pylint "TRY", # tryceratops "FLY", # flynt + "NPY", # numpy + "AIR", # airflow "PERF", # perflint, "FURB", # refurb + "LOG", # flake8-logging "RUF", # ruff ] extend-ignore = [ @@ -180,19 +153,26 @@ extend-ignore = [ "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) + "PYI036", # bad-exit-annotation (unreasonable) # refurb - "FURB118", # reimplemented-operator (that's kinda the point, bro) + "FURB118", # reimplemented-operator (unreasonable) ] [tool.ruff.lint.per-file-ignores] +"examples/*" = [ + # isort + "I001", # unsorted-imports + + # flake8-no-pep420 + "INP001", # implicit-namespace-package + + # pylint + "PLR2004", # magic-value-comparison +] "tests/*" = [ # flake8-annotations - "ANN201", # missing-return-type-undocumented-public-function + "ANN201", # missing-return-type # flake8-self "SLF001", # private-member-access @@ -201,7 +181,6 @@ extend-ignore = [ [tool.ruff.lint.isort] case-sensitive = true combine-as-imports = true -force-wrap-aliases = true known-first-party = ["optype"] lines-between-types = 0 lines-after-imports = 2 diff --git a/tests/test_protocols.py b/tests/test_protocols.py index 796158e..2d3923a 100644 --- a/tests/test_protocols.py +++ b/tests/test_protocols.py @@ -88,7 +88,6 @@ def test_name_matches_dunder(cls: type): own_members: frozenset[str] parents = list(filter(is_protocol, cls.mro()[1:])) - # parents = [is_protocol(parent) for parent in cls.mro()[1:]] if parents: overridden = { member for member in members @@ -105,7 +104,7 @@ def test_name_matches_dunder(cls: type): # this test should probably be split up... if member_count > min(1, own_member_count): - # ensure #parent protocols == #members (including inherited) + # ensure len(parent protocols) == len(members) (including inherited) assert len(parents) == member_count members_concrete = set(members)