diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 7f712eca90..c101579e7c 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -59,3 +59,18 @@ jobs: path: ./.mypy_cache key: mypy|${{ hashFiles('pyproject.toml') }} - run: uv run --locked --no-default-groups --group dev tox run -e typing + pyrefly: + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + with: + enable-cache: true + prune-cache: false + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version-file: pyproject.toml + - run: uv run --locked --no-default-groups --group dev tox run -e pyrefly -- --output-format=github diff --git a/pyproject.toml b/pyproject.toml index 5a0e37d048..2521273203 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,7 @@ tests-random = [ ] typing = [ "mypy", + "pyrefly", "pyright", "pytest", ] @@ -125,6 +126,10 @@ pythonVersion = "3.10" include = ["src", "tests/typing"] typeCheckingMode = "basic" +[tool.pyrefly] +python-version = "3.10" +project-includes = ["src", "tests/typing"] + [tool.ruff] extend-exclude = ["examples/"] src = ["src"] @@ -203,6 +208,11 @@ commands = [ ["pyright", "--ignoreexternal", "--verifytypes", "click"], ] +[tool.tox.env.pyrefly] +description = "run pyrefly type checker" +dependency_groups = ["typing"] +commands = [["pyrefly", "check", {replace = "posargs", default = [], extend = true}]] + [tool.tox.env.docs] description = "build docs" dependency_groups = ["docs"] diff --git a/src/click/_compat.py b/src/click/_compat.py index 134c4f3893..e0a5d85c5a 100644 --- a/src/click/_compat.py +++ b/src/click/_compat.py @@ -10,6 +10,15 @@ from types import TracebackType from weakref import WeakKeyDictionary +if t.TYPE_CHECKING: + from _typeshed import OpenBinaryMode + from _typeshed import OpenTextMode +else: + OpenBinaryMode = OpenTextMode = str + +AnyStr = t.TypeVar("AnyStr", str, bytes) +OpenFileMode = OpenTextMode | OpenBinaryMode + CYGWIN = sys.platform.startswith("cygwin") WIN = sys.platform.startswith("win") auto_wrap_for_ansi: t.Callable[[t.TextIO], t.TextIO] | None = None @@ -355,26 +364,64 @@ def get_text_stderr(encoding: str | None = None, errors: str | None = None) -> t return _force_correct_text_writer(sys.stderr, encoding, errors, force_writable=True) +@t.overload def _wrap_io_open( file: str | os.PathLike[str] | int, - mode: str, + mode: OpenBinaryMode, encoding: str | None, errors: str | None, -) -> t.IO[t.Any]: +) -> t.BinaryIO: ... + + +@t.overload +def _wrap_io_open( + file: str | os.PathLike[str] | int, + mode: OpenTextMode = "r", + encoding: str | None = None, + errors: str | None = "strict", +) -> t.TextIO: ... + + +def _wrap_io_open( + file: str | os.PathLike[str] | int, + mode: OpenFileMode = "r", + encoding: str | None = None, + errors: str | None = "strict", +) -> t.BinaryIO | t.TextIO: """Handles not passing ``encoding`` and ``errors`` in binary mode.""" if "b" in mode: - return open(file, mode) + return t.cast(t.BinaryIO, open(file, mode)) - return open(file, mode, encoding=encoding, errors=errors) + return t.cast(t.TextIO, open(file, mode, encoding=encoding, errors=errors)) +@t.overload def open_stream( filename: str | os.PathLike[str], - mode: str = "r", + mode: OpenBinaryMode, encoding: str | None = None, errors: str | None = "strict", atomic: bool = False, -) -> tuple[t.IO[t.Any], bool]: +) -> tuple[t.IO[bytes], bool]: ... + + +@t.overload +def open_stream( + filename: str | os.PathLike[str], + mode: OpenTextMode = "r", + encoding: str | None = None, + errors: str | None = "strict", + atomic: bool = False, +) -> tuple[t.IO[str], bool]: ... + + +def open_stream( + filename: str | os.PathLike[str], + mode: OpenFileMode = "r", + encoding: str | None = None, + errors: str | None = "strict", + atomic: bool = False, +) -> tuple[t.IO[bytes] | t.IO[str], bool]: binary = "b" in mode filename = os.fspath(filename) @@ -444,17 +491,25 @@ def open_stream( if perm is not None: os.chmod(tmp_filename, perm) # in case perm includes bits in umask - f = _wrap_io_open(fd, mode, encoding, errors) - af = _AtomicFile(f, tmp_filename, os.path.realpath(filename)) - return t.cast(t.IO[t.Any], af), True + if binary: + binary_f = _wrap_io_open(fd, t.cast("OpenBinaryMode", mode), encoding, errors) + binary_af = _AtomicFile(binary_f, tmp_filename, os.path.realpath(filename)) + return binary_af, True + + text_f = _wrap_io_open(fd, t.cast("OpenTextMode", mode), encoding, errors) + text_af = _AtomicFile(text_f, tmp_filename, os.path.realpath(filename)) + return t.cast(t.IO[t.Any], text_af), True -class _AtomicFile: - def __init__(self, f: t.IO[t.Any], tmp_filename: str, real_filename: str) -> None: - self._f = f +class _AtomicFile(t.IO[AnyStr], t.Generic[AnyStr]): + def __init__(self, f: t.IO[AnyStr], tmp_filename: str, real_filename: str) -> None: + self._f: t.IO[AnyStr] = f self._tmp_filename = tmp_filename self._real_filename = real_filename - self.closed = False + + @property + def closed(self) -> bool: + return self._f.closed @property def name(self) -> str: @@ -465,12 +520,11 @@ def close(self, delete: bool = False) -> None: return self._f.close() os.replace(self._tmp_filename, self._real_filename) - self.closed = True def __getattr__(self, name: str) -> t.Any: return getattr(self._f, name) - def __enter__(self) -> _AtomicFile: + def __enter__(self) -> _AtomicFile[AnyStr]: return self def __exit__( diff --git a/src/click/_winconsole.py b/src/click/_winconsole.py index d25178d66f..1d522ca9d8 100644 --- a/src/click/_winconsole.py +++ b/src/click/_winconsole.py @@ -70,19 +70,18 @@ MAX_BYTES_WRITTEN = 32767 if t.TYPE_CHECKING: - try: - # Using `typing_extensions.Buffer` instead of `collections.abc` - # on Windows for some reason does not have `Sized` implemented. - from collections.abc import Buffer # type: ignore - except ImportError: - from typing_extensions import Buffer + from typing_extensions import Buffer + + class _MSVCRTModule(t.Protocol): + def get_osfhandle(self, fd: int) -> int: ... try: from ctypes import pythonapi except ImportError: # On PyPy we cannot get buffers so our ability to operate here is # severely limited. - get_buffer = None + def get_buffer(obj: Buffer, writable: bool = False) -> Array[c_char]: + raise TypeError("PyPy does not support the buffer API used by Click") else: class Py_buffer(Structure): @@ -130,7 +129,8 @@ def readable(self) -> t.Literal[True]: return True def readinto(self, b: Buffer) -> int: - bytes_to_be_read = len(b) + bytes_to_be_read = memoryview(b).nbytes + if not bytes_to_be_read: return 0 elif bytes_to_be_read % 2: @@ -173,7 +173,7 @@ def _get_error_message(errno: int) -> str: return _("Windows error: {error}").format(error=errno) def write(self, b: Buffer) -> int: - bytes_to_be_written = len(b) + bytes_to_be_written = memoryview(b).nbytes buf = get_buffer(b) code_units_to_be_written = min(bytes_to_be_written, MAX_BYTES_WRITTEN) // 2 code_units_written = c_ulong() @@ -201,7 +201,7 @@ def __init__(self, text_stream: t.TextIO, byte_stream: t.BinaryIO) -> None: def name(self) -> str: return self.buffer.name - def write(self, x: t.AnyStr) -> int: + def write(self, x: str | bytes) -> int: if isinstance(x, str): return self._text_stream.write(x) try: @@ -210,7 +210,7 @@ def write(self, x: t.AnyStr) -> int: pass return self.buffer.write(x) - def writelines(self, lines: cabc.Iterable[t.AnyStr]) -> None: + def writelines(self, lines: cabc.Iterable[str] | cabc.Iterable[bytes]) -> None: for line in lines: self.write(line) @@ -270,7 +270,7 @@ def _is_console(f: t.TextIO) -> bool: except (OSError, io.UnsupportedOperation): return False - handle = msvcrt.get_osfhandle(fileno) + handle = t.cast(_MSVCRTModule, msvcrt).get_osfhandle(fileno) return bool(GetConsoleMode(handle, byref(DWORD()))) @@ -278,8 +278,7 @@ def _get_windows_console_stream( f: t.TextIO, encoding: str | None, errors: str | None ) -> t.TextIO | None: if ( - get_buffer is None - or encoding not in {"utf-16-le", None} + encoding not in {"utf-16-le", None} or errors not in {"strict", None} or not _is_console(f) ): diff --git a/src/click/core.py b/src/click/core.py index c5cab15c07..f0e4d5e5d6 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -1001,7 +1001,7 @@ def __init__( self.options_metavar = options_metavar self.short_help = short_help self.add_help_option = add_help_option - self._help_option = None + self._help_option: Option | None = None self.no_args_is_help = no_args_is_help self.hidden = hidden self.deprecated = deprecated @@ -1104,7 +1104,7 @@ def get_help_option(self, ctx: Context) -> Option | None: # Apply help_option decorator and pop resulting option help_option(*help_option_names)(self) - self._help_option = self.params.pop() # type: ignore[assignment] + self._help_option = t.cast(Option, self.params.pop()) return self._help_option diff --git a/src/click/exceptions.py b/src/click/exceptions.py index 4914d9cfe2..1f3bbceb88 100644 --- a/src/click/exceptions.py +++ b/src/click/exceptions.py @@ -302,12 +302,24 @@ class BadArgumentUsage(UsageError): class NoArgsIsHelpError(UsageError): + """ + Raised when no arguments are passed and help is requested via + ``no_arg_is_help`` field in ``Command`` or ``Group``. + + .. versionchanged:: 8.4.0 + ``ctx`` is now optional to be in line with ``UsageError``. + """ + def __init__(self, ctx: Context) -> None: - self.ctx: Context super().__init__(ctx.get_help(), ctx=ctx) def show(self, file: t.IO[t.Any] | None = None) -> None: - echo(self.format_message(), file=file, err=True, color=self.ctx.color) + echo( + self.format_message(), + file=file, + err=True, + color=self.ctx.color if self.ctx is not None else None, + ) class FileError(ClickException): diff --git a/src/click/shell_completion.py b/src/click/shell_completion.py index 5a89b64ec8..abe8bb73c3 100644 --- a/src/click/shell_completion.py +++ b/src/click/shell_completion.py @@ -436,7 +436,7 @@ def format_completion(self, item: CompletionItem) -> str: return f"{item.type}\n{value}\n{help_escaped}" -ShellCompleteType = t.TypeVar("ShellCompleteType", bound="type[ShellComplete]") +ShellCompleteType = t.TypeVar("ShellCompleteType", bound="ShellComplete") _available_shells: dict[str, type[ShellComplete]] = { @@ -447,8 +447,8 @@ def format_completion(self, item: CompletionItem) -> str: def add_completion_class( - cls: ShellCompleteType, name: str | None = None -) -> ShellCompleteType: + cls: type[ShellCompleteType], name: str | None = None +) -> type[ShellCompleteType]: """Register a :class:`ShellComplete` subclass under the given name. The name will be provided by the completion instruction environment variable during completion. diff --git a/src/click/termui.py b/src/click/termui.py index 08d732895e..00687322fb 100644 --- a/src/click/termui.py +++ b/src/click/termui.py @@ -696,7 +696,7 @@ def unstyle(text: str) -> str: def secho( message: t.Any | None = None, - file: t.IO[t.AnyStr] | None = None, + file: t.TextIO | t.BinaryIO | None = None, nl: bool = True, err: bool = False, color: bool | None = None, diff --git a/src/click/testing.py b/src/click/testing.py index 04e7f1d925..55af59b962 100644 --- a/src/click/testing.py +++ b/src/click/testing.py @@ -163,21 +163,21 @@ def mode(self) -> str: def make_input_stream( input: str | bytes | t.IO[t.Any] | None, charset: str ) -> t.BinaryIO: - # Is already an input stream. - if hasattr(input, "read"): - rv = _find_binary_reader(t.cast("t.IO[t.Any]", input)) + if input is None: + return io.BytesIO(b"") - if rv is not None: - return rv + if isinstance(input, str): + return io.BytesIO(input.encode(charset)) - raise TypeError("Could not find binary reader for input stream.") + if isinstance(input, bytes): + return io.BytesIO(input) - if input is None: - input = b"" - elif isinstance(input, str): - input = input.encode(charset) + rv = _find_binary_reader(input) + + if rv is not None: + return rv - return io.BytesIO(input) + raise TypeError("Could not find binary reader for input stream.") class Result: diff --git a/src/click/types.py b/src/click/types.py index 556f20f2b8..9877e58d05 100644 --- a/src/click/types.py +++ b/src/click/types.py @@ -20,11 +20,18 @@ from .utils import safecall if t.TYPE_CHECKING: - import typing_extensions as te - + from _typeshed import OpenBinaryMode + from _typeshed import OpenTextMode from .core import Context from .core import Parameter from .shell_completion import CompletionItem + import typing_extensions as te + +else: + OpenBinaryMode = OpenTextMode = str + +AnyStr = t.TypeVar("AnyStr", str, bytes) +OpenFileMode = t.TypeVar("OpenFileMode", OpenTextMode, OpenBinaryMode) ParamTypeValue = t.TypeVar("ParamTypeValue") @@ -787,7 +794,7 @@ class FileInfoDict(ParamTypeInfoDict): encoding: str | None -class File(ParamType[t.IO[t.Any]]): +class File(ParamType[t.IO[AnyStr]], t.Generic[AnyStr, OpenFileMode]): """Declares a parameter to be a file for reading or writing. The file is automatically closed once the context tears down (after the command finished working). @@ -820,15 +827,35 @@ class File(ParamType[t.IO[t.Any]]): name = "filename" envvar_list_splitter: t.ClassVar[str] = os.path.pathsep + @t.overload + def __init__( + self: File[bytes, OpenBinaryMode], + mode: OpenBinaryMode, + encoding: str | None = None, + errors: str | None = "strict", + lazy: bool | None = None, + atomic: bool = False, + ) -> None: ... + + @t.overload + def __init__( + self: File[str, OpenTextMode], + mode: OpenTextMode = "r", + encoding: str | None = None, + errors: str | None = "strict", + lazy: bool | None = None, + atomic: bool = False, + ) -> None: ... + def __init__( self, - mode: str = "r", + mode: OpenBinaryMode | OpenTextMode = "r", encoding: str | None = None, errors: str | None = "strict", lazy: bool | None = None, atomic: bool = False, ) -> None: - self.mode = mode + self.mode: OpenFileMode = mode self.encoding = encoding self.errors = errors self.lazy = lazy @@ -852,14 +879,15 @@ def resolve_lazy_flag(self, value: str | os.PathLike[str]) -> bool: def convert( self, - value: str | os.PathLike[str] | t.IO[t.Any], + value: str | os.PathLike[str] | t.IO[AnyStr], param: Parameter | None, ctx: Context | None, - ) -> t.IO[t.Any]: + ) -> t.IO[AnyStr]: if _is_file_like(value): return value + # return t.cast("t.IO[AnyStr]", value) - value = t.cast("str | os.PathLike[str]", value) + # value = t.cast("str | os.PathLike[str]", value) try: lazy = self.resolve_lazy_flag(value) @@ -872,7 +900,7 @@ def convert( if ctx is not None: ctx.call_on_close(lf.close_intelligently) - return t.cast("t.IO[t.Any]", lf) + return t.cast("t.IO[AnyStr]", lf) f, should_close = open_stream( value, self.mode, self.encoding, self.errors, atomic=self.atomic @@ -889,7 +917,7 @@ def convert( else: ctx.call_on_close(safecall(f.flush)) - return f + return t.cast("t.IO[AnyStr]", f) except OSError as e: self.fail( f"'{format_filename(value)}': {e.strerror}", @@ -914,7 +942,7 @@ def shell_complete( return [CompletionItem(incomplete, type="file")] -def _is_file_like(value: t.Any) -> te.TypeGuard[t.IO[t.Any]]: +def _is_file_like(value: t.IO[AnyStr] | t.Any) -> te.TypeIs[t.IO[AnyStr]]: return hasattr(value, "read") or hasattr(value, "write") diff --git a/src/click/utils.py b/src/click/utils.py index e2e3fe5f12..e6805b8624 100644 --- a/src/click/utils.py +++ b/src/click/utils.py @@ -23,11 +23,18 @@ from .globals import resolve_color_default if t.TYPE_CHECKING: + from _typeshed import OpenBinaryMode + from _typeshed import OpenTextMode import typing_extensions as te P = te.ParamSpec("P") +else: + OpenBinaryMode = OpenTextMode = str + R = t.TypeVar("R") +AnyStr = t.TypeVar("AnyStr", str, bytes) +OpenFileMode = t.TypeVar("OpenFileMode", OpenTextMode, OpenBinaryMode) def _posixify(name: str) -> str: @@ -110,31 +117,52 @@ def make_default_short_help(help: str, max_length: int = 45) -> str: return " ".join(words[:i]) + "..." -class LazyFile: +class LazyFile(t.Generic[AnyStr, OpenFileMode]): """A lazy file works like a regular file but it does not fully open the file but it does perform some basic checks early to see if the filename parameter does make sense. This is useful for safely opening files for writing. """ + @t.overload + def __init__( + self: LazyFile[bytes, OpenBinaryMode], + filename: str | os.PathLike[str], + mode: OpenBinaryMode, + encoding: str | None = None, + errors: str | None = "strict", + atomic: bool = False, + ) -> None: ... + + @t.overload + def __init__( + self: LazyFile[str, OpenTextMode], + filename: str | os.PathLike[str], + mode: OpenTextMode = "r", + encoding: str | None = None, + errors: str | None = "strict", + atomic: bool = False, + ) -> None: ... + def __init__( self, filename: str | os.PathLike[str], - mode: str = "r", + mode: OpenBinaryMode | OpenTextMode = "r", encoding: str | None = None, errors: str | None = "strict", atomic: bool = False, - ): + ) -> None: self.name: str = os.fspath(filename) - self.mode = mode + self.mode: OpenFileMode = mode self.encoding = encoding self.errors = errors self.atomic = atomic - self._f: t.IO[t.Any] | None + self._f: t.IO[AnyStr] | None self.should_close: bool if self.name == "-": - self._f, self.should_close = open_stream(filename, mode, encoding, errors) + rv, self.should_close = open_stream(filename, mode, encoding, errors) + self._f = t.cast("t.IO[t.AnyStr]", rv) else: if "r" in mode: # Open and close the file in case we're opening it for @@ -152,7 +180,7 @@ def __repr__(self) -> str: return repr(self._f) return f"" - def open(self) -> t.IO[t.Any]: + def open(self) -> t.IO[AnyStr]: """Opens the file if it's not yet open. This call might fail with a :exc:`FileError`. Not handling this error will produce an error that Click shows. @@ -167,8 +195,8 @@ def open(self) -> t.IO[t.Any]: from .exceptions import FileError raise FileError(self.name, hint=e.strerror) from e - self._f = rv - return rv + self._f = t.cast("t.IO[AnyStr]", rv) + return self._f def close(self) -> None: """Closes the underlying file, no matter what.""" @@ -182,7 +210,7 @@ def close_intelligently(self) -> None: if self.should_close: self.close() - def __enter__(self) -> LazyFile: + def __enter__(self) -> LazyFile[AnyStr]: return self def __exit__( @@ -193,19 +221,26 @@ def __exit__( ) -> None: self.close_intelligently() - def __iter__(self) -> cabc.Iterator[t.AnyStr]: + def __iter__(self) -> cabc.Iterator[AnyStr]: self.open() return iter(self._f) # type: ignore -class KeepOpenFile: - def __init__(self, file: t.IO[t.Any]) -> None: - self._file: t.IO[t.Any] = file +class KeepOpenFile(t.Generic[AnyStr]): + # @t.overload + # def __init__(self: KeepOpenFile[bytes], file: t.BinaryIO) -> None: ... + # + # @t.overload + # def __init__(self: KeepOpenFile[str], file: t.TextIO) -> None: ... + + def __init__(self, file: t.IO[AnyStr]) -> None: + # self._file: t.IO[t.AnyStr] = t.cast("t.IO[t.AnyStr]", file) + self._file = file def __getattr__(self, name: str) -> t.Any: return getattr(self._file, name) - def __enter__(self) -> KeepOpenFile: + def __enter__(self) -> KeepOpenFile[AnyStr]: return self def __exit__( @@ -219,7 +254,7 @@ def __exit__( def __repr__(self) -> str: return repr(self._file) - def __iter__(self) -> cabc.Iterator[t.AnyStr]: + def __iter__(self) -> cabc.Iterator[AnyStr]: return iter(self._file) @@ -359,9 +394,42 @@ def get_text_stream( return opener(encoding, errors) +@t.overload def open_file( filename: str | os.PathLike[str], - mode: str = "r", + mode: OpenBinaryMode, + encoding: str | None = None, + errors: str | None = "strict", + lazy: bool = False, + atomic: bool = False, +) -> t.IO[bytes]: ... + + +@t.overload +def open_file( + filename: str | os.PathLike[str], + mode: OpenTextMode = "r", + encoding: str | None = None, + errors: str | None = "strict", + lazy: bool = False, + atomic: bool = False, +) -> t.IO[str]: ... + + +@t.overload +def open_file( + filename: str | os.PathLike[str], + mode: OpenFileMode = "r", + encoding: str | None = None, + errors: str | None = "strict", + lazy: bool = False, + atomic: bool = False, +) -> t.IO[str] | t.IO[bytes]: ... + + +def open_file( + filename: str | os.PathLike[str], + mode: OpenFileMode = "r", encoding: str | None = None, errors: str | None = "strict", lazy: bool = False, @@ -396,14 +464,12 @@ def open_file( .. versionadded:: 3.0 """ if lazy: - return t.cast( - "t.IO[t.Any]", LazyFile(filename, mode, encoding, errors, atomic=atomic) - ) + return t.cast("t.IO[t.Any]", LazyFile(filename, mode, encoding, errors, atomic)) f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic) if not should_close: - f = t.cast("t.IO[t.Any]", KeepOpenFile(f)) + return t.cast("t.IO[t.Any]", KeepOpenFile(f)) return f diff --git a/tests/typing/typing_file.py b/tests/typing/typing_file.py new file mode 100644 index 0000000000..58f27bf68f --- /dev/null +++ b/tests/typing/typing_file.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import typing as t + +from typing_extensions import assert_type + +import click + +import _typeshed as ts + + +text_file = click.File() +assert_type(text_file, click.File[str, ts.OpenTextMode]) + +explicit_text_file = click.File("r") +assert_type(explicit_text_file, click.File[str, ts.OpenTextMode]) + +binary_file = click.File("rb") +assert_type(binary_file, click.File[bytes, ts.OpenBinaryMode]) + + +with click.open_file("test.txt") as text_stream: + assert_type(text_stream, t.IO[str]) + +with click.open_file("test.txt", "r") as explicit_text_stream: + assert_type(explicit_text_stream, t.IO[str]) + +with click.open_file("test.bin", "rb") as binary_stream: + assert_type(binary_stream, t.IO[bytes]) + +with click.open_file("test.txt", lazy=True) as lazy_text_stream: + assert_type(lazy_text_stream, t.IO[str]) + +with click.open_file("test.bin", "wb", atomic=True) as atomic_binary_stream: + assert_type(atomic_binary_stream, t.IO[bytes]) diff --git a/uv.lock b/uv.lock index 2785061845..29abe6d01b 100644 --- a/uv.lock +++ b/uv.lock @@ -216,6 +216,7 @@ tests-random = [ ] typing = [ { name = "mypy" }, + { name = "pyrefly" }, { name = "pyright" }, { name = "pytest" }, ] @@ -250,6 +251,7 @@ tests-random = [ ] typing = [ { name = "mypy" }, + { name = "pyrefly" }, { name = "pyright" }, { name = "pytest" }, ] @@ -869,6 +871,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/cc/cecf97be298bee2b2a37dd360618c819a2a7fd95251d8e480c1f0eb88f3b/pyproject_api-1.10.0-py3-none-any.whl", hash = "sha256:8757c41a79c0f4ab71b99abed52b97ecf66bd20b04fa59da43b5840bac105a09", size = 13218, upload-time = "2025-10-09T19:12:24.428Z" }, ] +[[package]] +name = "pyrefly" +version = "0.64.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/13/7f824fa240a7c6b5595defc749d7e0e41c1c7fa2a889f55d5bd7d5cca28c/pyrefly-0.64.1.tar.gz", hash = "sha256:6303095afeedf4a93c7cf5e273ad0ada3d76f3a66b8769e06a6b96f0b2b22a39", size = 5678602, upload-time = "2026-05-08T03:37:53.334Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/1a/2a26e0b87942830bb3cdf21e89a6772efadd07ebf1d4f25a05ffc00ab71a/pyrefly-0.64.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:df5c1a8edc83483b8fcc662a188ac09a129e1d051c676794ca9ac1005373ae0a", size = 13112175, upload-time = "2026-05-08T03:37:30.448Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c8/625ad5ef423d425498eef4f350fe0a788d3d5403960dac6ff660b5a8edad/pyrefly-0.64.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0388c486816043c745744c7cc7f1215005c3c2d9506c5ee89ebaee013cd62f3", size = 12593929, upload-time = "2026-05-08T03:37:33.23Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9e/0b11aa0d0310f274f41394884d57775d5ac7d36cdb57ba8f030b7dcfae50/pyrefly-0.64.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da4d384a6466c59ba6bfa46b20f9a9f6b73922aeee43d96c9f89480d5edd3ad9", size = 12986439, upload-time = "2026-05-08T03:37:35.779Z" }, + { url = "https://files.pythonhosted.org/packages/20/be/3f774b450415fffe6ef13d0fcf4e22c733bad7c3d826a54693e17c9ab302/pyrefly-0.64.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52fad756a47c9fc1d912f8564f2afdb816c4d057dfd8f90f07adce52314a5b56", size = 13936843, upload-time = "2026-05-08T03:37:38.203Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ee/e551c0ce512f01ddc09ed4258856ed4513bf49f3749c8c814a5b94ae59a3/pyrefly-0.64.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970a888b37eea53136a364b43e1466179ff8bb383e894c0afec3a3055e15cd9e", size = 13915094, upload-time = "2026-05-08T03:37:40.698Z" }, + { url = "https://files.pythonhosted.org/packages/48/c3/3aed3ff905f82833de7c48d8f9788e8ce9fed57c95bf876eef3cd02c4d29/pyrefly-0.64.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92ac7078f174a79d489a0d0c43d858523f8e5b95f4981269f2f75c4aaa66be3d", size = 13459840, upload-time = "2026-05-08T03:37:43.245Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ed/aa1dc8b913d416a3c8bffc5485b1efe23c6039ead0dc5a9ad5505bf161ce/pyrefly-0.64.1-py3-none-win32.whl", hash = "sha256:3d16c90365af2f424c6e6a5c87d85ca94e8618ebcb4bf4c910634fd65d32b2e6", size = 12208564, upload-time = "2026-05-08T03:37:45.988Z" }, + { url = "https://files.pythonhosted.org/packages/8c/0c/29dfe1cbcf3b1907558e67a3e4b304af3992933f05e22a596333489d5950/pyrefly-0.64.1-py3-none-win_amd64.whl", hash = "sha256:1fda307ecf414274445108a5df5b58c6fa1702a51b4ee6c1df87efeeb97dec59", size = 13134553, upload-time = "2026-05-08T03:37:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7c/449407653fe95e3f3a65dd8a54d8729ac0451247489d79d4e07808d73917/pyrefly-0.64.1-py3-none-win_arm64.whl", hash = "sha256:8f83a74c1463842d486d6578a000feccf47cd54d6d7d6628ffe73b1055ca9dce", size = 12528438, upload-time = "2026-05-08T03:37:50.923Z" }, +] + [[package]] name = "pyright" version = "1.1.408"