diff --git a/pyproject.toml b/pyproject.toml index 0fc91b46b..5a29fe677 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ tests = [ cov = [{ include-group = "tests" }, "coverage[toml]"] pyright = ["pyright", { include-group = "tests" }] ty = ["ty", { include-group = "tests" }] -pyrefly = ["pyrefly", { include-group = "tests" }] +pyrefly = ["pyrefly>=1.2.0-dev.3", { include-group = "tests" }] benchmark = [ { include-group = "tests" }, "pytest-codspeed", @@ -77,8 +77,8 @@ dev = [{ include-group = "tests" }, "ruff"] exclude-newer = "1 week" [tool.uv.exclude-newer-package] -# Need latest ABI wheels for 3.15. Remove 2026-07-18. -hypothesis = false +pyrefly = false +hypothesis = false # Need latest ABI wheels for 3.15. Remove 2026-07-18. [tool.hatch.version] @@ -327,7 +327,12 @@ showcontent = true pretty = true disallow_untyped_defs = true check_untyped_defs = true +exclude = ["typing-examples/pyrefly.py"] [tool.pyright] include = ["typing-examples/baseline.py"] + + +[tool.pyrefly] +project-includes = ["typing-examples"] diff --git a/tox.ini b/tox.ini index 445cc48ae..ad615f3f5 100644 --- a/tox.ini +++ b/tox.ini @@ -135,7 +135,7 @@ commands = ty check typing-examples/baseline.py [testenv:typing-pyrefly] dependency_groups = pyrefly -commands = pyrefly check typing-examples/baseline.py +commands = pyrefly check [testenv:docset] diff --git a/typing-examples/pyrefly.py b/typing-examples/pyrefly.py new file mode 100644 index 000000000..094fb4ecb --- /dev/null +++ b/typing-examples/pyrefly.py @@ -0,0 +1,394 @@ +# SPDX-License-Identifier: MIT + +""" +Typing examples that rely on Pyrefly's advanced attrs integration. +""" + +from __future__ import annotations + +import re + +from typing import Any + +import attrs + + +@attrs.define +class C: + a: int = attrs.field() + + +cc = C(1) +C(a=1) + + +@attrs.define +class D: + x: list[int] = attrs.field() + + +li: list[int] = D([1]).x + + +@attrs.define +class E: + y: "list[int]" = attrs.field() + + +li = E([1]).y + + +@attrs.define +class F: + z: Any = attrs.field() + + +# Inheritance -- + + +@attrs.define +class GG(D): + y: str = attrs.field() + + +GG(x=[1], y="foo") + + +@attrs.define +class HH(D, E): + z: float = attrs.field() + + +HH(x=[1], y=[], z=1.1) + + +# Exceptions +@attrs.define +class Error(Exception): + x: int = attrs.field() + + +try: + raise Error(1) +except Error as e: + e.x + e.args + str(e) + + +@attrs.define(auto_exc=False) +class Error2(Exception): + x: int + + +try: + raise Error2(1) +except Error as e: + e.x + e.args + str(e) + +# Field aliases + + +@attrs.define +class AliasExample: + without_alias: int + _with_alias: int = attrs.field(alias="_with_alias") + + +attrs.fields(AliasExample).without_alias.alias +attrs.fields(AliasExample)._with_alias.alias + + +# Converters + + +@attrs.define +class ConvCOptional: + x: int | None = attrs.field(converter=attrs.converters.optional(int)) + + +ConvCOptional(1) +ConvCOptional(None) + + +@attrs.define +class ConvCPipe: + x: str = attrs.field(converter=attrs.converters.pipe(int, str)) + + +ConvCPipe(3.4) +ConvCPipe("09") + + +@attrs.define +class ConvCDefaultIfNone: + x: int = attrs.field(converter=attrs.converters.default_if_none(42)) + + +ConvCDefaultIfNone(1) +ConvCDefaultIfNone(None) + + +@attrs.define +class ConvCToBool: + x: int = attrs.field(converter=attrs.converters.to_bool) + + +ConvCToBool(1) +ConvCToBool(True) +ConvCToBool("on") +ConvCToBool("yes") +ConvCToBool(0) +ConvCToBool(False) +ConvCToBool("n") + + +@attrs.define +class DecoratorConverter: + x: int = attrs.field() + + @x.converter + def _to_int(self, field: attrs.Attribute, val: str | float) -> int: + return int(val) + + +DecoratorConverter("foo") + + +# Validators +@attrs.define +class Validated: + a: list[C] = attrs.field( + validator=attrs.validators.deep_iterable( + attrs.validators.instance_of(C), attrs.validators.instance_of(list) + ), + ) + a2: tuple[C] = attrs.field( + validator=attrs.validators.deep_iterable( + attrs.validators.instance_of(C), + attrs.validators.instance_of(tuple), + ), + ) + a3: tuple[C] = attrs.field( + validator=attrs.validators.deep_iterable( + [attrs.validators.instance_of(C)], + [attrs.validators.instance_of(tuple)], + ), + ) + b: list[C] = attrs.field( + validator=attrs.validators.deep_iterable( + attrs.validators.instance_of(C) + ), + ) + c: dict[C, D] = attrs.field( + validator=attrs.validators.deep_mapping( + attrs.validators.instance_of(C), + attrs.validators.instance_of(D), + attrs.validators.instance_of(dict), + ), + ) + d: dict[C, D] = attrs.field( + validator=attrs.validators.deep_mapping( + attrs.validators.instance_of(C), attrs.validators.instance_of(D) + ), + ) + d2: dict[C, D] = attrs.field( + validator=attrs.validators.deep_mapping( + attrs.validators.instance_of(C) + ), + ) + d3: dict[C, D] = attrs.field( + validator=attrs.validators.deep_mapping( + value_validator=attrs.validators.instance_of(C) + ), + ) + d4: dict[C, D] = attrs.field( + validator=attrs.validators.deep_mapping( + key_validator=[attrs.validators.instance_of(C)], + value_validator=[attrs.validators.instance_of(C)], + mapping_validator=[attrs.validators.instance_of(dict)], + ), + ) + e: str = attrs.field( + validator=attrs.validators.matches_re(re.compile(r"foo")) + ) + f: str = attrs.field( + validator=attrs.validators.matches_re(r"foo", flags=42, func=re.search) + ) + + # Test different forms of instance_of + g: int = attrs.field(validator=attrs.validators.instance_of(int)) + h: int = attrs.field(validator=attrs.validators.instance_of((int,))) + j: int | str = attrs.field( + validator=attrs.validators.instance_of((int, str)) + ) + k: int | str | C = attrs.field( + validator=attrs.validators.instance_of((int, C, str)) + ) + kk: int | str | C = attrs.field( + validator=attrs.validators.instance_of(int | C | str) + ) + + l: Any = attrs.field( + validator=attrs.validators.not_(attrs.validators.in_("abc")) + ) + m: Any = attrs.field( + validator=attrs.validators.not_( + attrs.validators.in_("abc"), exc_types=ValueError + ) + ) + n: Any = attrs.field( + validator=attrs.validators.not_( + attrs.validators.in_("abc"), exc_types=(ValueError,) + ) + ) + o: Any = attrs.field( + validator=attrs.validators.not_( + attrs.validators.in_("abc"), msg="spam" + ) + ) + p: Any = attrs.field( + validator=attrs.validators.not_(attrs.validators.in_("abc"), msg=None) + ) + q: Any = attrs.field( + validator=attrs.validators.optional(attrs.validators.instance_of(C)) + ) + r: Any = attrs.field( + validator=attrs.validators.optional([attrs.validators.instance_of(C)]) + ) + s: Any = attrs.field( + validator=attrs.validators.optional((attrs.validators.instance_of(C),)) + ) + + +@attrs.define +class Validated2: + num: int = attrs.field(validator=attrs.validators.ge(0)) + + +with attrs.validators.disabled(): + Validated2(num=-1) + + +try: + attrs.validators.set_disabled(True) + Validated2(num=-1) +finally: + attrs.validators.set_disabled(False) + + +# Custom repr() +@attrs.define +class WithCustomRepr: + a: int = attrs.field(repr=True) + b: str = attrs.field(repr=False) + c: str = attrs.field(repr=lambda value: "c is for cookie") + d: bool = attrs.field(repr=str) + + +# Check some of our own types +@attrs.define(eq=True, order=False) +class OrderFlags: + a: int = attrs.field(eq=False, order=False) + b: int = attrs.field(eq=True, order=True) + + +# on_setattr hooks +@attrs.define(on_setattr=attrs.setters.validate) +class ValidatedSetter: + a: int + b: str = attrs.field(on_setattr=attrs.setters.NO_OP) + c: bool = attrs.field(on_setattr=attrs.setters.frozen) + d: int = attrs.field( + converter=int, + on_setattr=[attrs.setters.convert, attrs.setters.validate], + ) + e: bool = attrs.field( + converter=attrs.converters.to_bool, + on_setattr=attrs.setters.pipe( + attrs.setters.convert, attrs.setters.validate + ), + ) + + +vs = ValidatedSetter(1, "2", True, 4, False) +vs.d = "2" +vs.e = "yes" +vs.e = "foo" # XXX: should only allow the literals we know + + +# field_transformer +def ft_hook( + cls: type, attribs: list[attrs.Attribute] +) -> list[attrs.Attribute]: + return attribs + + +@attrs.define(field_transformer=ft_hook) +class TransformedAttrs: + x: int + + +# Auto-detect +@attrs.define(auto_detect=True) +class AutoDetect: + x: int + + def __init__(self, x: int): + self.x = x + + +@attrs.define(order=True) +class NGClass: + x: int = attrs.field(default=42) + + +ngc = NGClass(1) + + +@attrs.frozen(str=True) +class NGFrozen: + x: int + + +attrs.fields(NGFrozen).x.evolve(eq=False) +a = attrs.fields(NGFrozen).x +a.evolve(repr=False) + + +@attrs.define +class FactoryTest: + a: list[int] = attrs.field(default=attrs.Factory(list)) + b: list[Any] = attrs.field(default=attrs.Factory(list, False)) + c: list[int] = attrs.field(default=attrs.Factory((lambda s: s.a), True)) + + +attrs.asdict(FactoryTest(), tuple_keys=True) + + +# Check match_args stub +@attrs.define(match_args=False) +class MatchArgs: + a: int = attrs.field() + b: int = attrs.field() + + +attrs.asdict(FactoryTest()) +attrs.asdict(FactoryTest(), retain_collection_types=False) + + +foo = object +if attrs.has(foo) or attrs.has(foo): + foo.__attrs_attrs__ + + +@attrs.define(unsafe_hash=True) +class Hashable: + pass + + +def test(cls: type) -> None: + if attrs.has(cls): + attrs.resolve_types(cls) diff --git a/uv.lock b/uv.lock index 546c0788d..3235131d4 100644 --- a/uv.lock +++ b/uv.lock @@ -17,6 +17,7 @@ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for exclude-newer-span = "P1W" [options.exclude-newer-package] +pyrefly = false hypothesis = false [[package]] @@ -316,7 +317,7 @@ pyrefly = [ { name = "cloudpickle", marker = "platform_python_implementation == 'CPython'" }, { name = "hypothesis" }, { name = "pympler" }, - { name = "pyrefly" }, + { name = "pyrefly", specifier = ">=1.2.0.dev3" }, { name = "pytest" }, { name = "pytest-xdist", extras = ["psutil"] }, ] @@ -1595,21 +1596,21 @@ wheels = [ [[package]] name = "pyrefly" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/20/976165fa4b1517a1a92f393b3f4d4badabfff1165eff09d4cd4908428183/pyrefly-1.1.1.tar.gz", hash = "sha256:6deda959f8603a7dbdf112c48983e2275b2903cf33c8c739ed65d7e71a4fd520", size = 5880491, upload-time = "2026-06-18T23:45:43.785Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/d6/02ba666018c6a1cb4ddfa2db98ada721adddd374db5c29ba47a0bf2637fa/pyrefly-1.1.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f4b8595f91885bc8b5e3c282ab68d1df21201668a84e6508b1e15f2feec0bb8d", size = 13631867, upload-time = "2026-06-18T23:45:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/71/47/7a3457dbbddb513a83cf4fe527d5d5ebda5201a1010ad2a6034030e3e358/pyrefly-1.1.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6b238e1362622d47a6eb5af704fd8b613c94e8c303386efd6350e3da59fecc8", size = 13075304, upload-time = "2026-06-18T23:45:16.865Z" }, - { url = "https://files.pythonhosted.org/packages/84/df/70f4b3f42d58ed686a80df31e04eca54d88036cea4f9b96195c64ad0b2b5/pyrefly-1.1.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b50d4510e4f8aaea79e2c4b343a4d7a060c9451c0b2aa9bfe10d7ca1ef33d68d", size = 13446966, upload-time = "2026-06-18T23:45:19.644Z" }, - { url = "https://files.pythonhosted.org/packages/3c/53/12a19bd6c7af985bcbc13c6910d0f9f6684069ead2282a5c08c2bfbb5d03/pyrefly-1.1.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f330cf039ef3da3b910c84f3a7e431f0cf8d0c1d2dad26491d6cadf3c7cd4759", size = 14449222, upload-time = "2026-06-18T23:45:22.252Z" }, - { url = "https://files.pythonhosted.org/packages/93/f0/e55c48a50076fc0f9ecf4bdedec50456db383e01162f5e2121f8468be071/pyrefly-1.1.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6342d87c52b04f72156da04f554c4d57f3616f2b32d1763969efb22d05a1407", size = 14472947, upload-time = "2026-06-18T23:45:24.858Z" }, - { url = "https://files.pythonhosted.org/packages/b6/e7/30e085b31fed978ecb675bdbb54df566673ab550469e5af2d350f6af0be6/pyrefly-1.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c08b814ad03175e9cf47111390537161828b472044c39ab3320252b3ac6b2edd", size = 13975252, upload-time = "2026-06-18T23:45:27.247Z" }, - { url = "https://files.pythonhosted.org/packages/47/58/49c3e67641133d3fe5d8d9a660dc0826c6c37ca197d86cad05fa7dd8bfd6/pyrefly-1.1.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d50cad97f19fc893b04deff7239626cffff5dd27ffb29b7d303a1b770247b208", size = 13471780, upload-time = "2026-06-18T23:45:29.775Z" }, - { url = "https://files.pythonhosted.org/packages/71/1e/65a7ba8355e2c39d8331832905fb74dcc85fc122a3f1dfd6dbf2a88907ad/pyrefly-1.1.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2150b450ee6a6bcbe69b2d45d9a4ebc934a609e1abcf65e490433f38eb873d84", size = 13989306, upload-time = "2026-06-18T23:45:32.576Z" }, - { url = "https://files.pythonhosted.org/packages/37/de/b7ee1ab2392c36945738246fba7524439810befa3cfcc03cb6157567fc10/pyrefly-1.1.1-py3-none-win32.whl", hash = "sha256:5ffd8a8ed62fe4e6bf0afe1837d1bad149bb3b9f80e928ef248c96b836db3742", size = 12608469, upload-time = "2026-06-18T23:45:35.419Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9c/a0f5b52934bf80e9c7eff08222e7caf318287b9aef76acb8d9ac5740581b/pyrefly-1.1.1-py3-none-win_amd64.whl", hash = "sha256:4e0430f3ef69c8ac73505fd6584db70ed504665a9f0816fef7f723de510f26cb", size = 13502172, upload-time = "2026-06-18T23:45:38.375Z" }, - { url = "https://files.pythonhosted.org/packages/42/3d/4c6bcb3d456835f51445d3662a428f56c3ea5643ec798c577030ae34298c/pyrefly-1.1.1-py3-none-win_arm64.whl", hash = "sha256:83baf0db71e172665db1fca0ced50b8f7773f5192ca57e8ac6773a772b6d2fc5", size = 12895979, upload-time = "2026-06-18T23:45:41.026Z" }, +version = "1.2.0.dev3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/ae/78a02bb1fcb953a37565bd53d1f71014e8316dbc1e4e1bf37889423f72f1/pyrefly-1.2.0.dev3.tar.gz", hash = "sha256:e9bdeb0a1a547e1d165be71aa0853e968a05effccb61c67b42f22c270667165e", size = 6197548, upload-time = "2026-07-24T20:54:07.906Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/d3/71ea7545934a2633991ad2c551aac92de8afef4eb24a34a315bb64f71bcf/pyrefly-1.2.0.dev3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe9cf4922c3dbb2c694efa6683aa79b02f2a4f3a3f01252784a2d33b550b815d", size = 13902998, upload-time = "2026-07-24T20:53:36.607Z" }, + { url = "https://files.pythonhosted.org/packages/78/90/25935e5da71a09f0ca8357a182494bed34efd7d60053b8bf7ec17dbeb1ac/pyrefly-1.2.0.dev3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d8910e5e56405680b071e0dce065fc2eb0fc111a042fca309982206cee21db6", size = 13358037, upload-time = "2026-07-24T20:53:39.542Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/999e1e4299534eeb2444b0cf18093b5a0cf3cb01244691f836443a0e9770/pyrefly-1.2.0.dev3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35c4c4c9613f60d6f408bd33f142dabd519953f034ace4ec0fd8c4eda70a06dd", size = 13772678, upload-time = "2026-07-24T20:53:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/81/e21fbc80b28e0ac3203a7427fa197cd93339dcc0263cc1e9be8be3070eb1/pyrefly-1.2.0.dev3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:347a563e427f39f8b4e3b5ff997d7b3b4d9438f1917c0ba543394d08fbcb7bb5", size = 14807454, upload-time = "2026-07-24T20:53:45.12Z" }, + { url = "https://files.pythonhosted.org/packages/0c/0a/cfa2f8121b3a54d22d1441c52757d73257be466faf71c1eafb34dac48008/pyrefly-1.2.0.dev3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e9e220d60a9f115ce789321cdca11087484572cc3349a330a13e7ff3408995bb", size = 14820987, upload-time = "2026-07-24T20:53:47.983Z" }, + { url = "https://files.pythonhosted.org/packages/34/ce/ffc6c589544ca1631c16af7d776ebd4010dd1968659b189d70ed7135e3d3/pyrefly-1.2.0.dev3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06d9434c5e475baa6db45c346f9bdcf30c89cb19facb97feb4d4ab311431f25b", size = 14254722, upload-time = "2026-07-24T20:53:50.795Z" }, + { url = "https://files.pythonhosted.org/packages/32/a1/6c0438993f8fa5ddb60076b3e52eb47cf83036d268a6da1b1aad3692258b/pyrefly-1.2.0.dev3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:465ff2b1a49331ac2a07ae9a1efe8b7b351493333870ad30f111c8a0fb4e22ae", size = 13796734, upload-time = "2026-07-24T20:53:53.813Z" }, + { url = "https://files.pythonhosted.org/packages/00/91/52a1507687fa84a6b5fbb6af3c9158b522e90436b5cbbec499c6e6abc4c2/pyrefly-1.2.0.dev3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:44a1c896e3206a57d0c60aac7b12c01e6c1d629d557ef2a541a48e079c5c3bf7", size = 14295787, upload-time = "2026-07-24T20:53:57.199Z" }, + { url = "https://files.pythonhosted.org/packages/14/76/0d8a77e9d2bcaa555a66df13c3ed14111e4180270cfd002282654e55c452/pyrefly-1.2.0.dev3-py3-none-win32.whl", hash = "sha256:946b1c8d5d41139d5931ba16bbb567d02e2f8fe9d35a18c812f782e3cce0df1f", size = 13019850, upload-time = "2026-07-24T20:54:00.058Z" }, + { url = "https://files.pythonhosted.org/packages/74/d9/3f543aca87864d3e360a3205d6152d84849696c9be134883976e37d1c524/pyrefly-1.2.0.dev3-py3-none-win_amd64.whl", hash = "sha256:de77594256092309a529a7d89a92ad0c4775f82bd4082235de792b96324216eb", size = 13967675, upload-time = "2026-07-24T20:54:02.786Z" }, + { url = "https://files.pythonhosted.org/packages/ee/27/a52738bfb8e835fb325984e49c4d00362859b6a99b4c244164145b27010f/pyrefly-1.2.0.dev3-py3-none-win_arm64.whl", hash = "sha256:a407c6a0c9374cf7a1bdb9e0769ce88fbbfb1ee1d88a056bebc7c7e1c6bbbdbc", size = 13326689, upload-time = "2026-07-24T20:54:05.463Z" }, ] [[package]]