From 8320cec2bb014b8fcfe880d27ab5011fa21a1ea4 Mon Sep 17 00:00:00 2001 From: Thomas M Kehrenberg Date: Thu, 14 Dec 2023 15:01:18 +0100 Subject: [PATCH] Better Generic support (#46) --- .github/workflows/ci.yml | 2 +- README.md | 9 +--- type-enum/poetry.lock | 65 ++++++++++++++------------- type-enum/pyproject.toml | 2 +- type-enum/tests/test_instantiation.py | 5 +++ type-enum/type_enum/_core.py | 45 +++++++++++++------ 6 files changed, 73 insertions(+), 55 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5de2b1b..22e7651 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,4 +80,4 @@ jobs: poetry install --no-interaction --no-root - name: Run type check run: | - poetry run mypy tests --enable-incomplete-feature=TypeVarTuple --enable-incomplete-feature=Unpack + poetry run mypy tests diff --git a/README.md b/README.md index 75e9695..71e5bdf 100644 --- a/README.md +++ b/README.md @@ -31,13 +31,13 @@ match background_color: ## Installation -Requires Python >= 3.11 +Requires Python >= 3.11 (because features from PEP 646 are required) ``` pip install type-enum ``` -And for the mypy plugin (requires mypy >= 1.5): +And for the mypy plugin (requires mypy >= 1.7): ``` pip install type-enum-plugin ``` @@ -48,11 +48,6 @@ Then register the plugin with plugins = "type_enum_plugin" ``` -And then run mypy with the following flags (hopefully this soon won't be needed anymore): -``` ---enable-incomplete-feature=TypeVarTuple --enable-incomplete-feature=Unpack -``` - ## Advanced usage ### Exhaustiveness checking To make exhaustiveness checking work, you first need the mypy plugin, and then you also need to use the special `T` attribute that is synthesized for each `TypeEnum`: diff --git a/type-enum/poetry.lock b/type-enum/poetry.lock index 49d3e2b..35ff902 100644 --- a/type-enum/poetry.lock +++ b/type-enum/poetry.lock @@ -1,39 +1,39 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "mypy" -version = "1.5.1" +version = "1.7.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f33592ddf9655a4894aef22d134de7393e95fcbdc2d15c1ab65828eee5c66c70"}, - {file = "mypy-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:258b22210a4a258ccd077426c7a181d789d1121aca6db73a83f79372f5569ae0"}, - {file = "mypy-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9ec1f695f0c25986e6f7f8778e5ce61659063268836a38c951200c57479cc12"}, - {file = "mypy-1.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:abed92d9c8f08643c7d831300b739562b0a6c9fcb028d211134fc9ab20ccad5d"}, - {file = "mypy-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a156e6390944c265eb56afa67c74c0636f10283429171018446b732f1a05af25"}, - {file = "mypy-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ac9c21bfe7bc9f7f1b6fae441746e6a106e48fc9de530dea29e8cd37a2c0cc4"}, - {file = "mypy-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51cb1323064b1099e177098cb939eab2da42fea5d818d40113957ec954fc85f4"}, - {file = "mypy-1.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:596fae69f2bfcb7305808c75c00f81fe2829b6236eadda536f00610ac5ec2243"}, - {file = "mypy-1.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:32cb59609b0534f0bd67faebb6e022fe534bdb0e2ecab4290d683d248be1b275"}, - {file = "mypy-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:159aa9acb16086b79bbb0016145034a1a05360626046a929f84579ce1666b315"}, - {file = "mypy-1.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f6b0e77db9ff4fda74de7df13f30016a0a663928d669c9f2c057048ba44f09bb"}, - {file = "mypy-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26f71b535dfc158a71264e6dc805a9f8d2e60b67215ca0bfa26e2e1aa4d4d373"}, - {file = "mypy-1.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc3a600f749b1008cc75e02b6fb3d4db8dbcca2d733030fe7a3b3502902f161"}, - {file = "mypy-1.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:26fb32e4d4afa205b24bf645eddfbb36a1e17e995c5c99d6d00edb24b693406a"}, - {file = "mypy-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:82cb6193de9bbb3844bab4c7cf80e6227d5225cc7625b068a06d005d861ad5f1"}, - {file = "mypy-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4a465ea2ca12804d5b34bb056be3a29dc47aea5973b892d0417c6a10a40b2d65"}, - {file = "mypy-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9fece120dbb041771a63eb95e4896791386fe287fefb2837258925b8326d6160"}, - {file = "mypy-1.5.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d28ddc3e3dfeab553e743e532fb95b4e6afad51d4706dd22f28e1e5e664828d2"}, - {file = "mypy-1.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:57b10c56016adce71fba6bc6e9fd45d8083f74361f629390c556738565af8eeb"}, - {file = "mypy-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:ff0cedc84184115202475bbb46dd99f8dcb87fe24d5d0ddfc0fe6b8575c88d2f"}, - {file = "mypy-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8f772942d372c8cbac575be99f9cc9d9fb3bd95c8bc2de6c01411e2c84ebca8a"}, - {file = "mypy-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5d627124700b92b6bbaa99f27cbe615c8ea7b3402960f6372ea7d65faf376c14"}, - {file = "mypy-1.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:361da43c4f5a96173220eb53340ace68cda81845cd88218f8862dfb0adc8cddb"}, - {file = "mypy-1.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:330857f9507c24de5c5724235e66858f8364a0693894342485e543f5b07c8693"}, - {file = "mypy-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:c543214ffdd422623e9fedd0869166c2f16affe4ba37463975043ef7d2ea8770"}, - {file = "mypy-1.5.1-py3-none-any.whl", hash = "sha256:f757063a83970d67c444f6e01d9550a7402322af3557ce7630d3c957386fa8f5"}, - {file = "mypy-1.5.1.tar.gz", hash = "sha256:b031b9601f1060bf1281feab89697324726ba0c0bae9d7cd7ab4b690940f0b92"}, + {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"}, + {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"}, + {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"}, + {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"}, + {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"}, + {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"}, + {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"}, + {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"}, + {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"}, + {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"}, + {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"}, + {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"}, + {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"}, + {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"}, + {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"}, + {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"}, + {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"}, + {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"}, + {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"}, ] [package.dependencies] @@ -43,6 +43,7 @@ typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] install-types = ["pip"] +mypyc = ["setuptools (>=50)"] reports = ["lxml"] [[package]] @@ -84,10 +85,10 @@ files = [ [[package]] name = "type-enum-plugin" -version = "0.2.2a0" +version = "0.3.1a0" description = "Mypy plugin for type-enum." optional = false -python-versions = "^3.10" +python-versions = "^3.11" files = [] develop = true @@ -112,4 +113,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "22fbb2ab06c54487eed2ff0e9845d7a1835564490db5eb2378d771a8690b8a39" +content-hash = "de58e1cf4fe4b04127a6eb9bc4f963d5deb3c42cfad923aa948a258bb541e418" diff --git a/type-enum/pyproject.toml b/type-enum/pyproject.toml index c1b182a..f1efc26 100644 --- a/type-enum/pyproject.toml +++ b/type-enum/pyproject.toml @@ -20,7 +20,7 @@ python = "^3.11" [tool.poetry.group.dev.dependencies] ruff = "^0.0.254" -mypy = "^1.5.1" +mypy = "^1.7.1" type-enum-plugin = { path = "../type-enum-plugin", develop = true } typing-extensions = "^4.5.0" diff --git a/type-enum/tests/test_instantiation.py b/type-enum/tests/test_instantiation.py index 9d1f9af..e6912f4 100644 --- a/type-enum/tests/test_instantiation.py +++ b/type-enum/tests/test_instantiation.py @@ -72,6 +72,10 @@ def f(x: Maybe.T[int]) -> int: return 0 self.assertEqual(f(a), 3) + self.assertEqual(repr(Maybe.Some), "") + + with self.assertRaises(TypeError): + Maybe.Some[int, str](3) # type: ignore[misc] def test_generic_multi(self) -> None: U = TypeVar("U") @@ -92,6 +96,7 @@ def f(x: E.T[str, int]) -> int: return u self.assertEqual(f(a), 3) + self.assertEqual(repr(a), "E.A(3)") def test_invalid_body(self) -> None: with self.assertRaises(TypeError): diff --git a/type-enum/type_enum/_core.py b/type-enum/type_enum/_core.py index c1f04c8..ac238b8 100644 --- a/type-enum/type_enum/_core.py +++ b/type-enum/type_enum/_core.py @@ -1,5 +1,6 @@ import functools from operator import or_ +from types import new_class from typing import ( Any, Generic, @@ -60,12 +61,15 @@ def __new__(cls, name: str, bases: tuple[type, ...], ns: dict[str, Any]): if len(types) == 1 and types[0] == (): types = () # subtype = _create_tuple_class(name, attr_name, types) - subtype = _create_subclass(name, attr_name, types) + typevars: tuple[type, ...] = tuple( + typ for typ in types if isinstance(typ, TypeVar) + ) + subtype = _create_subclass( + name, attr_name, types, typevars, ns["__module__"] + ) ns[attr_name] = subtype - member_map[attr_name] = Entry( - subtype, tuple(typ for typ in types if isinstance(typ, TypeVar)) - ) + member_map[attr_name] = Entry(subtype, typevars) for attr_name in ns: if not is_dunder(attr_name) and attr_name not in typ_anns: @@ -112,19 +116,32 @@ def __iter__(cls) -> Iterator[type]: return (typ for typ, _ in cls._member_map.values()) -def _create_subclass(basename: str, typename: str, types: tuple[type, ...]): - subtype = NamedTuple(typename, [(f"field{i}", typ) for i, typ in enumerate(types)]) +def _create_subclass( + basename: str, + typename: str, + types: tuple[type, ...], + typevars: tuple[type, ...], + module: str, +) -> type: + def body(namespace: dict[str, Any]) -> None: + namespace["__module__"] = module + namespace["__annotations__"] = {f"field{i}": typ for i, typ in enumerate(types)} + num_values = len(types) + repr_fmt = "(" + ", ".join("%r" for _ in range(num_values)) + ")" + + def __repr__(self) -> str: + "Return a nicely formatted representation string" + return basename + "." + self.__class__.__name__ + repr_fmt % tuple(self) + + namespace["__repr__"] = __repr__ + + baseclasses = (NamedTuple,) + if typevars: + baseclasses += (Generic[*typevars],) + subtype = new_class(typename, baseclasses, exec_body=body) subtype.__doc__ = ( f"{basename}.{typename}(" + ", ".join(type_to_str(typ) for typ in types) + ")" ) - num_values = len(types) - repr_fmt = "(" + ", ".join("%r" for _ in range(num_values)) + ")" - - def __repr__(self) -> str: - "Return a nicely formatted representation string" - return basename + "." + self.__class__.__name__ + repr_fmt % tuple(self) - - subtype.__repr__ = __repr__ return subtype