From b28c669b365a23de279910d13296023bcd89d1ae Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 14:33:50 +0000 Subject: [PATCH 1/2] Upgrade ruff to 0.17 and ty to 0.0.86 Apply ruff 0.17 autofixes, switch @contextmanager return annotations from Iterator to Generator (ty deprecation), and drop redundant casts. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HjNAvzqKcPsGCk5KdkaCQ8 --- plain-code/pyproject.toml | 2 +- plain-dev/plain/dev/poncho/manager.py | 2 +- plain-jobs/plain/jobs/locks.py | 4 +- plain-jobs/plain/jobs/otel.py | 4 +- plain-jobs/tests/public/test_parameters.py | 1 - plain-jobs/tests/public/test_scheduling.py | 1 - plain-postgres/plain/postgres/base.py | 4 +- .../plain/postgres/cli/decorators.py | 4 +- .../plain/postgres/convergence/analysis.py | 6 +- plain-postgres/plain/postgres/dialect.py | 4 +- .../plain/postgres/fields/primary_key.py | 6 +- plain-postgres/plain/postgres/schema_lock.py | 4 +- plain/plain/forms/fields.py | 6 +- plain/plain/utils/timezone.py | 2 +- plain/tests/internal/test_server_websocket.py | 4 +- plain/tests/internal/test_websocket.py | 4 +- plain/tests/server_stubs.py | 4 +- pyproject.toml | 2 +- uv.lock | 90 +++++++++---------- 19 files changed, 76 insertions(+), 78 deletions(-) diff --git a/plain-code/pyproject.toml b/plain-code/pyproject.toml index dde2f2a0cf..cc612b0c3f 100644 --- a/plain-code/pyproject.toml +++ b/plain-code/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "plain>=0.155.0,<1.0.0", # Bounded so every checkout and every app lints with the same rule set — # ruff adds rules (and import-sort tweaks) in minor releases. - "ruff>=0.16.2,<0.17", + "ruff>=0.17.0,<0.18", "ty>=0.0.82,<1.0.0", "httpx>=0.27", "tomlkit>=0.11.0", diff --git a/plain-dev/plain/dev/poncho/manager.py b/plain-dev/plain/dev/poncho/manager.py index 74083b4c5b..b30b970d7b 100644 --- a/plain-dev/plain/dev/poncho/manager.py +++ b/plain-dev/plain/dev/poncho/manager.py @@ -61,7 +61,7 @@ def __init__(self, printer: Printer | None = None) -> None: self._processes = {} self._terminating = False - self._terminate_start = datetime.datetime.min # noqa: DTZ901 — poncho's clock is consistently naive + self._terminate_start = datetime.datetime.min self._killed = False def add_process( diff --git a/plain-jobs/plain/jobs/locks.py b/plain-jobs/plain/jobs/locks.py index 177c9d87ef..bf37a34c09 100644 --- a/plain-jobs/plain/jobs/locks.py +++ b/plain-jobs/plain/jobs/locks.py @@ -1,7 +1,7 @@ """Lock implementations for job enqueueing.""" import hashlib -from collections.abc import Iterator +from collections.abc import Generator from contextlib import contextmanager from typing import TYPE_CHECKING @@ -10,7 +10,7 @@ @contextmanager -def postgres_advisory_lock(job: Job, concurrency_key: str) -> Iterator[None]: +def postgres_advisory_lock(job: Job, concurrency_key: str) -> Generator[None]: """ PostgreSQL advisory lock context manager. diff --git a/plain-jobs/plain/jobs/otel.py b/plain-jobs/plain/jobs/otel.py index b671564610..a06eb5a3c3 100644 --- a/plain-jobs/plain/jobs/otel.py +++ b/plain-jobs/plain/jobs/otel.py @@ -1,6 +1,6 @@ import datetime import importlib.metadata -from collections.abc import Callable, Iterable, Iterator +from collections.abc import Callable, Generator, Iterable from contextlib import contextmanager from dataclasses import dataclass from functools import wraps @@ -70,7 +70,7 @@ def record_span_error(span: trace.Span, exc: BaseException) -> str: @contextmanager -def error_consumer_span(*, name: str, exc: BaseException) -> Iterator[None]: +def error_consumer_span(*, name: str, exc: BaseException) -> Generator[None]: """Open a one-off CONSUMER span solely to carry a failure that has no other entry span — a DB error before the span that would normally own the work is ever reached. Stamps the canonical failure signal so the diff --git a/plain-jobs/tests/public/test_parameters.py b/plain-jobs/tests/public/test_parameters.py index d82b79acef..51c211f5c5 100644 --- a/plain-jobs/tests/public/test_parameters.py +++ b/plain-jobs/tests/public/test_parameters.py @@ -1,4 +1,3 @@ -# ruff: noqa: DTZ001 — naive datetimes are deliberate here: Schedule does # tz-independent calendar math, and these exercise exactly that. import datetime diff --git a/plain-jobs/tests/public/test_scheduling.py b/plain-jobs/tests/public/test_scheduling.py index aadfa77556..3191051934 100644 --- a/plain-jobs/tests/public/test_scheduling.py +++ b/plain-jobs/tests/public/test_scheduling.py @@ -1,4 +1,3 @@ -# ruff: noqa: DTZ001 — naive datetimes are deliberate here: Schedule does # tz-independent calendar math, and these exercise exactly that. import datetime diff --git a/plain-postgres/plain/postgres/base.py b/plain-postgres/plain/postgres/base.py index 9ffe82439a..627ed3b60a 100644 --- a/plain-postgres/plain/postgres/base.py +++ b/plain-postgres/plain/postgres/base.py @@ -1,6 +1,6 @@ import copy import warnings -from collections.abc import Collection, Iterable, Iterator, Sequence +from collections.abc import Collection, Generator, Iterable, Iterator, Sequence from contextlib import contextmanager from itertools import chain from typing import TYPE_CHECKING, Any, ClassVar, Self, dataclass_transform @@ -564,7 +564,7 @@ def _integrity_error_to_validation_error( return ValidationError(error.update_error_dict({})) @contextmanager - def _mapped_write(self) -> Iterator[None]: + def _mapped_write(self) -> Generator[None]: """Run a table write inside the rollback guard, mapping a declared-constraint IntegrityError to the ValidationError a pre-check would have raised -- so the same violation surfaces identically whether diff --git a/plain-postgres/plain/postgres/cli/decorators.py b/plain-postgres/plain/postgres/cli/decorators.py index 2bdfe63c08..13155acfc4 100644 --- a/plain-postgres/plain/postgres/cli/decorators.py +++ b/plain-postgres/plain/postgres/cli/decorators.py @@ -1,5 +1,5 @@ import functools -from collections.abc import Callable, Iterator +from collections.abc import Callable, Generator from contextlib import contextmanager from typing import Any @@ -11,7 +11,7 @@ @contextmanager -def cli_schema_lock() -> Iterator[Callable[[], None]]: +def cli_schema_lock() -> Generator[Callable[[], None]]: """`schema_lock()` for CLI commands: a timeout or lost lock surfaces as a clean one-line error instead of a traceback burying it. Yields the lock's verify callable for multi-phase commands to check between phases.""" diff --git a/plain-postgres/plain/postgres/convergence/analysis.py b/plain-postgres/plain/postgres/convergence/analysis.py index 2a9f487783..46667e5eba 100644 --- a/plain-postgres/plain/postgres/convergence/analysis.py +++ b/plain-postgres/plain/postgres/convergence/analysis.py @@ -1,7 +1,7 @@ import contextvars import json import re -from collections.abc import Iterator +from collections.abc import Generator from contextlib import contextmanager from dataclasses import dataclass, field from enum import StrEnum @@ -1790,7 +1790,7 @@ class _ProbeSession: @contextmanager -def _probe_session(cursor: CursorWrapper, model: type[Model]) -> Iterator[None]: +def _probe_session(cursor: CursorWrapper, model: type[Model]) -> Generator[None]: """Reuse one probe table across every round-trip in a model's analysis. The temp table is created lazily by the first probe inside this scope and @@ -1833,7 +1833,7 @@ def _create_probe_table(cursor: CursorWrapper, model: type[Model]) -> None: @contextmanager -def _probe_table(cursor: CursorWrapper, model: type[Model]) -> Iterator[None]: +def _probe_table(cursor: CursorWrapper, model: type[Model]) -> Generator[None]: """Provide an empty temp table mirroring the model's real table for one round-trip, isolating the probe's DDL so it can't leak into the analyze transaction. diff --git a/plain-postgres/plain/postgres/dialect.py b/plain-postgres/plain/postgres/dialect.py index f5cd0dcc96..d2b2f1e91f 100644 --- a/plain-postgres/plain/postgres/dialect.py +++ b/plain-postgres/plain/postgres/dialect.py @@ -482,8 +482,8 @@ def year_lookup_bounds_for_datetime_field( value + 1, 1, 1 ) - datetime.timedelta(microseconds=1) else: - first = datetime.datetime(value, 1, 1) # noqa: DTZ001 — made aware just below - second = datetime.datetime(value, 12, 31, 23, 59, 59, 999999) # noqa: DTZ001 — made aware just below + first = datetime.datetime(value, 1, 1) + second = datetime.datetime(value, 12, 31, 23, 59, 59, 999999) # Make sure that datetimes are aware in the current timezone tz = timezone.get_current_timezone() diff --git a/plain-postgres/plain/postgres/fields/primary_key.py b/plain-postgres/plain/postgres/fields/primary_key.py index 8bfd6597d0..db4355edb1 100644 --- a/plain-postgres/plain/postgres/fields/primary_key.py +++ b/plain-postgres/plain/postgres/fields/primary_key.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any from plain.preflight import PreflightResult @@ -43,8 +43,8 @@ def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]: return ( self.name, "plain.postgres.PrimaryKeyField", - cast(list[Any], []), - cast(dict[str, Any], {}), + [], + {}, ) def validate(self, value: Any, model_instance: Model) -> None: diff --git a/plain-postgres/plain/postgres/schema_lock.py b/plain-postgres/plain/postgres/schema_lock.py index 640429348c..48db8e7a62 100644 --- a/plain-postgres/plain/postgres/schema_lock.py +++ b/plain-postgres/plain/postgres/schema_lock.py @@ -17,7 +17,7 @@ """ import time -from collections.abc import Callable, Iterator +from collections.abc import Callable, Generator from contextlib import contextmanager import psycopg @@ -55,7 +55,7 @@ class SchemaLockLost(Exception): @contextmanager -def schema_lock() -> Iterator[Callable[[], None]]: +def schema_lock() -> Generator[Callable[[], None]]: """Hold the schema advisory lock for the duration of the block. Acquisition is non-blocking with retry (`pg_try_advisory_lock`), so a diff --git a/plain/plain/forms/fields.py b/plain/plain/forms/fields.py index d520606273..0aa2ffdaed 100644 --- a/plain/plain/forms/fields.py +++ b/plain/plain/forms/fields.py @@ -456,7 +456,7 @@ def to_python(self, value: Any) -> datetime.date | None: def strptime(self, value: str, format: str) -> datetime.date: # Only the date survives; the intermediate datetime's tz is irrelevant. - return datetime.datetime.strptime(value, format).date() # noqa: DTZ007 + return datetime.datetime.strptime(value, format).date() class TimeField(BaseTemporalField): @@ -476,7 +476,7 @@ def to_python(self, value: Any) -> datetime.time | None: def strptime(self, value: str, format: str) -> datetime.time: # Only the time survives; the intermediate datetime's tz is irrelevant. - return datetime.datetime.strptime(value, format).time() # noqa: DTZ007 + return datetime.datetime.strptime(value, format).time() class DateTimeFormatsIterator: @@ -517,7 +517,7 @@ def to_python(self, value: Any) -> datetime.datetime | None: def strptime(self, value: str, format: str) -> datetime.datetime: # Parsed naive by design; to_python localizes via from_current_timezone. - return datetime.datetime.strptime(value, format) # noqa: DTZ007 + return datetime.datetime.strptime(value, format) class DurationField(Field): diff --git a/plain/plain/utils/timezone.py b/plain/plain/utils/timezone.py index e0574a0751..683594d13a 100644 --- a/plain/plain/utils/timezone.py +++ b/plain/plain/utils/timezone.py @@ -210,7 +210,7 @@ def naive_datetime_from_date(value: date) -> datetime: Deliberately naive: callers either localize it (form/field parsing) or are doing tz-independent calendar math (timesince). """ - return datetime(value.year, value.month, value.day) # noqa: DTZ001 + return datetime(value.year, value.month, value.day) def make_aware(value: datetime, timezone: tzinfo | None = None) -> datetime: diff --git a/plain/tests/internal/test_server_websocket.py b/plain/tests/internal/test_server_websocket.py index 25f29f72a1..bda20a8e6a 100644 --- a/plain/tests/internal/test_server_websocket.py +++ b/plain/tests/internal/test_server_websocket.py @@ -8,7 +8,7 @@ import asyncio import logging -from collections.abc import Iterator +from collections.abc import Generator from contextlib import contextmanager from typing import Any @@ -35,7 +35,7 @@ @contextmanager -def _socket_test() -> Iterator[CapturedSpans]: +def _socket_test() -> Generator[CapturedSpans]: """Every test here runs over plain HTTP — the socketpair is plain HTTP; keep the HTTPS redirect out of the way. Yields the spans the test emits.""" with override_settings(HTTPS_REDIRECT_ENABLED=False), capture_spans() as spans: diff --git a/plain/tests/internal/test_websocket.py b/plain/tests/internal/test_websocket.py index 4d8f2670fc..b7736bc433 100644 --- a/plain/tests/internal/test_websocket.py +++ b/plain/tests/internal/test_websocket.py @@ -5,7 +5,7 @@ """ import asyncio -from collections.abc import AsyncIterator +from collections.abc import AsyncGenerator from contextlib import asynccontextmanager from plain.http import WebSocket, WebSocketClosed @@ -23,7 +23,7 @@ async def _open( max_message_size: int = 1024, ping_interval: float = PING_INTERVAL, ping_timeout: float = PING_TIMEOUT, -) -> AsyncIterator[tuple[WebSocket, asyncio.StreamReader, asyncio.StreamWriter]]: +) -> AsyncGenerator[tuple[WebSocket, asyncio.StreamReader, asyncio.StreamWriter]]: """A started `WebSocket` on the server end of a socketpair, plus the client end.""" conn, client_reader, client_writer = await socketpair_connection(StubApp()) ws = WebSocket( diff --git a/plain/tests/server_stubs.py b/plain/tests/server_stubs.py index 3eca727282..3fa3c6ea0e 100644 --- a/plain/tests/server_stubs.py +++ b/plain/tests/server_stubs.py @@ -11,7 +11,7 @@ import os import socket import time -from collections.abc import Callable, Iterator +from collections.abc import Callable, Generator from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager from typing import Any @@ -188,7 +188,7 @@ def emit(self, record: logging.LogRecord) -> None: @contextmanager -def capture_logger(name: str, *, level: int = logging.INFO) -> Iterator[LogCapture]: +def capture_logger(name: str, *, level: int = logging.INFO) -> Generator[LogCapture]: logger = logging.getLogger(name) handler = LogCapture() original_level = logger.level diff --git a/pyproject.toml b/pyproject.toml index 7ad80c9046..2381598998 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ dev = [ # uv.lock: every ty release so far has moved which diagnostics fire on the # typed model surface, so a bump is a deliberate change that has to pass # plain-postgres/tests/typing/ -- never a silent lockfile update. - "ty>=0.0.80", + "ty>=0.0.86", "psycopg[binary]>=3.2.12", ] diff --git a/uv.lock b/uv.lock index 450a734663..5948b40eed 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 5 +revision = 3 requires-python = ">=3.14" [manifest] @@ -936,7 +936,7 @@ dev = [ requires-dist = [ { name = "httpx", specifier = ">=0.27" }, { name = "plain", editable = "plain" }, - { name = "ruff", specifier = ">=0.16.2,<0.17" }, + { name = "ruff", specifier = ">=0.17.0,<0.18" }, { name = "tomlkit", specifier = ">=0.11.0" }, { name = "ty", specifier = ">=0.0.82,<1.0.0" }, ] @@ -1632,7 +1632,7 @@ dev = [ { name = "plain-vendor", editable = "plain-vendor" }, { name = "plainx-dev", editable = "plainx-dev" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.2.12" }, - { name = "ty", specifier = ">=0.0.80" }, + { name = "ty", specifier = ">=0.0.86" }, ] [[package]] @@ -1950,27 +1950,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c4/49/23802c45f093eb14bde54b141d2b2f058edfa63a06db7beed047308cc08f/ruff-0.16.10.tar.gz", hash = "sha256:eff4728c4eaae93f0955cd264d24b2ab348e74bf59986ccf282ba6dc16b3b017", size = 4958724, upload-time = "2026-10-01T18:03:21.697Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/21/ebce22e1d90cdb2cd691026b9c6e9bec6499f0b396089481755b5d49efff/ruff-0.16.10-py3-none-linux_armv6l.whl", hash = "sha256:488b0fe3f3574210e5cf80d9f59b9e3ab17a127a8155de3f307b392589cfb511", size = 10095557, upload-time = "2026-10-01T18:02:36.072Z" }, - { url = "https://files.pythonhosted.org/packages/cb/98/a54de85876a8b2612bfa0d84c7b9abfb39c6a3354aee7800b09c1649b9e3/ruff-0.16.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e748ff95c934c4e978783b8e687bc174e7bd84e8ad24e3243e1ecfcda5e0282d", size = 10340577, upload-time = "2026-10-01T18:02:39.258Z" }, - { url = "https://files.pythonhosted.org/packages/9f/16/1a5a4a2657effe4806110f2b907313802f1367fcdbb29e8122766e407fab/ruff-0.16.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3031a4a2e8e7b8a46f70be45f198c35a11ece509a94b80334d8d397a33c67550", size = 9774282, upload-time = "2026-10-01T18:02:41.79Z" }, - { url = "https://files.pythonhosted.org/packages/6e/fb/470085af734da396e68cd80fb0a3e7c109a459716ae59588c0f6fab8a17d/ruff-0.16.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:494401c86df4c4c25f69b9419605d944467ee98c42fb6ad405ef4fa40b8fb67d", size = 9920895, upload-time = "2026-10-01T18:02:44.458Z" }, - { url = "https://files.pythonhosted.org/packages/57/de/f10cffe4f88a37ea6615ec460f01bf76f0bc1477737a35d9ee61a630a78b/ruff-0.16.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d203abc0ff2b773ee33d00ab8df0bb67046fbc7c07b119332f08b9b345cf8221", size = 9902707, upload-time = "2026-10-01T18:02:47.041Z" }, - { url = "https://files.pythonhosted.org/packages/ff/44/3fdcedf83ae60ef837dd239170e480dce606a9142cfa2db911afdf855fd9/ruff-0.16.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd83d1235a5258d318477bc5b576303974cbdef5df0c01a1bff14efcc23bd12a", size = 10619287, upload-time = "2026-10-01T18:02:49.485Z" }, - { url = "https://files.pythonhosted.org/packages/c1/62/02e76a5574002153618eafbb70e159468addc72a5d2c488e65cbb4639d2d/ruff-0.16.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc2610fb269fa56dd8a68669ae470fa6272902668c0fc2ebc3aa112b2633d5b8", size = 11339942, upload-time = "2026-10-01T18:02:52.008Z" }, - { url = "https://files.pythonhosted.org/packages/6b/c4/cde27d47ad8d4126c587e608c67c46e7feac11763f606d261a1bc8a489e6/ruff-0.16.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e70175e29cc94c26ea296c80e470180b744b7419026898e58f520c6ab32578e", size = 10934316, upload-time = "2026-10-01T18:02:54.811Z" }, - { url = "https://files.pythonhosted.org/packages/e4/03/17234145f302a645a123e8c3bb2411ecf4669fbc350de3b0d803230a1729/ruff-0.16.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f33f43a864a8483eebd160e713336c8bab02c934feaff0a33cf5ccb41546d09a", size = 10387968, upload-time = "2026-10-01T18:02:57.497Z" }, - { url = "https://files.pythonhosted.org/packages/71/29/2493af60240ee7644b38a4b821f5f9c3b5a4fa3178770fe1d0c685217395/ruff-0.16.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1dfc6f0088149fb6a362c1c446bcbb3fd2157b3852fe2fa68409276eab9ad9b3", size = 10537906, upload-time = "2026-10-01T18:03:00.006Z" }, - { url = "https://files.pythonhosted.org/packages/2e/9a/f56b28f3b143bb9e518c34fb89ab191b626bca8b70dbf8af0d2e2d473572/ruff-0.16.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6553498afc35f580f036030795810b9e6bcea31604b0fd9e8d352795473042e3", size = 10012938, upload-time = "2026-10-01T18:03:03.006Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c4/fca37362848ea4d4d80712df13632e645e7c7cfb1bdedf140699ac7a090b/ruff-0.16.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a3b8471dea115d37f123882be852bed13403746d3a76c11de4a19ec5f9ff5a03", size = 9897945, upload-time = "2026-10-01T18:03:05.818Z" }, - { url = "https://files.pythonhosted.org/packages/7d/c7/e0bc57664d6e0c61fd22f620af260fe9662d7e1ddb320ea7f160e76ef165/ruff-0.16.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92e59a70bcbd9d3a5483656da906ec28edfdacfce00afd99edb8b4e9d15644be", size = 10333134, upload-time = "2026-10-01T18:03:08.25Z" }, - { url = "https://files.pythonhosted.org/packages/21/aa/5c9f3b68737c0e4a33a1db5dfab44f7b786d96ca91a7233112ddab1e6dc2/ruff-0.16.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7ae7375f803b5520dc9f546bed7e3a0acb70b91812e9bb4b19927de22f25b77d", size = 10741702, upload-time = "2026-10-01T18:03:10.775Z" }, - { url = "https://files.pythonhosted.org/packages/78/fa/0f9c2020dc316c53d983be011720b8157cc052b97e3f4dfa7db6d0f880a6/ruff-0.16.10-py3-none-win32.whl", hash = "sha256:2a12e01cb9156c10c466f63b46eaae5ecea28dfbd21b5836353ae498e7d1349a", size = 10139176, upload-time = "2026-10-01T18:03:13.267Z" }, - { url = "https://files.pythonhosted.org/packages/99/29/cfb0df9448d4d4ad48c2de029ada9ebd71baa6da983a6c77ee6c6cd0fe82/ruff-0.16.10-py3-none-win_amd64.whl", hash = "sha256:97f2015c92aa97105b0eab19eb5d224884399281cfc5da86a92db4ab5e7fb2ca", size = 10584734, upload-time = "2026-10-01T18:03:16.006Z" }, - { url = "https://files.pythonhosted.org/packages/fc/05/c16957eb287c3fc062e032619a25868d93d408a844b725bcf514f7a378ff/ruff-0.16.10-py3-none-win_arm64.whl", hash = "sha256:25a65fe998c4e6861ec079ada5826a2fc605e6cbccbe9dcd7fac1f54e791621b", size = 10366440, upload-time = "2026-10-01T18:03:19.04Z" }, +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/a7/70debb024dfacda67b8e560cc7511f52b34b9348a8cc8c1ec23036dcd51d/ruff-0.17.0.tar.gz", hash = "sha256:5cd03240d8208a557c2a9655a5cb07ebe36aa6bb35065f97d48c1f6adef5a322", size = 5320554, upload-time = "2026-10-09T19:47:29.248Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/f8/ee5ab9da6089eae2a33e6008b01deb1eda19992c1c8e10661e98cee1640f/ruff-0.17.0-py3-none-linux_armv6l.whl", hash = "sha256:0e271826af9a20d18c6cfae8c51e82959167c24859686ddd3eb9a7f0842ce81e", size = 10148907, upload-time = "2026-10-09T19:46:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d9/2f81fb5a9d580afbb11b1c8ff915233a11f2a1b27405d7991f183c5e1976/ruff-0.17.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5f0ca4a40f81403689c04f12966e22f44e329ae362072d8f1587b7bda87f603b", size = 10224208, upload-time = "2026-10-09T19:46:41.711Z" }, + { url = "https://files.pythonhosted.org/packages/a7/20/643f3c8f75594f937b2bf74801241c56a2e2b8e139d24dff8b66b28cdd7f/ruff-0.17.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cbf7149e0927dc3295d5d64679a4765576eef71b00782b2ae969ef82274d6bb9", size = 9273324, upload-time = "2026-10-09T19:46:44.323Z" }, + { url = "https://files.pythonhosted.org/packages/ec/91/627700b233d367736cb274f1bd0b47d1f2b12f68878192812bd875adadc3/ruff-0.17.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ee90156522998c3037059d8f66885c8adeeaf7643bdce2caceee196ecd23e0", size = 9953190, upload-time = "2026-10-09T19:46:47.155Z" }, + { url = "https://files.pythonhosted.org/packages/cd/92/91f7b5ed39490f89d6cbf56e1f543c383667a725efa8e2c2dee0f01f5591/ruff-0.17.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d8e4a002a94cd9d0dc48b51dc69d807a172b5b9bf2b668e656424dc5b55ead1", size = 9961746, upload-time = "2026-10-09T19:46:50.098Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/7310f9fc63ce11ff6394edbd5e85433dfb0e14c9fbf6ccc97f1538491bc7/ruff-0.17.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0b8a60c06a218c337e1161638d34757f83449243e2db161483ddf948e53ad14", size = 10625483, upload-time = "2026-10-09T19:46:53.379Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8d/97443f0dca4a03a0bc7629fd396fd494a1cb6666121e38c5075acb217d8f/ruff-0.17.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a330178bdffc4205dbf3bda11d93e059e388fd6546f8cdd304501a9160363c0d", size = 11350129, upload-time = "2026-10-09T19:46:56.486Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0a/c525efd9777be4b6b012e6969a3012648468e7e6c4b3e5b46af69f46e8eb/ruff-0.17.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7bb08489e234876fa2da67ae3ea938e9a2156da80293e0e4365abd6973d98329", size = 10906223, upload-time = "2026-10-09T19:47:00.263Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3c/4a01195d93420cad1175bedad13a515dc8a56f95a6e39789b92e582682f5/ruff-0.17.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc73e7c133e82d55b5f15897b2a442d72c0cb4a0c886c46801ce3c247150b60c", size = 10386314, upload-time = "2026-10-09T19:47:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/c7/72/1a3951665485a921f6375f91e754a1854d5a645d41acc3642668064ff64d/ruff-0.17.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:db4f74c533403ab70fe4007873f6ae0c9f94a8b03158cf48d78788e47cdbe399", size = 10565464, upload-time = "2026-10-09T19:47:05.831Z" }, + { url = "https://files.pythonhosted.org/packages/90/8c/539b4d8c082f57e18db8ae2be85a460d77861c79dcd5798e32b536a6a06f/ruff-0.17.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3d8cc360e666d1914e47b0777c6906d70cf18891a55532bd0a16844195d70859", size = 10079468, upload-time = "2026-10-09T19:47:08.617Z" }, + { url = "https://files.pythonhosted.org/packages/e0/b8/84286966db79434e8c26b585b0a0f6897cb3ab1c51a4aa4df10c28488b62/ruff-0.17.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d66de796b726c4801e05fa99a2a8d7a780e107be222486c304ab61765561e866", size = 9958556, upload-time = "2026-10-09T19:47:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/69/50/27b6eed27b83fcdd5bfa0d52b83231e29094754374698da404d094487ae3/ruff-0.17.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c3f268baf004aea944f040623327119527ea231af15f7fb7890e82cea0679589", size = 10352173, upload-time = "2026-10-09T19:47:14.188Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/955399fd13044cd827862044117d784a59e3196f6cce7424908ac9a7f914/ruff-0.17.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:864b6c1acb6b0bccf94b5a3938a1531fd09aaca5e5659a2e7bf0f3cf2a685540", size = 10791761, upload-time = "2026-10-09T19:47:16.931Z" }, + { url = "https://files.pythonhosted.org/packages/ae/bf/024e01e1f6aec87768696725e648ed5b438941341eea8f8100beb681961f/ruff-0.17.0-py3-none-win32.whl", hash = "sha256:5e50aa5b84decd9fe5b0bb0e6f71c3b592f1767ed09faa4b7207d933961e35cd", size = 9904112, upload-time = "2026-10-09T19:47:19.75Z" }, + { url = "https://files.pythonhosted.org/packages/cc/77/1ee73df41dcc8d1cdb686ee4bc46ea29704ea175feb6b95c78420f631ab8/ruff-0.17.0-py3-none-win_amd64.whl", hash = "sha256:8ab76bcda86dfd28e13776cb5de3c7bcdcf1ae3d37ed761113d1a5a415dc134c", size = 10115585, upload-time = "2026-10-09T19:47:22.698Z" }, + { url = "https://files.pythonhosted.org/packages/fd/71/eb4f0ccc844aece56963e8578df9c95d4c00f580547d52329d4035d3af18/ruff-0.17.0-py3-none-win_arm64.whl", hash = "sha256:c154c73ff43f9854395e24cac507af13078962e53d2b511605058d22af1fdb88", size = 9872035, upload-time = "2026-10-09T19:47:26.306Z" }, ] [[package]] @@ -2018,27 +2018,27 @@ wheels = [ [[package]] name = "ty" -version = "0.0.84" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/f6/34f8869c45ea87fe6a931ffa824b1fe4428167173543d92988d631add355/ty-0.0.84.tar.gz", hash = "sha256:0cefdb0cd5d399418dbe83c349ba605047b7dff815ae6f460c80e8522b40be67", size = 7409537, upload-time = "2026-09-24T13:16:24.367Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/0c/600648778e1c79133ce6b6a5c4c2531ae16b90c9b30f5041b38b574f61b8/ty-0.0.84-py3-none-linux_armv6l.whl", hash = "sha256:868438e2b9abfc835dd5b5b07ed693db95d8348ac40e3228252511d63ae87695", size = 13935333, upload-time = "2026-09-24T13:15:31.167Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3b/27f06f49945c36170765fad82076898039ae3873338e9865f876dd62f12c/ty-0.0.84-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b00f39b521f0b559cde0f6abf8dd47213b513b04f33d58efeb9b98486a16bb17", size = 13511328, upload-time = "2026-09-24T13:15:34.446Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b6/b12dd130e7f30141f0645614f454c925991cd389b1cc6c8284fd6bf32326/ty-0.0.84-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8f78c2915567f2ee62ce0c24d86091bc76f81577677755e242d09951db588b9a", size = 13445445, upload-time = "2026-09-24T13:15:37.476Z" }, - { url = "https://files.pythonhosted.org/packages/44/96/f012de4b3c1d9a4773326621380d768b4d48c47e620dc012775c5e01c7d7/ty-0.0.84-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61155cad0921e890d86d1d911a40b3a0e924ef4bdc3b4f3865a6c89c7e3012dc", size = 13471235, upload-time = "2026-09-24T13:15:40.64Z" }, - { url = "https://files.pythonhosted.org/packages/05/32/dca630f5f5353fa7bf67eebccf1549df99b16656d21071d0df6677c826a5/ty-0.0.84-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:417c3bf14947ed16b0b92976ab333c27915590354e96d4f581709068d3811355", size = 13680704, upload-time = "2026-09-24T13:15:43.817Z" }, - { url = "https://files.pythonhosted.org/packages/44/8d/33702c8f62f05ad45b33644fb914d483de6a0a7bc8e51884e9b8773e6d69/ty-0.0.84-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e06ef117759f654f5379efe9ae79bda8a186016c016f1023d05c34f8e15c1546", size = 14518923, upload-time = "2026-09-24T13:15:46.802Z" }, - { url = "https://files.pythonhosted.org/packages/c8/a1/da3246e6ef2ae0e842bca4058b25c54bee55ab058ffff4ee54e72517ec01/ty-0.0.84-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a05d1d53db8b64410ab38c6e32c5898510584869d1d5dd9c0605db81c9aef7df", size = 15027731, upload-time = "2026-09-24T13:15:49.755Z" }, - { url = "https://files.pythonhosted.org/packages/20/17/b0099098a560aeb4f9fde6c912f9a81622c3e44e6555e1fbe322b368ec31/ty-0.0.84-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b92400494ab855cfb1204ac1101e3f53b2d09a8e21d171758b2cf9acdab1809e", size = 14748260, upload-time = "2026-09-24T13:15:52.713Z" }, - { url = "https://files.pythonhosted.org/packages/60/d5/32cd67aedb271f006e716ad093806c96c5d7e2af86be8255469dbd3d76c8/ty-0.0.84-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af17eb391ae3027fed9be1780bea555c2b8a25ba68e184c627b4981b2c9eaab7", size = 14205858, upload-time = "2026-09-24T13:15:55.677Z" }, - { url = "https://files.pythonhosted.org/packages/43/c4/638ac62ca2e8eca1b92e3e16273085744fada728b993500819b5c258ca15/ty-0.0.84-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:44cf06af0e7ec78ea6d8ff33a3450fc19627756a4fba681f7410d4e3ef63047a", size = 14573196, upload-time = "2026-09-24T13:15:59.165Z" }, - { url = "https://files.pythonhosted.org/packages/a6/f6/9ee81c5d8921e5fd5f10889ae563c95cd3272b31df2ada528919ca1cef6b/ty-0.0.84-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c6e56b443ff9ba462e34bfbc1e582004c2719ed35f8f93a433744b0ba0afb2d9", size = 13480760, upload-time = "2026-09-24T13:16:02.313Z" }, - { url = "https://files.pythonhosted.org/packages/f6/83/fbe5d1177667ae16d84c3242846b143809c9cc09568b333e5a9b4d5908f6/ty-0.0.84-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b6562fcc48820c1030e2a4509ef2c14ef93043ca4860280074314ac3a825b95", size = 13696346, upload-time = "2026-09-24T13:16:05.391Z" }, - { url = "https://files.pythonhosted.org/packages/86/9a/1b4ac9ddc34cc0ab4624bc219825d66af52f1ad87f4778ad08ad03fa06d5/ty-0.0.84-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dee7421451efcbd70f03e95a8f2c8d9e5ec3d35edcd27b4095ee00ee062abfc4", size = 13959410, upload-time = "2026-09-24T13:16:08.661Z" }, - { url = "https://files.pythonhosted.org/packages/c9/04/70e289437a67dfd686056ab4fd91880261a8233033a6a95d29393741fb97/ty-0.0.84-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:31e96307cf621babe969307dd657783d672e2d8bdbd06493710da635bf63fa07", size = 14350294, upload-time = "2026-09-24T13:16:11.865Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f4/ae7dbd118425c850981d23efb850f0d9d7f6b8d2b852bde2767434f668b4/ty-0.0.84-py3-none-win32.whl", hash = "sha256:4bdc85f91cbaae35825f2b17bb4ac5de0563367875cb0f42b2bf07a1bf7ff2ba", size = 13176439, upload-time = "2026-09-24T13:16:15.049Z" }, - { url = "https://files.pythonhosted.org/packages/29/ec/994d87252f065dda5a500bc8afc726d18f6582d81d6f4ff1feb8a267c800/ty-0.0.84-py3-none-win_amd64.whl", hash = "sha256:71cc9aa7a7c89d4e12c6a814e9c0c3310ad5bee44efedbd6f5a42d91e9ac0f0d", size = 13962879, upload-time = "2026-09-24T13:16:18.191Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1c/e6e91852b72566b1ab9aca504712857dfb89324c12092a5bd61d42d34427/ty-0.0.84-py3-none-win_arm64.whl", hash = "sha256:6c9d3718c4a0d318bc9c92af9b2aa85c3294e7dff54cf1d69ad79568f56516da", size = 13656453, upload-time = "2026-09-24T13:16:21.51Z" }, +version = "0.0.86" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/fa/9b35728cc3c04d64f434c656836fa3c5f6e384a7813e48e63245510d5c69/ty-0.0.86.tar.gz", hash = "sha256:6edcaf52e207d653873d5f495c1a8470075b232030714c981f07f0a1075bc343", size = 7911723, upload-time = "2026-10-09T22:20:57.83Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/27/dd422283667ce10d60af57edc3ccab078084282ee304a5c22b5ed79390c6/ty-0.0.86-py3-none-linux_armv6l.whl", hash = "sha256:305c238df5dedf97f1b65e52d2c446a41d11551291e37054858c00c5627bf15b", size = 14159390, upload-time = "2026-10-09T22:20:17.583Z" }, + { url = "https://files.pythonhosted.org/packages/57/e3/838ea547c177983976103d3a54b346afc22d0ff97943ab58c7b7aff1f753/ty-0.0.86-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27a8a7cf26a1cd17d93586eb20aadbcc535fa6ad4b535813ee8fc52560c31efd", size = 13684426, upload-time = "2026-10-09T22:20:19.939Z" }, + { url = "https://files.pythonhosted.org/packages/bc/62/dfde84ffab702545f866350675b16f8df6555d3c41123c25e7a509fe857e/ty-0.0.86-py3-none-macosx_11_0_arm64.whl", hash = "sha256:40e5fc602af41b0ea89dd1d19fe8da7e75322e6e0db95ea23340a858b409ac2b", size = 12845457, upload-time = "2026-10-09T22:20:21.979Z" }, + { url = "https://files.pythonhosted.org/packages/47/b2/7e0e7ffc8b3961f5f74893ba9b98433b922d3f1ae6621d1e3e3e20663e1d/ty-0.0.86-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4b525ce4feb60aa2dd6becae4c470daea889149ba9dc7699ffbcf41448f69a9", size = 13656310, upload-time = "2026-10-09T22:20:24.544Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c8/977594e024d154f0e064b48a6f7e39a0e1e55a524349dba22146f01e02db/ty-0.0.86-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a000ddcc0b99bd1ab8c8277630711061a36ae83395780fdb935bad540ebe7103", size = 13887036, upload-time = "2026-10-09T22:20:26.844Z" }, + { url = "https://files.pythonhosted.org/packages/3a/75/67e8e7bf89a6be7535768380cb20569b460b89cb09d5c82ce27411782786/ty-0.0.86-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3848f3cab206e9798e5a9f7c4ce8a80297b0a88ba9aa8a091360a298e97424", size = 14805103, upload-time = "2026-10-09T22:20:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/a1/14/a7f3b88cc76156feaf5d48601bd5da96a116f846ad0abc526df03a735bcc/ty-0.0.86-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:087bad6ae8fa7248a16c01f5646c121dccaacbaf320ff544eed4e8c5c1fd7555", size = 15203931, upload-time = "2026-10-09T22:20:31.736Z" }, + { url = "https://files.pythonhosted.org/packages/a7/23/42f6ad5d4bb7091d70a0e96d7d436d6bc9a490210a928727dde5d883ed26/ty-0.0.86-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe7f2721746c9ee64626c69da9245faedc0f375024bf41458e45eb7b28fe5f63", size = 15094434, upload-time = "2026-10-09T22:20:34.269Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b8/211781d3c8de5fa88437412b7a8c0d7a632596033cff51d5004b08e94e1c/ty-0.0.86-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5c18501c54874149d05e5199211146876afeaeee2af234042e56dccc9d5812a", size = 14436812, upload-time = "2026-10-09T22:20:36.795Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2a/518cc7ef2586384345be675d470e0087591810304b5d994d0a2194c7d68e/ty-0.0.86-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0a48967c8adba6b12035243f225a0585b0bc5f0bf940a4829876bc455ebf3d2f", size = 14671116, upload-time = "2026-10-09T22:20:39.277Z" }, + { url = "https://files.pythonhosted.org/packages/7a/18/a820553ad3206d0cdfad5866dd64939c7e3be73e891d88c73f7fd3acb5d6/ty-0.0.86-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:561a4eae98f6ff05395e8d7be8151fdb328eeaead1ac63dc721ca891623f7924", size = 13586361, upload-time = "2026-10-09T22:20:41.444Z" }, + { url = "https://files.pythonhosted.org/packages/22/58/f1700176e6ae4d0a3d25e447734c60ca464d6e76ed2e8a32920ebd41e523/ty-0.0.86-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccad053ce880a1f8c0850d9dd1f277f3ee559a98bded3f93472268376ceccc2d", size = 13900289, upload-time = "2026-10-09T22:20:43.955Z" }, + { url = "https://files.pythonhosted.org/packages/59/86/2f0e3c9a055f8606911a80311763a089784b66af22dd80fd3290e3b167c6/ty-0.0.86-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7c16d22857bad1da56b147bf53ef0ed2252d7677dc5e7ee51c757b2e52ab6351", size = 14235785, upload-time = "2026-10-09T22:20:46.401Z" }, + { url = "https://files.pythonhosted.org/packages/b4/1d/72fa2347b641143b0440296144a65f424fe047302cb13af059988f1de7a4/ty-0.0.86-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d863f8f190e96878ed1900a8a6d5093f37761c0c47409e2bdb3d9370e37471e1", size = 14559840, upload-time = "2026-10-09T22:20:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/47/4a/a798be4aee4ea0bb427c22f157945d0ac41310f4d6e058b3758dfb859451/ty-0.0.86-py3-none-win32.whl", hash = "sha256:1ac3d8fe9efae04fbf1407583ed4118e0364a45b49a020d62b202846695a2ba1", size = 13133227, upload-time = "2026-10-09T22:20:51.034Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a0/c9d585fa1c5bf4ed2727f576f1542bc13e268f036451a79d057c249fc26f/ty-0.0.86-py3-none-win_amd64.whl", hash = "sha256:6fe4116fb1a7ad3c4f7804d9241dc81187717fbd9f5fed5b50e2752a20e17a67", size = 13665391, upload-time = "2026-10-09T22:20:53.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7e/d9811358bce1e54ac1b93bd44f4c45eba5e2e92672c5410248776c54319e/ty-0.0.86-py3-none-win_arm64.whl", hash = "sha256:0df36f494c5d07f68ebec3edb74fc719c4c959db7061c2018f1dff2dfe62150b", size = 13237981, upload-time = "2026-10-09T22:20:55.586Z" }, ] [[package]] From fd77c9209f11f5c544e36d2512f33a8062a2057d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 02:07:40 +0000 Subject: [PATCH 2/2] Upgrade to ruff 0.17.1 and ty 0.0.89, fix new ty diagnostics - Pin the lint job to Python 3.14: a bare `uv python install` picked up 3.15, which psycopg-binary has no wheel for, so lint never ran. - ty 0.0.86+ checks attribute/property override types and 0.0.87 enables invalid-mutable-override by default. Fixed the underlying annotations where they were wrong: admin `cards`/`parent_view_class` hold classes, card `filters` holds an Enum class, `background_command` is a tuple, `success_url` is `str | None`, `oauth_error` is declared instead of read through getattr, `UploadedFile` no longer shadows `File.size` with None, and streaming responses define a raising `content` setter. - plain-postgres: `Lookup.lookup_name` is a ClassVar (drops 4 asserts), `on_delete` lives only on ForeignKeyRel, SQLWriteCompiler is generic over its query, ChoicesMeta properties are typed against TextChoices (drops 2 ignores), and Query.chain checks for UpdateQuery directly (drops 1 ignore). - Admin list `filters` accepts the dict form at the base, so the model view's get_filter_names override goes away. - Remaining narrowings ty can't verify (FK remote_field/rel.field, AdminView.user, the MCP `mcp: AppMCP` pattern, two non-cacheable identity/output_field properties) carry targeted ignores with reasons. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SDZ9zhE4BiReFLFQ9sH2tW --- .github/workflows/test.yml | 2 +- example/app/mcp.py | 4 +- plain-admin/plain/admin/cards/base.py | 4 +- plain-admin/plain/admin/views/base.py | 14 +-- plain-admin/plain/admin/views/models.py | 11 --- plain-admin/plain/admin/views/objects.py | 22 +++-- .../tests/public/test_search_fields.py | 4 +- plain-code/pyproject.toml | 2 +- plain-dev/plain/dev/process.py | 2 +- plain-loginlink/plain/loginlink/views.py | 4 +- plain-oauth/plain/oauth/views.py | 13 ++- plain-passwords/plain/passwords/views.py | 4 +- plain-postgres/plain/postgres/enums.py | 14 +-- .../plain/postgres/fields/related.py | 9 +- .../plain/postgres/fields/related_lookups.py | 6 -- .../plain/postgres/fields/reverse_related.py | 23 +++-- .../plain/postgres/functions/datetime.py | 8 +- plain-postgres/plain/postgres/lookups.py | 61 ++++++------- plain-postgres/plain/postgres/sql/compiler.py | 12 +-- plain-postgres/plain/postgres/sql/query.py | 10 ++- plain-templates/plain/templates/views.py | 3 +- plain/plain/http/response.py | 14 +++ plain/plain/internal/files/uploadedfile.py | 4 +- .../internal/test_server_connection_errors.py | 2 +- plain/tests/internal/test_server_websocket.py | 2 +- plain/tests/internal/test_urls_preflight.py | 2 +- pyproject.toml | 2 +- uv.lock | 88 +++++++++---------- 28 files changed, 177 insertions(+), 169 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d47f85f2b9..224087247b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,7 +52,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v2 - name: Set up Python - run: uv python install + run: uv python install 3.14 - run: uv sync - run: uv run plain-code check . --skip-ty # Annotation coverage plus `ty check` over every fully typed path -- diff --git a/example/app/mcp.py b/example/app/mcp.py index baf8c99978..fded6ae514 100644 --- a/example/app/mcp.py +++ b/example/app/mcp.py @@ -29,7 +29,9 @@ class NotesTool(MCPTool): `self.user` as the demo user the bearer token maps to. """ - mcp: NotesMCP + # The documented narrowing pattern; ty flags any narrowed mutable + # attribute, but the dispatcher only ever sets a NotesMCP here. + mcp: NotesMCP # ty: ignore[invalid-mutable-override] @property def user(self) -> User: diff --git a/plain-admin/plain/admin/cards/base.py b/plain-admin/plain/admin/cards/base.py index f1839a3516..53001b27b9 100644 --- a/plain-admin/plain/admin/cards/base.py +++ b/plain-admin/plain/admin/cards/base.py @@ -29,7 +29,7 @@ class Sizes(Enum): text: str = "" link: str = "" metric: int | float | Decimal | None = None - filters: list[str] | Enum | None = None + filters: list[str] | type[Enum] | None = None # These will be accessible at render time view: AdminView @@ -97,7 +97,7 @@ def get_link(self) -> str: def get_current_filter(self) -> str: return self.request.query_params.get(f"{self.get_slug()}.filter", "") - def get_filters(self) -> list[str] | Enum | None: + def get_filters(self) -> list[str] | type[Enum] | None: if isinstance(self.filters, list): # Avoid mutating the class attribute return self.filters.copy() diff --git a/plain-admin/plain/admin/views/base.py b/plain-admin/plain/admin/views/base.py index fb7a135fad..e306cb0cd1 100644 --- a/plain-admin/plain/admin/views/base.py +++ b/plain-admin/plain/admin/views/base.py @@ -14,6 +14,7 @@ from .types import Img if TYPE_CHECKING: + from app.users.models import User from plain.http import Response from plain.postgres import Model @@ -26,7 +27,10 @@ class AdminView(AuthView, TemplateView): admin_required = True - user: Model # Always set — admin_required guarantees authentication + # Always a user once `check_auth` passes -- admin_required rejects anonymous + # requests before any handler runs. Narrowing a writable attribute is + # unsound to the checker, which is the cost of not re-checking everywhere. + user: User # ty: ignore[invalid-mutable-override] # True for framework-provided views (index, search, settings, etc.) # Available for use in ADMIN_HAS_PERMISSION to make per-view decisions. @@ -61,7 +65,7 @@ def has_permission(cls, user: Model) -> bool: extra_links: ClassVar[dict[str, str]] = {} field_templates: ClassVar[dict[str, str]] = {} - parent_view_class: AdminView | None = None + parent_view_class: type[AdminView] | None = None # Set dynamically by AdminViewset.get_views() viewset: type[AdminViewset] | None = None @@ -84,7 +88,7 @@ def get_delete_url(self, obj: Any) -> str: return "" template_name = "admin/page.html" - cards: tuple[Card, ...] = () + cards: tuple[type[Card], ...] = () def __init_subclass__(cls, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) @@ -160,7 +164,7 @@ def get_path(cls) -> str: return cls.path @classmethod - def get_parent_view_classes(cls) -> list[AdminView]: + def get_parent_view_classes(cls) -> list[type[AdminView]]: parents = [] parent = cls.parent_view_class while parent: @@ -194,7 +198,7 @@ def get_links(self) -> dict[str, str]: def get_extra_links(self) -> dict[str, str]: return self.extra_links.copy() - def get_cards(self) -> tuple[Card, ...]: + def get_cards(self) -> tuple[type[Card], ...]: return self.cards def get_field_value(self, obj: Any, field: str) -> Any: diff --git a/plain-admin/plain/admin/views/models.py b/plain-admin/plain/admin/views/models.py index 969e91af73..d1c8551b1c 100644 --- a/plain-admin/plain/admin/views/models.py +++ b/plain-admin/plain/admin/views/models.py @@ -53,10 +53,6 @@ class AdminModelListView(AdminListView): queryset_order: tuple[FieldRef, ...] = () search_fields: tuple[FieldRef, ...] = ("id",) - # Filter *names* shown in the UI, not fields. Can also be a dict mapping a - # filter name to a Q object, which filters the queryset automatically. - filters: tuple[str, ...] | dict[str, Q] = () - def __init_subclass__(cls, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) # Normalize the declared field references now, so a reference to @@ -99,13 +95,6 @@ def get_initial_queryset(self) -> postgres.QuerySet: """Override this to customize the base queryset (e.g., add annotations).""" return self.model.query.all() - def get_filter_names(self) -> tuple[str, ...]: - """Return filter names. Supports both tuple[str, ...] and dict[str, Q] formats.""" - filters = self.filters - if isinstance(filters, dict): - return tuple(filters.keys()) - return super().get_filter_names() - def get_search_fields(self) -> tuple[str, ...]: """`search_fields` as lookup paths, checked against the view's model.""" return field_lookup_paths( diff --git a/plain-admin/plain/admin/views/objects.py b/plain-admin/plain/admin/views/objects.py index fde90c06fd..0d9bf407f0 100644 --- a/plain-admin/plain/admin/views/objects.py +++ b/plain-admin/plain/admin/views/objects.py @@ -4,7 +4,7 @@ from plain.htmx.views import HTMXView from plain.http import RedirectResponse, Response -from plain.postgres import QuerySet +from plain.postgres import Q, QuerySet from plain.templates.views import ( CreateView, DeleteView, @@ -62,9 +62,10 @@ class AdminListView(HTMXView, AdminView, ListView): search_fields: tuple[FieldRef, ...] = () actions: tuple[str, ...] = () # Filter *names* shown in the UI, not fields -- `filter_queryset` maps a - # name to whatever query it means. - filters: tuple[str, ...] = () - page_size: int = 20 + # name to whatever query it means. `AdminModelListView` also takes a dict + # mapping a filter name to a Q object, which filters the queryset itself. + filters: tuple[str, ...] | dict[str, Q] = () + page_size = 20 allow_global_search = False @cached_property @@ -75,13 +76,16 @@ def filter(self) -> str: def get_objects(self) -> list[Any] | QuerySet[Any]: return self.process_objects() - def get_page_size(self) -> int: + def get_page_size(self) -> int | None: + default = self.page_size + if default is None: + return None try: - page_size = int(self.request.query_params.get("page_size", self.page_size)) + page_size = int(self.request.query_params.get("page_size", default)) except ValueError: - return self.page_size + return default if page_size < 1: - return self.page_size + return default return page_size def get_template_context(self) -> dict[str, Any]: @@ -245,7 +249,7 @@ def get_actions(self) -> tuple[str, ...]: return self.actions def get_filter_names(self) -> tuple[str, ...]: - return self.filters + return tuple(self.filters) def get_search_fields(self) -> tuple[str, ...]: """`search_fields` as lookup paths, whichever way each was declared. diff --git a/plain-admin/tests/public/test_search_fields.py b/plain-admin/tests/public/test_search_fields.py index da4285446e..fdab4a1147 100644 --- a/plain-admin/tests/public/test_search_fields.py +++ b/plain-admin/tests/public/test_search_fields.py @@ -124,8 +124,10 @@ class PropertySearch(AdminModelListView): model = PinnedNavItem title = "Property search" + # A read-only property where the base declares an attribute: unsound to + # the checker, and exactly the runtime path under test. @property - def search_fields(self) -> tuple[FieldRef, ...]: + def search_fields(self) -> tuple[FieldRef, ...]: # ty: ignore[invalid-property-type-override] return (PinnedNavItem.view_slug,) diff --git a/plain-code/pyproject.toml b/plain-code/pyproject.toml index cc612b0c3f..577e337e2a 100644 --- a/plain-code/pyproject.toml +++ b/plain-code/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "plain>=0.155.0,<1.0.0", # Bounded so every checkout and every app lints with the same rule set — # ruff adds rules (and import-sort tweaks) in minor releases. - "ruff>=0.17.0,<0.18", + "ruff>=0.17.1,<0.18", "ty>=0.0.82,<1.0.0", "httpx>=0.27", "tomlkit>=0.11.0", diff --git a/plain-dev/plain/dev/process.py b/plain-dev/plain/dev/process.py index 60c8e0907b..ad1fac5477 100644 --- a/plain-dev/plain/dev/process.py +++ b/plain-dev/plain/dev/process.py @@ -37,7 +37,7 @@ class Supervisor: state_filename: str log_dir: Path # Foreground command that re-runs this supervisor, e.g. ["dev", "services"]. - background_command: list[str] + background_command: tuple[str, ...] # Human label for "already running" warnings, e.g. "Services". display_name: str diff --git a/plain-loginlink/plain/loginlink/views.py b/plain-loginlink/plain/loginlink/views.py index e703ba00b3..3ac45d8281 100644 --- a/plain-loginlink/plain/loginlink/views.py +++ b/plain-loginlink/plain/loginlink/views.py @@ -50,9 +50,9 @@ def get_success_url(self, form: LoginLinkForm) -> str: if next_url := form.cleaned_data.get("next"): # Keep the next URL in the query string so the sent # view can redirect to it if reloaded and logged in already. - return f"{self.success_url}?next={next_url}" + return f"{super().get_success_url(form)}?next={next_url}" else: - return self.success_url + return super().get_success_url(form) class LoginLinkSentView(AuthView, TemplateView): diff --git a/plain-oauth/plain/oauth/views.py b/plain-oauth/plain/oauth/views.py index 40b92855ca..7730f05b5f 100644 --- a/plain-oauth/plain/oauth/views.py +++ b/plain-oauth/plain/oauth/views.py @@ -31,6 +31,7 @@ class OAuthCallbackView(TemplateView): """ template_name = "oauth/error.html" + oauth_error: OAuthError | None = None def get(self) -> Response: provider = self.url_kwargs["provider"] @@ -44,17 +45,15 @@ def get(self) -> Response: return self.render(status_code=400) def get_template_names(self) -> list[str]: - names = [] - if ( - oauth_error := getattr(self, "oauth_error", None) - ) and oauth_error.template_name: - names.append(oauth_error.template_name) - names.append(self.template_name) + names: list[str] = [] + if self.oauth_error and self.oauth_error.template_name: + names.append(self.oauth_error.template_name) + names.extend(super().get_template_names()) return names def get_template_context(self) -> dict: context = super().get_template_context() - context["oauth_error"] = getattr(self, "oauth_error", None) + context["oauth_error"] = self.oauth_error return context diff --git a/plain-passwords/plain/passwords/views.py b/plain-passwords/plain/passwords/views.py index 3b335c255d..a7e1210c8e 100644 --- a/plain-passwords/plain/passwords/views.py +++ b/plain-passwords/plain/passwords/views.py @@ -166,7 +166,9 @@ class PasswordLoginView(AuthView, FormView[PasswordLoginForm]): def get(self) -> Response: # Redirect if the user is already logged in if self.user: - return RedirectResponse(self.success_url, status_code=302) + return RedirectResponse( + str(self.success_url) if self.success_url else "/", status_code=302 + ) return super().get() diff --git a/plain-postgres/plain/postgres/enums.py b/plain-postgres/plain/postgres/enums.py index e7c4ab3bee..4eafe3413e 100644 --- a/plain-postgres/plain/postgres/enums.py +++ b/plain-postgres/plain/postgres/enums.py @@ -49,21 +49,21 @@ def __contains__(cls, member: object) -> bool: # ty: ignore[invalid-method-over return super().__contains__(member) @property - def names(cls) -> list[str]: + def names(cls: type[TextChoices]) -> list[str]: empty = ["__empty__"] if hasattr(cls, "__empty__") else [] - return empty + [member.name for member in cls] # ty: ignore[unresolved-attribute] + return empty + [member.name for member in cls] @property - def choices(cls) -> list[tuple[Any, str]]: + def choices(cls: type[TextChoices]) -> list[tuple[Any, str]]: empty = [(None, cls.__empty__)] if hasattr(cls, "__empty__") else [] - return empty + [(member.value, member.label) for member in cls] # ty: ignore[unresolved-attribute] + return empty + [(member.value, member.label) for member in cls] @property - def labels(cls) -> list[str]: + def labels(cls: type[TextChoices]) -> list[str]: return [label for _, label in cls.choices] @property - def values(cls) -> list[Any]: + def values(cls: type[TextChoices]) -> list[Any]: return [value for value, _ in cls.choices] @@ -72,6 +72,8 @@ class TextChoices(str, enum.Enum, metaclass=ChoicesMeta): # Dynamically set by metaclass _label_: str + # Optional label for a blank choice, set by subclasses that want one + __empty__: str @DynamicClassAttribute def label(self) -> str: diff --git a/plain-postgres/plain/postgres/fields/related.py b/plain-postgres/plain/postgres/fields/related.py index b33ac92335..f0f5751d67 100644 --- a/plain-postgres/plain/postgres/fields/related.py +++ b/plain-postgres/plain/postgres/fields/related.py @@ -304,8 +304,10 @@ def get_cache_name(self) -> str: class ForeignKeyField(ColumnField, RelatedField): # Narrow the base class's `ForeignObjectRel` annotation — a FK's remote_field - # is always a ForeignKeyRel with a concrete on_delete action. - remote_field: ForeignKeyRel + # is always a ForeignKeyRel with a concrete on_delete action. Only + # __init__ and __deepcopy__ assign it, both with a ForeignKeyRel, which + # is the write ty can't rule out. + remote_field: ForeignKeyRel # ty: ignore[invalid-mutable-override] """ Provide a many-to-one relation by adding a column to the local model @@ -590,7 +592,8 @@ class ManyToManyField(RelatedField): """ # ManyToManyField uses ManyToManyRel which has through/through_fields - remote_field: ManyToManyRel + # (see ForeignKeyField.remote_field for the ignore) + remote_field: ManyToManyRel # ty: ignore[invalid-mutable-override] def __init__( self, diff --git a/plain-postgres/plain/postgres/fields/related_lookups.py b/plain-postgres/plain/postgres/fields/related_lookups.py index 4a7a48f1dc..6612c1da66 100644 --- a/plain-postgres/plain/postgres/fields/related_lookups.py +++ b/plain-postgres/plain/postgres/fields/related_lookups.py @@ -58,12 +58,6 @@ def get_prep_lookup(self) -> list[Any]: class RelatedLookupMixin(Lookup): - # Type hints for attributes/methods expected from Lookup base class - lhs: Any - rhs: Any - prepare_rhs: bool - lookup_name: str | None - def get_prep_lookup(self) -> Any: if not hasattr(self.rhs, "resolve_expression"): self.rhs = get_normalized_value(self.rhs, self.lhs) diff --git a/plain-postgres/plain/postgres/fields/reverse_related.py b/plain-postgres/plain/postgres/fields/reverse_related.py index 28fa962e55..09f2876ada 100644 --- a/plain-postgres/plain/postgres/fields/reverse_related.py +++ b/plain-postgres/plain/postgres/fields/reverse_related.py @@ -45,8 +45,6 @@ class ForeignObjectRel(FieldCacheMixin): allow_null = True empty_strings_allowed = False - # Type annotations for instance attributes - on_delete: OnDelete | None # The target model as given to the field: a "package.Model" string (or # "self") until lazy_related_operation resolves it to the class. Read # `model` for the resolved class. @@ -58,7 +56,6 @@ def __init__( field: RelatedField, to: str | type[Model], related_query_name: str | None = None, - on_delete: OnDelete | None = None, ): # Annotated here rather than as a class-body attribute: RelatedField # is itself a descriptor, so a class-body `field: RelatedField` would @@ -67,7 +64,6 @@ def __init__( self.field: RelatedField = field self.model_ref = to self.related_query_name = related_query_name - self.on_delete = on_delete self.symmetrical = False @@ -125,7 +121,6 @@ def identity(self) -> tuple[Any, ...]: self.field, self.model_ref, self.related_query_name, - self.on_delete, self.symmetrical, ) @@ -173,9 +168,6 @@ class ForeignKeyRel(ForeignObjectRel): one-to-many reverse relations. """ - # Type annotations for instance attributes - on_delete: OnDelete # narrowed: FK rel always has a concrete action - def __init__( self, *, @@ -188,12 +180,17 @@ def __init__( field=field, to=to, related_query_name=related_query_name, - on_delete=on_delete, ) # Narrows self.field from RelatedField to ForeignKeyField (see # ForeignObjectRel.__init__ for why this can't be a class-body - # annotation). - self.field: ForeignKeyField + # annotation). The rel is built by its field and never repointed at + # another kind of field, which is the write ty can't rule out. + self.field: ForeignKeyField # ty: ignore[invalid-mutable-override] + self.on_delete = on_delete + + @property + def identity(self) -> tuple[Any, ...]: + return super().identity + (self.on_delete,) def __getstate__(self) -> dict[str, Any]: state = super().__getstate__() @@ -235,8 +232,8 @@ def __init__( ) # Narrows self.field from RelatedField to ManyToManyField (see # ForeignObjectRel.__init__ for why this can't be a class-body - # annotation). - self.field: ManyToManyField + # annotation, and ForeignKeyRel.__init__ for the ignore). + self.field: ManyToManyField # ty: ignore[invalid-mutable-override] self.through_ref = through self.through_fields = through_fields diff --git a/plain-postgres/plain/postgres/functions/datetime.py b/plain-postgres/plain/postgres/functions/datetime.py index c5015ddf71..3a203b8e53 100644 --- a/plain-postgres/plain/postgres/functions/datetime.py +++ b/plain-postgres/plain/postgres/functions/datetime.py @@ -1,4 +1,6 @@ +from collections.abc import Callable from datetime import datetime +from functools import cached_property from typing import TYPE_CHECKING, Any from plain.postgres.dialect import ( @@ -349,7 +351,11 @@ def resolve_expression( ) return copy - def convert_value( + @cached_property + def convert_value(self) -> Callable[[Any, Any, Any], Any]: + return self._convert_truncated_value + + def _convert_truncated_value( self, value: Any, expression: Any, connection: DatabaseConnection ) -> Any: if isinstance(self.output_field, DateTimeField): diff --git a/plain-postgres/plain/postgres/lookups.py b/plain-postgres/plain/postgres/lookups.py index 41014fcea8..822c16e775 100644 --- a/plain-postgres/plain/postgres/lookups.py +++ b/plain-postgres/plain/postgres/lookups.py @@ -3,7 +3,7 @@ import math from collections.abc import Sequence from functools import cached_property -from typing import TYPE_CHECKING, Any, Self +from typing import TYPE_CHECKING, Any, ClassVar, Self from plain.postgres.dialect import ( OPERATORS, @@ -36,7 +36,8 @@ class Lookup(Expression): - lookup_name: str | None = None + # Every concrete lookup names itself; only abstract bases leave it unset. + lookup_name: ClassVar[str] prepare_rhs: bool = True can_use_none_as_rhs: bool = False lhs: Any @@ -165,11 +166,13 @@ def get_group_by_cols(self) -> list[Any]: return cols @cached_property - def output_field(self) -> BooleanField: + def output_field(self) -> Field: return BooleanField() + # Expression caches its identity, but a lookup's must follow lhs and rhs, + # which resolve_expression() reassigns on a copy. @property - def identity(self) -> tuple[type[Lookup], Any, Any]: + def identity(self) -> tuple[type[Lookup], Any, Any]: # ty: ignore[invalid-property-type-override] return self.__class__, self.lhs, self.rhs def __eq__(self, other: object) -> bool: @@ -214,7 +217,7 @@ class Transform(RegisterLookupMixin, Func): lookup_name: str | None = None bilateral: bool = False - arity: int = 1 + arity = 1 @property def lhs(self) -> Any: @@ -234,9 +237,6 @@ class BuiltinLookup(Lookup): def process_lhs( self, compiler: SQLCompiler, connection: DatabaseConnection, lhs: Any = None ) -> tuple[str, list[Any]]: - assert self.lookup_name is not None, ( - "lookup_name must be set on Lookup subclass" - ) lhs_sql, params = super().process_lhs(compiler, connection, lhs) lhs_sql = lookup_cast(self.lookup_name, self.lhs.output_field) % lhs_sql return lhs_sql, list(params) @@ -251,9 +251,6 @@ def as_sql( return f"{lhs_sql} {rhs_sql}", params def get_rhs_op(self, connection: DatabaseConnection, rhs: str | list[str]) -> str: - assert self.lookup_name is not None, ( - "lookup_name must be set on Lookup subclass" - ) return OPERATORS[self.lookup_name] % rhs @@ -382,7 +379,7 @@ def as_sql( @Field.register_lookup class Exact(FieldGetDbPrepValueMixin, BuiltinLookup): - lookup_name: str = "exact" + lookup_name = "exact" def get_prep_lookup(self) -> Any: from plain.postgres.sql.query import Query # avoid circular import @@ -414,28 +411,28 @@ def as_sql( @Field.register_lookup class IExact(BuiltinLookup): - lookup_name: str = "iexact" + lookup_name = "iexact" prepare_rhs: bool = False @Field.register_lookup class GreaterThan(FieldGetDbPrepValueMixin, BuiltinLookup): - lookup_name: str = "gt" + lookup_name = "gt" @Field.register_lookup class GreaterThanOrEqual(FieldGetDbPrepValueMixin, BuiltinLookup): - lookup_name: str = "gte" + lookup_name = "gte" @Field.register_lookup class LessThan(FieldGetDbPrepValueMixin, BuiltinLookup): - lookup_name: str = "lt" + lookup_name = "lt" @Field.register_lookup class LessThanOrEqual(FieldGetDbPrepValueMixin, BuiltinLookup): - lookup_name: str = "lte" + lookup_name = "lte" class IntegerFieldOverflow: @@ -500,7 +497,7 @@ class IntegerLessThanOrEqual(IntegerFieldOverflow, LessThanOrEqual): @Field.register_lookup class In(FieldGetDbPrepValueIterableMixin, BuiltinLookup): - lookup_name: str = "in" + lookup_name = "in" def get_prep_lookup(self) -> Any: from plain.postgres.sql.query import Query # avoid circular import @@ -571,7 +568,7 @@ class AnyOf(FieldGetDbPrepValueIterableMixin, BuiltinLookup): values are keys already -- and the kwarg path has `__in` for that. """ - lookup_name: str = "any_of" + lookup_name = "any_of" def get_prep_lookup(self) -> Any: """Refuse everything a single bound array can't carry. @@ -692,9 +689,6 @@ def get_rhs_op(self, connection: DatabaseConnection, rhs: str | list[str]) -> st # SQL reference values or SQL transformations we need the correct # pattern added. if hasattr(self.rhs, "as_sql") or self.bilateral_transforms: - assert self.lookup_name is not None, ( - "lookup_name must be set on Lookup subclass" - ) pattern = PATTERN_OPS[self.lookup_name].format(PATTERN_ESC) return pattern.format(rhs) else: @@ -714,39 +708,39 @@ def process_rhs( @Field.register_lookup class Contains(PatternLookup): - lookup_name: str = "contains" + lookup_name = "contains" @Field.register_lookup class IContains(Contains): - lookup_name: str = "icontains" + lookup_name = "icontains" @Field.register_lookup class StartsWith(PatternLookup): - lookup_name: str = "startswith" + lookup_name = "startswith" param_pattern: str = "%s%%" @Field.register_lookup class IStartsWith(StartsWith): - lookup_name: str = "istartswith" + lookup_name = "istartswith" @Field.register_lookup class EndsWith(PatternLookup): - lookup_name: str = "endswith" + lookup_name = "endswith" param_pattern: str = "%%%s" @Field.register_lookup class IEndsWith(EndsWith): - lookup_name: str = "iendswith" + lookup_name = "iendswith" @Field.register_lookup class Range(FieldGetDbPrepValueIterableMixin, BuiltinLookup): - lookup_name: str = "range" + lookup_name = "range" def get_rhs_op(self, connection: DatabaseConnection, rhs: str | list[str]) -> str: # Range lookup always receives a list of two elements from process_rhs @@ -756,7 +750,7 @@ def get_rhs_op(self, connection: DatabaseConnection, rhs: str | list[str]) -> st @Field.register_lookup class IsNull(BuiltinLookup): - lookup_name: str = "isnull" + lookup_name = "isnull" prepare_rhs: bool = False def as_sql( @@ -775,7 +769,7 @@ def as_sql( @Field.register_lookup class Regex(BuiltinLookup): - lookup_name: str = "regex" + lookup_name = "regex" prepare_rhs: bool = False def as_sql( @@ -792,7 +786,7 @@ def as_sql( @Field.register_lookup class IRegex(Regex): - lookup_name: str = "iregex" + lookup_name = "iregex" class YearLookup(Lookup): @@ -828,9 +822,6 @@ def as_sql( return super().as_sql(compiler, connection) def get_direct_rhs_sql(self, connection: DatabaseConnection, rhs: str) -> str: - assert self.lookup_name is not None, ( - "lookup_name must be set on Lookup subclass" - ) return OPERATORS[self.lookup_name] % rhs def get_bound_params(self, start: Any, finish: Any) -> tuple[Any, ...]: diff --git a/plain-postgres/plain/postgres/sql/compiler.py b/plain-postgres/plain/postgres/sql/compiler.py index a4ecef773f..8189250f13 100644 --- a/plain-postgres/plain/postgres/sql/compiler.py +++ b/plain-postgres/plain/postgres/sql/compiler.py @@ -1625,7 +1625,7 @@ def execute_sql( # ty: ignore[invalid-method-override] return convert_returning_rows(rows, self.returning_fields, self.connection) -class SQLWriteCompiler(SQLCompiler): +class SQLWriteCompiler[Q: UpdateQuery | DeleteQuery](SQLCompiler): """Base for the UPDATE and DELETE compilers. Both of their queries can carry returning_fields, so both run the @@ -1633,7 +1633,7 @@ class SQLWriteCompiler(SQLCompiler): rows when fields were asked for. """ - query: UpdateQuery | DeleteQuery + query: Q def lock_only_the_target(self, inner: Query) -> None: """Point the sub-select's lock at the table being written. @@ -1671,9 +1671,7 @@ def execute_sql(self, result_type: str) -> Any: # ty: ignore[invalid-method-ove cursor.close() -class SQLDeleteCompiler(SQLWriteCompiler): - query: DeleteQuery - +class SQLDeleteCompiler(SQLWriteCompiler["DeleteQuery"]): @cached_property def single_alias(self) -> bool: # Ensure base table is in aliases. @@ -1743,9 +1741,7 @@ def as_sql( return self._as_sql(outerq) -class SQLUpdateCompiler(SQLWriteCompiler): - query: UpdateQuery - +class SQLUpdateCompiler(SQLWriteCompiler["UpdateQuery"]): def as_sql( self, with_limits: bool = True, with_col_aliases: bool = False ) -> SqlWithParams: diff --git a/plain-postgres/plain/postgres/sql/query.py b/plain-postgres/plain/postgres/sql/query.py index f8593f3ec1..9191bd8dc9 100644 --- a/plain-postgres/plain/postgres/sql/query.py +++ b/plain-postgres/plain/postgres/sql/query.py @@ -272,8 +272,10 @@ def __init__(self, model: type[Model] | None, alias_cols: bool = True): # Maps alias -> Annotation Expression. self.annotations = {} + # Follows the current select, so it can't be cached like an expression's, + # and None ("not a single column") is an answer Subquery relies on. @property - def output_field(self) -> Field | None: + def output_field(self) -> Field | None: # ty: ignore[invalid-property-type-override] if len(self.select) == 1: select = self.select[0] return getattr(select, "target", None) or select.field @@ -356,7 +358,7 @@ def chain(self, klass: None = None) -> Self: ... @overload def chain(self, klass: type[QueryType]) -> QueryType: ... - def chain(self, klass: type[Query] | None = None) -> Query: + def chain(self: Query, klass: type[Query] | None = None) -> Query: """ Return a copy of the current Query that's ready for another operation. The klass argument changes the type of the Query, e.g. UpdateQuery. @@ -367,8 +369,8 @@ def chain(self, klass: type[Query] | None = None) -> Query: if not obj.filter_is_sticky: obj.used_aliases = set() obj.filter_is_sticky = False - if hasattr(obj, "_setup_query"): - obj._setup_query() # ty: ignore[call-non-callable] + if isinstance(obj, UpdateQuery): + obj._setup_query() return obj def relabeled_clone(self, change_map: dict[str, str]) -> Self: diff --git a/plain-templates/plain/templates/views.py b/plain-templates/plain/templates/views.py index 503db74505..700ed3b418 100644 --- a/plain-templates/plain/templates/views.py +++ b/plain-templates/plain/templates/views.py @@ -1,5 +1,4 @@ from abc import ABC, abstractmethod -from collections.abc import Callable from functools import cached_property from typing import Any, NoReturn @@ -142,7 +141,7 @@ class FormView[F: "BaseForm"](TemplateView): """ form_class: type[F] | None = None - success_url: Callable | str | None = None + success_url: str | None = None def get_form(self) -> F: """Return an instance of the form to be used in this view.""" diff --git a/plain/plain/http/response.py b/plain/plain/http/response.py index e5902deedc..c835772752 100644 --- a/plain/plain/http/response.py +++ b/plain/plain/http/response.py @@ -554,6 +554,13 @@ def content(self) -> bytes: "`streaming_content` instead." ) + @content.setter + def content(self, value: bytes | str | Iterator[bytes] | None) -> None: + raise AttributeError( + f"This {self.__class__.__name__} instance has no `content` attribute. Use " + "`streaming_content` instead." + ) + @property def streaming_content(self) -> Iterator[bytes]: return map(self.make_bytes, self._iterator) @@ -612,6 +619,13 @@ def content(self) -> bytes: "`streaming_content` instead." ) + @content.setter + def content(self, value: bytes | str | Iterator[bytes] | None) -> None: + raise AttributeError( + f"This {self.__class__.__name__} instance has no `content` attribute. Use " + "`streaming_content` instead." + ) + def __iter__(self) -> Iterator[bytes]: raise TypeError( f"{self.__class__.__name__} is async — use `async for` / `__aiter__` instead." diff --git a/plain/plain/internal/files/uploadedfile.py b/plain/plain/internal/files/uploadedfile.py index 55fe7a7e1b..604a1c2b3f 100644 --- a/plain/plain/internal/files/uploadedfile.py +++ b/plain/plain/internal/files/uploadedfile.py @@ -35,7 +35,9 @@ def __init__( content_type_extra: dict[str, str] | None = None, ) -> None: super().__init__(file, name) - self.size: int | None = size + if size is not None: + # Otherwise `File.size` works it out from the file itself. + self.size = size self.content_type = content_type self.charset = charset self.content_type_extra = content_type_extra diff --git a/plain/tests/internal/test_server_connection_errors.py b/plain/tests/internal/test_server_connection_errors.py index 7867ad23b3..8e9f1bdd85 100644 --- a/plain/tests/internal/test_server_connection_errors.py +++ b/plain/tests/internal/test_server_connection_errors.py @@ -61,5 +61,5 @@ async def scenario() -> None: if record.getMessage() == "Unexpected connection error" ] assert len(records) == 1 - assert records[0].exc_info is not None + assert records[0].exc_info assert records[0].exc_info[0] is RuntimeError diff --git a/plain/tests/internal/test_server_websocket.py b/plain/tests/internal/test_server_websocket.py index bda20a8e6a..b179a15702 100644 --- a/plain/tests/internal/test_server_websocket.py +++ b/plain/tests/internal/test_server_websocket.py @@ -202,7 +202,7 @@ async def run() -> None: errors = [r for r in request_log.records if r.levelno >= logging.ERROR] assert len(errors) == 1 - assert errors[0].exc_info is not None + assert errors[0].exc_info assert "websocket view boom" in str(errors[0].exc_info[1]) socket_spans = [s for s in spans if s.name.startswith("WEBSOCKET")] diff --git a/plain/tests/internal/test_urls_preflight.py b/plain/tests/internal/test_urls_preflight.py index 799182ae8c..a6f099b178 100644 --- a/plain/tests/internal/test_urls_preflight.py +++ b/plain/tests/internal/test_urls_preflight.py @@ -79,7 +79,7 @@ def test_duplicate_namespace_among_includes_raises(): """Two `include()`s under one router sharing a namespace is always a mistake (one side is unreachable from `reverse()`) — registration-time error, not just a preflight warning.""" - child_urls = [path("home/", _View, name="home")] + child_urls = (path("home/", _View, name="home"),) class _ChildRouter(Router): namespace = "shared" diff --git a/pyproject.toml b/pyproject.toml index 2381598998..54b2f6060e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ dev = [ # uv.lock: every ty release so far has moved which diagnostics fire on the # typed model surface, so a bump is a deliberate change that has to pass # plain-postgres/tests/typing/ -- never a silent lockfile update. - "ty>=0.0.86", + "ty>=0.0.89", "psycopg[binary]>=3.2.12", ] diff --git a/uv.lock b/uv.lock index 5948b40eed..dee7ca7a81 100644 --- a/uv.lock +++ b/uv.lock @@ -936,7 +936,7 @@ dev = [ requires-dist = [ { name = "httpx", specifier = ">=0.27" }, { name = "plain", editable = "plain" }, - { name = "ruff", specifier = ">=0.17.0,<0.18" }, + { name = "ruff", specifier = ">=0.17.1,<0.18" }, { name = "tomlkit", specifier = ">=0.11.0" }, { name = "ty", specifier = ">=0.0.82,<1.0.0" }, ] @@ -1632,7 +1632,7 @@ dev = [ { name = "plain-vendor", editable = "plain-vendor" }, { name = "plainx-dev", editable = "plainx-dev" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.2.12" }, - { name = "ty", specifier = ">=0.0.86" }, + { name = "ty", specifier = ">=0.0.89" }, ] [[package]] @@ -1950,27 +1950,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e9/a7/70debb024dfacda67b8e560cc7511f52b34b9348a8cc8c1ec23036dcd51d/ruff-0.17.0.tar.gz", hash = "sha256:5cd03240d8208a557c2a9655a5cb07ebe36aa6bb35065f97d48c1f6adef5a322", size = 5320554, upload-time = "2026-10-09T19:47:29.248Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/f8/ee5ab9da6089eae2a33e6008b01deb1eda19992c1c8e10661e98cee1640f/ruff-0.17.0-py3-none-linux_armv6l.whl", hash = "sha256:0e271826af9a20d18c6cfae8c51e82959167c24859686ddd3eb9a7f0842ce81e", size = 10148907, upload-time = "2026-10-09T19:46:38.695Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d9/2f81fb5a9d580afbb11b1c8ff915233a11f2a1b27405d7991f183c5e1976/ruff-0.17.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5f0ca4a40f81403689c04f12966e22f44e329ae362072d8f1587b7bda87f603b", size = 10224208, upload-time = "2026-10-09T19:46:41.711Z" }, - { url = "https://files.pythonhosted.org/packages/a7/20/643f3c8f75594f937b2bf74801241c56a2e2b8e139d24dff8b66b28cdd7f/ruff-0.17.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cbf7149e0927dc3295d5d64679a4765576eef71b00782b2ae969ef82274d6bb9", size = 9273324, upload-time = "2026-10-09T19:46:44.323Z" }, - { url = "https://files.pythonhosted.org/packages/ec/91/627700b233d367736cb274f1bd0b47d1f2b12f68878192812bd875adadc3/ruff-0.17.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ee90156522998c3037059d8f66885c8adeeaf7643bdce2caceee196ecd23e0", size = 9953190, upload-time = "2026-10-09T19:46:47.155Z" }, - { url = "https://files.pythonhosted.org/packages/cd/92/91f7b5ed39490f89d6cbf56e1f543c383667a725efa8e2c2dee0f01f5591/ruff-0.17.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d8e4a002a94cd9d0dc48b51dc69d807a172b5b9bf2b668e656424dc5b55ead1", size = 9961746, upload-time = "2026-10-09T19:46:50.098Z" }, - { url = "https://files.pythonhosted.org/packages/87/c5/7310f9fc63ce11ff6394edbd5e85433dfb0e14c9fbf6ccc97f1538491bc7/ruff-0.17.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0b8a60c06a218c337e1161638d34757f83449243e2db161483ddf948e53ad14", size = 10625483, upload-time = "2026-10-09T19:46:53.379Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8d/97443f0dca4a03a0bc7629fd396fd494a1cb6666121e38c5075acb217d8f/ruff-0.17.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a330178bdffc4205dbf3bda11d93e059e388fd6546f8cdd304501a9160363c0d", size = 11350129, upload-time = "2026-10-09T19:46:56.486Z" }, - { url = "https://files.pythonhosted.org/packages/9c/0a/c525efd9777be4b6b012e6969a3012648468e7e6c4b3e5b46af69f46e8eb/ruff-0.17.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7bb08489e234876fa2da67ae3ea938e9a2156da80293e0e4365abd6973d98329", size = 10906223, upload-time = "2026-10-09T19:47:00.263Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3c/4a01195d93420cad1175bedad13a515dc8a56f95a6e39789b92e582682f5/ruff-0.17.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc73e7c133e82d55b5f15897b2a442d72c0cb4a0c886c46801ce3c247150b60c", size = 10386314, upload-time = "2026-10-09T19:47:03.057Z" }, - { url = "https://files.pythonhosted.org/packages/c7/72/1a3951665485a921f6375f91e754a1854d5a645d41acc3642668064ff64d/ruff-0.17.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:db4f74c533403ab70fe4007873f6ae0c9f94a8b03158cf48d78788e47cdbe399", size = 10565464, upload-time = "2026-10-09T19:47:05.831Z" }, - { url = "https://files.pythonhosted.org/packages/90/8c/539b4d8c082f57e18db8ae2be85a460d77861c79dcd5798e32b536a6a06f/ruff-0.17.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3d8cc360e666d1914e47b0777c6906d70cf18891a55532bd0a16844195d70859", size = 10079468, upload-time = "2026-10-09T19:47:08.617Z" }, - { url = "https://files.pythonhosted.org/packages/e0/b8/84286966db79434e8c26b585b0a0f6897cb3ab1c51a4aa4df10c28488b62/ruff-0.17.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d66de796b726c4801e05fa99a2a8d7a780e107be222486c304ab61765561e866", size = 9958556, upload-time = "2026-10-09T19:47:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/69/50/27b6eed27b83fcdd5bfa0d52b83231e29094754374698da404d094487ae3/ruff-0.17.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c3f268baf004aea944f040623327119527ea231af15f7fb7890e82cea0679589", size = 10352173, upload-time = "2026-10-09T19:47:14.188Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/955399fd13044cd827862044117d784a59e3196f6cce7424908ac9a7f914/ruff-0.17.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:864b6c1acb6b0bccf94b5a3938a1531fd09aaca5e5659a2e7bf0f3cf2a685540", size = 10791761, upload-time = "2026-10-09T19:47:16.931Z" }, - { url = "https://files.pythonhosted.org/packages/ae/bf/024e01e1f6aec87768696725e648ed5b438941341eea8f8100beb681961f/ruff-0.17.0-py3-none-win32.whl", hash = "sha256:5e50aa5b84decd9fe5b0bb0e6f71c3b592f1767ed09faa4b7207d933961e35cd", size = 9904112, upload-time = "2026-10-09T19:47:19.75Z" }, - { url = "https://files.pythonhosted.org/packages/cc/77/1ee73df41dcc8d1cdb686ee4bc46ea29704ea175feb6b95c78420f631ab8/ruff-0.17.0-py3-none-win_amd64.whl", hash = "sha256:8ab76bcda86dfd28e13776cb5de3c7bcdcf1ae3d37ed761113d1a5a415dc134c", size = 10115585, upload-time = "2026-10-09T19:47:22.698Z" }, - { url = "https://files.pythonhosted.org/packages/fd/71/eb4f0ccc844aece56963e8578df9c95d4c00f580547d52329d4035d3af18/ruff-0.17.0-py3-none-win_arm64.whl", hash = "sha256:c154c73ff43f9854395e24cac507af13078962e53d2b511605058d22af1fdb88", size = 9872035, upload-time = "2026-10-09T19:47:26.306Z" }, +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e7/e6d5749983b020e6cb1055abec0a6f361a8e94f0184cdcf56d4760995f02/ruff-0.17.1.tar.gz", hash = "sha256:5bb796c5112e9fb9527f2ef2faa0a130d6312d19de92bcfaddd70dbb2d41020f", size = 5323349, upload-time = "2026-10-15T15:23:18.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/78/90c46ca6250709f7c37e397b60d06061adee8d0f7d13959ed6a8e49b14fd/ruff-0.17.1-py3-none-linux_armv6l.whl", hash = "sha256:1796c41eafec66f4635125dd1e074a7aa27b8d20c187d484360acf11d247fc4a", size = 10147528, upload-time = "2026-10-15T15:22:33.667Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d2/a2e28bc35b083ae51fa07dfba5280a667c8b01ecaee7802fc8849dcd5c46/ruff-0.17.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8eadb09a788010be29c895418bdfa513e4e1c65e64b94e5b19e4cd381f6cc7fb", size = 10225974, upload-time = "2026-10-15T15:22:36.328Z" }, + { url = "https://files.pythonhosted.org/packages/8e/71/7a9029a81c8206ee01a98f6cfeeb56e6c095af86857581a37045c46d7e94/ruff-0.17.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:29b454f18ca3eeaf37fbf140841dcf27d36109e29dee9aae8acecb68c26c6f74", size = 9270361, upload-time = "2026-10-15T15:22:38.651Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8c/e075e9c917cb0575297880c96768304e881240171c870cbd8d9b7b54bec2/ruff-0.17.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872e03dd231744d6c5ec5c8af6be52b71323fc6ee096e4c8b34d9f844ee3b978", size = 9938300, upload-time = "2026-10-15T15:22:41.284Z" }, + { url = "https://files.pythonhosted.org/packages/87/27/0b4bb926a49de2383f161272b4d863571a4c7ecdbf595a6bc58813a13367/ruff-0.17.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4c0d6cb5f8fbac77eec9af49f4fc0eaf856b05545e372b0694d24b4e76b1c640", size = 9948464, upload-time = "2026-10-15T15:22:43.681Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/518d57e0e02aecb8ea4acac6b57b51a53173d25fa734952bfdd7d531e88e/ruff-0.17.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78beced4a3c94dfedbee4b999b95b6a27b35820fdc0afbacc6d37a8da26c1cd1", size = 10638639, upload-time = "2026-10-15T15:22:46.081Z" }, + { url = "https://files.pythonhosted.org/packages/01/82/d89a4e498ade59e6cee52d67f8da569019372212b87468c1fdeae884601c/ruff-0.17.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7feefc63921202894f0e158b0e32f45a5950d70a7fb693ccb9264816c25b1c05", size = 11337238, upload-time = "2026-10-15T15:22:48.894Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7c/3b0fdef9352656317c977086fe1d802902406e4f2a0d8578da90c0e6d1c5/ruff-0.17.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684154861a819bf1f433669d2a0ce9c3e54677c65b8ebb866cc2a7ec86345785", size = 10896327, upload-time = "2026-10-15T15:22:51.45Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/5566290219708ce2928f3a29c4f843a1185b341ec6111c4631a33c5ceced/ruff-0.17.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6ef98e51de8ef38082f90653ceeb84944bd7673783eea13e1647c5b9b064189", size = 10350811, upload-time = "2026-10-15T15:22:54.401Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d2/ca8462dabb3c413990d49703ba306c30c862195a75448d080fa8fcce9775/ruff-0.17.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8f9aa5b5bd5fa1937803ea609c8f153b8de39bf3e0e149897ae34b0c5b52f467", size = 10569994, upload-time = "2026-10-15T15:22:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ad/dd1864c1f1db17c8cfb297ce70b352388e6977620f66f7182c52855407df/ruff-0.17.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:80e53b0e5faa3c5667f035daaa032893ae42b7cfe708c886d39ed131e54688ab", size = 10059672, upload-time = "2026-10-15T15:23:00.084Z" }, + { url = "https://files.pythonhosted.org/packages/65/45/3f56cf1d2168c2165cdd5ac6f9668aee66a4cfdd4caf2b2cc5ccb2b1d568/ruff-0.17.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:056c5d5fa0ac790051ae165d7035c7bf956d02de2db557f2e9b31d877a505fca", size = 9947803, upload-time = "2026-10-15T15:23:03.132Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4b/5457cb2be4c1d1ea5e919558131a0f197a4a454a0530d9e30820e8206e4c/ruff-0.17.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd8f5a55b10f371642eacd0e8665a42a8b97da15c9a7187c79e697b77b5e0ce4", size = 10351807, upload-time = "2026-10-15T15:23:05.628Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/db15b4f8fa24a1e36b99e1401f42ef6df16ee299a48670d5d262b771e7d7/ruff-0.17.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ce05e6f3973611d9550a600e04bb531061e368baba31610eb57c1509a8f21a0a", size = 10765716, upload-time = "2026-10-15T15:23:08.092Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e8/1e186578076aff7a80735f625005fde678412ab609cd323ed182eb7d6b87/ruff-0.17.1-py3-none-win32.whl", hash = "sha256:1de0030a8aa78c65c5fecb84b2e5c390ac684c0452875a02afc7c083d49a496a", size = 9895093, upload-time = "2026-10-15T15:23:10.582Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2e/da3d9589bae8e8b58f3a48bd5bc0b980232fb14a83bf02c3be7660f87c1b/ruff-0.17.1-py3-none-win_amd64.whl", hash = "sha256:5df76f16580fab58d0f6c98a3ac254a4abceb7b2489b8b678335869cff77b3d7", size = 10097955, upload-time = "2026-10-15T15:23:13.309Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/0f2a4dea06a5e12c1191ee3d96b1e1233797d0d5e18f97a2b909a95a9836/ruff-0.17.1-py3-none-win_arm64.whl", hash = "sha256:42400b9a6ff8515ff27796f07f92e0195be011c86e81e381606f58ea5502fef4", size = 9853542, upload-time = "2026-10-15T15:23:16.05Z" }, ] [[package]] @@ -2018,27 +2018,27 @@ wheels = [ [[package]] name = "ty" -version = "0.0.86" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/fa/9b35728cc3c04d64f434c656836fa3c5f6e384a7813e48e63245510d5c69/ty-0.0.86.tar.gz", hash = "sha256:6edcaf52e207d653873d5f495c1a8470075b232030714c981f07f0a1075bc343", size = 7911723, upload-time = "2026-10-09T22:20:57.83Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/27/dd422283667ce10d60af57edc3ccab078084282ee304a5c22b5ed79390c6/ty-0.0.86-py3-none-linux_armv6l.whl", hash = "sha256:305c238df5dedf97f1b65e52d2c446a41d11551291e37054858c00c5627bf15b", size = 14159390, upload-time = "2026-10-09T22:20:17.583Z" }, - { url = "https://files.pythonhosted.org/packages/57/e3/838ea547c177983976103d3a54b346afc22d0ff97943ab58c7b7aff1f753/ty-0.0.86-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27a8a7cf26a1cd17d93586eb20aadbcc535fa6ad4b535813ee8fc52560c31efd", size = 13684426, upload-time = "2026-10-09T22:20:19.939Z" }, - { url = "https://files.pythonhosted.org/packages/bc/62/dfde84ffab702545f866350675b16f8df6555d3c41123c25e7a509fe857e/ty-0.0.86-py3-none-macosx_11_0_arm64.whl", hash = "sha256:40e5fc602af41b0ea89dd1d19fe8da7e75322e6e0db95ea23340a858b409ac2b", size = 12845457, upload-time = "2026-10-09T22:20:21.979Z" }, - { url = "https://files.pythonhosted.org/packages/47/b2/7e0e7ffc8b3961f5f74893ba9b98433b922d3f1ae6621d1e3e3e20663e1d/ty-0.0.86-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4b525ce4feb60aa2dd6becae4c470daea889149ba9dc7699ffbcf41448f69a9", size = 13656310, upload-time = "2026-10-09T22:20:24.544Z" }, - { url = "https://files.pythonhosted.org/packages/e7/c8/977594e024d154f0e064b48a6f7e39a0e1e55a524349dba22146f01e02db/ty-0.0.86-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a000ddcc0b99bd1ab8c8277630711061a36ae83395780fdb935bad540ebe7103", size = 13887036, upload-time = "2026-10-09T22:20:26.844Z" }, - { url = "https://files.pythonhosted.org/packages/3a/75/67e8e7bf89a6be7535768380cb20569b460b89cb09d5c82ce27411782786/ty-0.0.86-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3848f3cab206e9798e5a9f7c4ce8a80297b0a88ba9aa8a091360a298e97424", size = 14805103, upload-time = "2026-10-09T22:20:29.274Z" }, - { url = "https://files.pythonhosted.org/packages/a1/14/a7f3b88cc76156feaf5d48601bd5da96a116f846ad0abc526df03a735bcc/ty-0.0.86-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:087bad6ae8fa7248a16c01f5646c121dccaacbaf320ff544eed4e8c5c1fd7555", size = 15203931, upload-time = "2026-10-09T22:20:31.736Z" }, - { url = "https://files.pythonhosted.org/packages/a7/23/42f6ad5d4bb7091d70a0e96d7d436d6bc9a490210a928727dde5d883ed26/ty-0.0.86-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe7f2721746c9ee64626c69da9245faedc0f375024bf41458e45eb7b28fe5f63", size = 15094434, upload-time = "2026-10-09T22:20:34.269Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b8/211781d3c8de5fa88437412b7a8c0d7a632596033cff51d5004b08e94e1c/ty-0.0.86-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5c18501c54874149d05e5199211146876afeaeee2af234042e56dccc9d5812a", size = 14436812, upload-time = "2026-10-09T22:20:36.795Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2a/518cc7ef2586384345be675d470e0087591810304b5d994d0a2194c7d68e/ty-0.0.86-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0a48967c8adba6b12035243f225a0585b0bc5f0bf940a4829876bc455ebf3d2f", size = 14671116, upload-time = "2026-10-09T22:20:39.277Z" }, - { url = "https://files.pythonhosted.org/packages/7a/18/a820553ad3206d0cdfad5866dd64939c7e3be73e891d88c73f7fd3acb5d6/ty-0.0.86-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:561a4eae98f6ff05395e8d7be8151fdb328eeaead1ac63dc721ca891623f7924", size = 13586361, upload-time = "2026-10-09T22:20:41.444Z" }, - { url = "https://files.pythonhosted.org/packages/22/58/f1700176e6ae4d0a3d25e447734c60ca464d6e76ed2e8a32920ebd41e523/ty-0.0.86-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccad053ce880a1f8c0850d9dd1f277f3ee559a98bded3f93472268376ceccc2d", size = 13900289, upload-time = "2026-10-09T22:20:43.955Z" }, - { url = "https://files.pythonhosted.org/packages/59/86/2f0e3c9a055f8606911a80311763a089784b66af22dd80fd3290e3b167c6/ty-0.0.86-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7c16d22857bad1da56b147bf53ef0ed2252d7677dc5e7ee51c757b2e52ab6351", size = 14235785, upload-time = "2026-10-09T22:20:46.401Z" }, - { url = "https://files.pythonhosted.org/packages/b4/1d/72fa2347b641143b0440296144a65f424fe047302cb13af059988f1de7a4/ty-0.0.86-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d863f8f190e96878ed1900a8a6d5093f37761c0c47409e2bdb3d9370e37471e1", size = 14559840, upload-time = "2026-10-09T22:20:48.753Z" }, - { url = "https://files.pythonhosted.org/packages/47/4a/a798be4aee4ea0bb427c22f157945d0ac41310f4d6e058b3758dfb859451/ty-0.0.86-py3-none-win32.whl", hash = "sha256:1ac3d8fe9efae04fbf1407583ed4118e0364a45b49a020d62b202846695a2ba1", size = 13133227, upload-time = "2026-10-09T22:20:51.034Z" }, - { url = "https://files.pythonhosted.org/packages/c7/a0/c9d585fa1c5bf4ed2727f576f1542bc13e268f036451a79d057c249fc26f/ty-0.0.86-py3-none-win_amd64.whl", hash = "sha256:6fe4116fb1a7ad3c4f7804d9241dc81187717fbd9f5fed5b50e2752a20e17a67", size = 13665391, upload-time = "2026-10-09T22:20:53.237Z" }, - { url = "https://files.pythonhosted.org/packages/2a/7e/d9811358bce1e54ac1b93bd44f4c45eba5e2e92672c5410248776c54319e/ty-0.0.86-py3-none-win_arm64.whl", hash = "sha256:0df36f494c5d07f68ebec3edb74fc719c4c959db7061c2018f1dff2dfe62150b", size = 13237981, upload-time = "2026-10-09T22:20:55.586Z" }, +version = "0.0.89" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/ea/dfb9bdbc168c50a568809466596751afa0c7da60da03b8c12906d1decde4/ty-0.0.89.tar.gz", hash = "sha256:bc1027b1ea95cca25c2a3a0b41433c809cf4b86a384d2b2434eba4bcd34548d4", size = 7978241, upload-time = "2026-10-16T10:35:05.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/84/e199d9a3dc3c94c804ddd9e94b63dbe543ae7f39329574963b6a6a95872f/ty-0.0.89-py3-none-linux_armv6l.whl", hash = "sha256:739b087e8873e177ffda99ff8a3600a4a7704d06523a8e88c4493021eee40b8a", size = 14254879, upload-time = "2026-10-16T10:34:24.813Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/5afe09bf5c83c4e3a9e5913386a460e240a7029f2ae5fc96df5b43563a91/ty-0.0.89-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:47ebc98480c8a301955e18a8bdd67ef07090071ad47349b3254f325d3d5ab4e7", size = 13790417, upload-time = "2026-10-16T10:34:27.672Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f9/88648f0ba9400dfd642c1e8dc0aeecb87945b121f3ff51f3902883c016d6/ty-0.0.89-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b0631291af5c850a15007c0ffb906d813966466e4ec12173c44977c0c5a2d51d", size = 12856900, upload-time = "2026-10-16T10:34:29.871Z" }, + { url = "https://files.pythonhosted.org/packages/6a/89/baae2b407101c4cb02f19216123900c147af8fd87cdb1ed7eba139472734/ty-0.0.89-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d768300ae16274ce656006e9199869ad68684b74400d963e1c82deddf3a22ad", size = 13653330, upload-time = "2026-10-16T10:34:32.296Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/293516c2e10a77134cfc40d22c3fc08edd8c5eadae9c75531ec8a9263348/ty-0.0.89-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dd4c57ba58bab8f6c816b2c254db52b58834b5dbf8268287382f7a2f9735c8f", size = 13986215, upload-time = "2026-10-16T10:34:34.506Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c3/da911c8fc22d2664692432570619576b25c7cd0a586401cab08987763094/ty-0.0.89-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1af774c24117d366b69142d4f35edee518617f8b926d13560d0334cbe5aa3f85", size = 14905008, upload-time = "2026-10-16T10:34:36.632Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c3/aed8d920b9bbf7b582e9fc0305acb2f2c748ede788e7dba9d6952e939c78/ty-0.0.89-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8147811b9b3bc2dc9338cd01c7893ac931fc3fae7aace169047ecbd627e6628", size = 15278217, upload-time = "2026-10-16T10:34:39.144Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/c6b4320f08fca44ffe279e5ddeb3eae12869c6341f596931accdefea1182/ty-0.0.89-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d5f178c91d50fb62461b5e579028b7d2e92872e1d8f02e96ace26464c2277a6", size = 15206289, upload-time = "2026-10-16T10:34:41.626Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/da22c714fd4f4dff642d60fb0d8bf8da1985d126f21cc89237f8bf707c92/ty-0.0.89-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a410f10b26cc3861757b626727db9d979deba7d2a3ab59f964fac8f0514fdc", size = 14510395, upload-time = "2026-10-16T10:34:44.063Z" }, + { url = "https://files.pythonhosted.org/packages/f0/32/5f9ce2377a77ad9ff36d8996edf86b1f069c1e8716e37c7d6d668402b110/ty-0.0.89-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a70ab1a402746d7837407dc169e461fdf90945bc1eef0d3c0f1be543490060bf", size = 14767366, upload-time = "2026-10-16T10:34:46.154Z" }, + { url = "https://files.pythonhosted.org/packages/63/72/2b28c71d6ebd97b5bb1378f1806598c5dd90278f7b99a431dd3879dddcfc/ty-0.0.89-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b55c28a49bd2676eac9b3540a1dcc4cc053e4671c65dcec9fdad3dfe55fb148c", size = 13670284, upload-time = "2026-10-16T10:34:48.225Z" }, + { url = "https://files.pythonhosted.org/packages/34/cc/68b8ebf98c9bc582fddc3d2b93f6e0f53719d1ddee1c5037dfa2e1cb8467/ty-0.0.89-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c9f22edeb8176f8ab189337e8cbd8f92b66744534a2886df9e252b52ade991c", size = 13997891, upload-time = "2026-10-16T10:34:50.556Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4d/a1d61e06092fd5726f48a8711bd32eac6af132308eb222a0027caba4f42d/ty-0.0.89-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0c186185cad57a1469b851096b51345de7b625e353c1f4acd338402daed22fa7", size = 14326247, upload-time = "2026-10-16T10:34:53.005Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d2/466d3762e330504a2c114dbbd0f0383229f9652dccd56277dde008bfe0da/ty-0.0.89-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5e38302ac218d2404758241093b5542ed0345d4f569096589f1810034244e998", size = 14661527, upload-time = "2026-10-16T10:34:55.555Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d5/dfd7ed5e4af472b20a11ef651da4bca55f9dc8be485cd152337f94caddde/ty-0.0.89-py3-none-win32.whl", hash = "sha256:0146637ed2321717280bfe4e4b20a18813b741da280e8039d7df2bf22b0251f1", size = 13224078, upload-time = "2026-10-16T10:34:57.735Z" }, + { url = "https://files.pythonhosted.org/packages/b4/14/1ee663c422e3a2d910c5cc27e7d84bd7709a3df0aca28a713843c6c3fa5d/ty-0.0.89-py3-none-win_amd64.whl", hash = "sha256:9fc56322a1aff29a19088d2025f3c42a6d25a7035668048bb908c9f8b66f480d", size = 13727433, upload-time = "2026-10-16T10:35:00.15Z" }, + { url = "https://files.pythonhosted.org/packages/09/93/af8f7250920823ca6d582bcf0abf803fb92c8b99415ba7f40f2304102c6f/ty-0.0.89-py3-none-win_arm64.whl", hash = "sha256:1d4967c8f7bfd67c9708115b41d7049197c5d20c76fb2e136fe78b0437ada05b", size = 13287101, upload-time = "2026-10-16T10:35:02.918Z" }, ] [[package]]