diff --git a/pyproject.toml b/pyproject.toml index 0fc91b46b..f33002b96 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.2", { 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..e42a9de8a --- /dev/null +++ b/typing-examples/pyrefly.py @@ -0,0 +1,397 @@ +# 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") +ConvCPipe({}) # XXX pipe makes field Any + + +@attrs.define +class ConvCDefaultIfNone: + x: int = attrs.field(converter=attrs.converters.default_if_none(42)) + + +ConvCDefaultIfNone(1) +ConvCDefaultIfNone(None) +ConvCDefaultIfNone({}) # XXX: default_if_none makes field any + + +@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, val: str | float) -> int: + return int(val) + + +# XXX: fails +# 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..e5545524d 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.dev2" }, { 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.dev2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/da/3314a78b57062293ad244f9f179ee465c43d9fc7bb6b8419184e91928316/pyrefly-1.2.0.dev2.tar.gz", hash = "sha256:5fac5fae3aaf732d7e7ab819933a87f9e18c8665979b0504164a4e3c314da06a", size = 6052480, upload-time = "2026-07-11T10:04:37.102Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/60/fadc19001fc09edf4f8f434ddab34d9097143bef518c1a36e72a7d796df6/pyrefly-1.2.0.dev2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:60ff2533608febf74bc2a6b4d8171b153dd7b6d5ef090e504ddac9af067f6aa6", size = 13622012, upload-time = "2026-07-11T10:04:00.655Z" }, + { url = "https://files.pythonhosted.org/packages/63/28/d0db0484d9f0c7e094e7e65d16953912ca2b139e604e7381ee80996d8552/pyrefly-1.2.0.dev2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d356f78f88a9dc15c0fa355edffb9d6e39ce21be7af9d1af3c3838ff7d506b8f", size = 13097215, upload-time = "2026-07-11T10:04:03.795Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5e/c61105c8a57c8aae2609f7333ae296dec1eff6f5d009a40c66acf8ededad/pyrefly-1.2.0.dev2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6a22a55536217e3b40c57df8facba785fed400c4c444e0da65832b07a8f3498", size = 13502827, upload-time = "2026-07-11T10:04:06.986Z" }, + { url = "https://files.pythonhosted.org/packages/d0/4b/23581a70a742b41635932bedd55fa4c7a6d5e1200e2cdfd46d99d205e390/pyrefly-1.2.0.dev2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cbdeffa0386fc8f10af5ab2950bee363b6a5d9309ffa99fa738e34df2c933205", size = 14484382, upload-time = "2026-07-11T10:04:10.512Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f9/7a2a9cfb4839bb537beffeb238ce2d1a5e039e3fd7d8a558694958b5abdc/pyrefly-1.2.0.dev2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12e01f08189b827a4f517073ea3136b050554caae1ce6af712b741e84f5e90a0", size = 14531849, upload-time = "2026-07-11T10:04:14.039Z" }, + { url = "https://files.pythonhosted.org/packages/08/d4/66aba976d52f79a070076eeb2d5318a86891995e2e6969f266014bf956f0/pyrefly-1.2.0.dev2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ee44582a92a99bb884bad6e7f8cbeddef39092aaefefcfb3b9407ad92e52fae", size = 13978624, upload-time = "2026-07-11T10:04:17.571Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e3/f1f88485024f80475931a3d57bd82d7f083278719912b0c3643926f5dc97/pyrefly-1.2.0.dev2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b960a697f12e77ffe3f1a40fb7989a8eccd4c1ae319434957afc8141e3146169", size = 13533240, upload-time = "2026-07-11T10:04:20.782Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ab/12cd393fe2bae48d1926ac94a1dcfa50489507cfc7b90ea86ea0440ec490/pyrefly-1.2.0.dev2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a370f154f7a7c233d05e8cf3b94f9766712d330adc3628e68ee6918f6996bbfb", size = 14016583, upload-time = "2026-07-11T10:04:24.164Z" }, + { url = "https://files.pythonhosted.org/packages/bb/75/b5c7f2e71130e4e5cc8b689a2032fb9e807060e9c2899910dedf01b9fab6/pyrefly-1.2.0.dev2-py3-none-win32.whl", hash = "sha256:b0bcc942ef6910ff0b48f4f1e117e289d343a2d237e6e1fece116dcb85bfa94d", size = 12763661, upload-time = "2026-07-11T10:04:27.42Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/ad38a6de2f857d3114680fa557e66b3f51b708d3d947a619b390849d45e4/pyrefly-1.2.0.dev2-py3-none-win_amd64.whl", hash = "sha256:124eb0c5b4404a920641083e2b0ce759cea5c1a08e677856eac25ba25bcb425e", size = 13686328, upload-time = "2026-07-11T10:04:30.948Z" }, + { url = "https://files.pythonhosted.org/packages/62/a6/13a82e4780e811c5bd249c4f8b326630c436bb137b1fcbe1296c5ee759a8/pyrefly-1.2.0.dev2-py3-none-win_arm64.whl", hash = "sha256:79707206dfc35533bfb7b64b94c5e0d1d73de1a46a81e7cc90c823711f316876", size = 13051509, upload-time = "2026-07-11T10:04:34.444Z" }, ] [[package]]