From 01101362d3e5b10dbe6a9b28f1ae9540f5037768 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Tue, 20 Oct 2026 00:14:57 +0200 Subject: [PATCH] Pyrefly typing tests --- CONTRIBUTING.md | 5 ++-- pyproject.toml | 4 +++- tests/test_typing_structure.md | 44 ++++++++++++++++++++++++++-------- uv.lock | 29 ++++++++++++++++++---- 4 files changed, 65 insertions(+), 17 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 43ed5a8c..3c566743 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -121,13 +121,14 @@ To run a subset of tests: $ just test tests/test_unstructure.py ``` -Static typing tests live in `tests/test_typing_structure.md` and run through `pytest-typing` with Mypy on CPython 3.14. +Static typing tests live in `tests/test_typing_structure.md` and run through `pytest-typing` with Mypy and Pyrefly on CPython 3.14. To run them on their own: ```shell $ just --set python python3.14 test tests/test_typing_structure.md ``` -The test dependencies include a Mypy version with `TypeForm` support enabled by default. +The test dependencies pin both checkers in `uv.lock`, including a Mypy version with `TypeForm` support enabled by default. +`pytest-typing` is configured to use Pyrefly's `strict` preset for the temporary test snippets. [llm]: https://github.com/python-attrs/cattrs/blob/main/.github/AI_POLICY.md diff --git a/pyproject.toml b/pyproject.toml index 73e9ce2a..9a87995d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ test = [ "pytest>=8.4.1", "pytest-typing; python_version == '3.14' and implementation_name == 'cpython'", "mypy; python_version == '3.14' and implementation_name == 'cpython'", + "pyrefly; python_version == '3.14' and implementation_name == 'cpython'", "pytest-benchmark>=5.1.0", "immutables>=0.21", "coverage>=7.9.2", @@ -39,7 +40,8 @@ addopts = [ "--benchmark-warmup-iterations=5", "--benchmark-group-by=fullname", ] -typing_checkers = ["mypy"] +typing_checkers = ["mypy", "pyrefly"] +typing_pyrefly_preset = "strict" [project] name = "cattrs" diff --git a/tests/test_typing_structure.md b/tests/test_typing_structure.md index abaa5017..11859dd3 100644 --- a/tests/test_typing_structure.md +++ b/tests/test_typing_structure.md @@ -6,6 +6,7 @@ These tests exercise the static return types for the structuring APIs. ```python from attrs import define +from typing_extensions import assert_type from cattrs import structure @@ -17,7 +18,7 @@ class User: user = structure({"id": 1, "name": "Ada"}, User) -reveal_type(user) # revealed: test_snippet.User +assert_type(user, User) name: str = user.name ``` @@ -27,6 +28,8 @@ name: str = user.name ```python from dataclasses import dataclass +from typing_extensions import assert_type + from cattrs import Converter @@ -38,7 +41,7 @@ class Point: converter = Converter() point = converter.structure({"x": 1, "y": 2}, Point) -reveal_type(point) # revealed: test_snippet.Point +assert_type(point, Point) x: int = point.x ``` @@ -48,6 +51,8 @@ x: int = point.x ```python from typing import TypedDict +from typing_extensions import assert_type + from cattrs import Converter @@ -58,7 +63,7 @@ class Movie(TypedDict): converter = Converter() movie = converter.structure({"title": "Alien", "year": 1979}, Movie) -reveal_type(movie) # revealed: TypedDict(test_snippet.Movie, {"title": str, "year": int}) +assert_type(movie, Movie) title: str = movie["title"] ``` @@ -66,12 +71,14 @@ title: str = movie["title"] ## Converter structure returns homogeneous tuples ```python +from typing_extensions import assert_type + from cattrs import Converter converter = Converter() numbers = converter.structure([1, 2, 3], tuple[int, ...]) -reveal_type(numbers) # revealed: tuple[int, ...] +assert_type(numbers, tuple[int, ...]) first: int = numbers[0] ``` @@ -79,12 +86,14 @@ first: int = numbers[0] ## Converter structure returns heterogeneous tuples ```python +from typing_extensions import assert_type + from cattrs import Converter converter = Converter() row = converter.structure(["Ada", 37, True], tuple[str, int, bool]) -reveal_type(row) # revealed: tuple[str, int, bool] +assert_type(row, tuple[str, int, bool]) name: str = row[0] age: int = row[1] @@ -100,7 +109,9 @@ from cattrs import Converter converter = Converter() value: int = converter.structure("1", int) -bad: str = converter.structure("1", int) # mypy-error: [assignment] +# mypy-error: [assignment] +# pyrefly-error: [bad-assignment] +bad: str = converter.structure("1", int) ``` ## Hook factory decorators preserve factory types @@ -109,6 +120,8 @@ bad: str = converter.structure("1", int) # mypy-error: [assignment] from collections.abc import Callable from typing import Any +from typing_extensions import assert_type + from cattrs import Converter @@ -129,8 +142,15 @@ def structure_factory(cl: type[int]) -> Callable[[str, type[int]], int]: return lambda value, _: int(value) -reveal_type(unstructure_factory) # revealed: def (cl: type[int]) -> def (int) -> str -reveal_type(structure_factory) # revealed: def (cl: type[int]) -> def (str, type[int]) -> int +assert_type(unstructure_factory(cl=int), Callable[[int], str]) +assert_type(structure_factory(cl=int), Callable[[str, type[int]], int]) + +# mypy-error: [arg-type] +# pyrefly-error: [bad-argument-type] +unstructure_factory(str) +# mypy-error: [arg-type] +# pyrefly-error: [bad-argument-type] +structure_factory(str) ``` ## Structure accepts type forms and preserves their result types @@ -181,8 +201,12 @@ def convert(value: object, target: TypeForm[T]) -> T: assert_type(convert("1", int | None), int | None) -bad: str = converter.structure("1", int | None) # mypy-error: [assignment] -converter.structure("1", 42) # mypy-error: [arg-type] +# mypy-error: [assignment] +# pyrefly-error: [bad-assignment] +bad: str = converter.structure("1", int | None) +# mypy-error: [arg-type] +# pyrefly-error: [bad-argument-type] +converter.structure("1", 42) ``` ## Structure hook lookup preserves type forms diff --git a/uv.lock b/uv.lock index 08435c85..f4075872 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.15'", @@ -187,6 +187,7 @@ test = [ { name = "hypothesis" }, { name = "immutables" }, { name = "mypy", marker = "python_full_version == '3.14.*' and implementation_name == 'cpython'" }, + { name = "pyrefly", marker = "python_full_version == '3.14.*' and implementation_name == 'cpython'" }, { name = "pytest" }, { name = "pytest-benchmark" }, { name = "pytest-typing", marker = "python_full_version == '3.14.*' and implementation_name == 'cpython'" }, @@ -227,6 +228,7 @@ test = [ { name = "hypothesis", specifier = ">=6.135.26" }, { name = "immutables", specifier = ">=0.21" }, { name = "mypy", marker = "python_full_version == '3.14.*' and implementation_name == 'cpython'" }, + { name = "pyrefly", marker = "python_full_version == '3.14.*' and implementation_name == 'cpython'" }, { name = "pytest", specifier = ">=8.4.1" }, { name = "pytest-benchmark", specifier = ">=5.1.0" }, { name = "pytest-typing", marker = "python_full_version == '3.14.*' and implementation_name == 'cpython'" }, @@ -1327,6 +1329,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/29/ce5d2dcb095debb3bed0654950c45207a7d138e194abef0ded4b86de1dc1/pyperf-2.9.0-py3-none-any.whl", hash = "sha256:215673fb60f3fbbc6c7a90b609b0806d8466fded8dd502bf5731e2b92506076e", size = 144204, upload-time = "2025-03-04T13:45:40.491Z" }, ] +[[package]] +name = "pyrefly" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/78/b6ec12acb586606618c71ebe1008af59e1ca50e667895f1946fa7b656619/pyrefly-1.3.2.tar.gz", hash = "sha256:30a68ff5429a1546ec08a2d913dfe442c9553a2940d274de18833d8eda939b5e", size = 6679591, upload-time = "2026-09-28T22:10:06.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/01/144a78839822abad55628881bcb45f8173e6d8b87976897c9105884b5a6b/pyrefly-1.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:626dbaee16ae1169a10a4ba3293583b5ab0320102f35efaad9e03457a2fc646c", size = 15088488, upload-time = "2026-09-28T22:09:40.743Z" }, + { url = "https://files.pythonhosted.org/packages/23/aa/599756bd42e6533ee9261ebd288f846ab453b00debaef2b4789893967809/pyrefly-1.3.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2d5fc704dbf0e990dc4f15cb1ead64f1a325ae27383a50c76cfcaf0387f926bb", size = 14429996, upload-time = "2026-09-28T22:09:43.316Z" }, + { url = "https://files.pythonhosted.org/packages/90/0c/4b0678889136b1514ecd25cec11974d5bfdaeaaf9f5fe8a766ca45ec4ab5/pyrefly-1.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89dbe8a33e735fa96023c33ce5dfd81af9b0bdd2ee771e8d69b2ebffc7e0fbc7", size = 14871810, upload-time = "2026-09-28T22:09:45.42Z" }, + { url = "https://files.pythonhosted.org/packages/a6/4e/a96236391c6bb0d8b50af85e0ea6c0c56206f739931ac3031f9e000325ee/pyrefly-1.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ec0c31715d48bff279f548b85a93b5bc43d2c7bd1492a8cd703a9185cb089e5", size = 16113462, upload-time = "2026-09-28T22:09:47.619Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c3/d4d0966aa7008ebd8481a6d05aa53202ad40c9597705eeee87ba6e0e2025/pyrefly-1.3.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b3de468bed691e40dc6271284c8dcc05e649d7ed45280ef3d07332741efe34c", size = 16043341, upload-time = "2026-09-28T22:09:50.083Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d1/52d9ea1b7048c7316d9ff7594c170b1c06d37547a75d140c8deea7b9ddfd/pyrefly-1.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb94365fe5f854cb4fe82f600c4e2453160d328610b12981004851ee0b6e8aeb", size = 15437183, upload-time = "2026-09-28T22:09:52.56Z" }, + { url = "https://files.pythonhosted.org/packages/08/ec/24f83e4a397fa9d5c416bb4d378ee1bb18fe48bdd5698fe541636f5352f3/pyrefly-1.3.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4f730d2e515ac25827fc68f343a7aa02b1072e0062df6a430346a4ffdfb5db2f", size = 14892976, upload-time = "2026-09-28T22:09:54.916Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/a473f8e9ec31ba0682ebe44f4e51167a22a4cbdc8bfd87365b9aca23be20/pyrefly-1.3.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:60f440e1626bd5cc4a41ee90cecba80796ed01ebd6c15d9f933efb8d07ffe2b8", size = 15469689, upload-time = "2026-09-28T22:09:57.541Z" }, + { url = "https://files.pythonhosted.org/packages/d0/df/779092365e8e469fc8e7349e1c26c35b8a73df9e6be80a00265fc26a2b2b/pyrefly-1.3.2-py3-none-win32.whl", hash = "sha256:b31684189de44a25a5bf00bdf3ad16be44a7b14654702d6018bce1043357202c", size = 14135550, upload-time = "2026-09-28T22:09:59.757Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/466333e7f2891f8c357b5c226559c39ba8dde4f90be09189ecc771a086f5/pyrefly-1.3.2-py3-none-win_amd64.whl", hash = "sha256:693960cdb247d1f76b9e09b3f2554dae20034f7d442ccd4e9b3e8962a3622363", size = 15067523, upload-time = "2026-09-28T22:10:02.099Z" }, + { url = "https://files.pythonhosted.org/packages/18/32/ee66df92c692c0686d82c9ecef3876ccb0386aa318bf507016f0121a8f78/pyrefly-1.3.2-py3-none-win_arm64.whl", hash = "sha256:f7424b21331670522980ff13e08bd143659879a4d92f4832282c7d7c41a9b5d6", size = 14348271, upload-time = "2026-09-28T22:10:04.253Z" }, +] + [[package]] name = "pytest" version = "9.0.3" @@ -1360,14 +1381,14 @@ wheels = [ [[package]] name = "pytest-typing" -version = "26.3.0" +version = "26.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/3f/982c57ce061a6fd667e85b1256eb3ad5d67dc56ba53d430b3bd9aca26716/pytest_typing-26.3.0.tar.gz", hash = "sha256:c0e155612088bd30351bd6a38fea2bf039a280b5303423e8c3eebe568c180b20", size = 65028, upload-time = "2026-03-24T21:31:32.923Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/40/88fdd6dd1c4c2dd632b6b6b278160d411a396c720f4c9e4e7d17d93f7633/pytest_typing-26.5.0.tar.gz", hash = "sha256:102858c2a93b9d077996e259f399eebb3458fd019c474f936847517f0e0b2523", size = 66470, upload-time = "2026-10-13T22:03:54.301Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/c1/8c4b27724dc0cc0c566e7b1a27a1afd4497d6c5b9cc93a3f9330c742ac12/pytest_typing-26.3.0-py3-none-any.whl", hash = "sha256:b44a990622e8f77684b0f35c13e1d8711fd20a43c53a53b378619d69bea47007", size = 17594, upload-time = "2026-03-24T21:31:31.48Z" }, + { url = "https://files.pythonhosted.org/packages/6b/19/26b8f3a4f0c67dc95041fb1475e761f47974e88c800d410fa21bec6bf5bd/pytest_typing-26.5.0-py3-none-any.whl", hash = "sha256:96ad768a7aa7ee71a8ec6c85c3c330101f6be4bc091f400e54c98d9a6b46364f", size = 18043, upload-time = "2026-10-13T22:03:53.145Z" }, ] [[package]]