Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ uv run poe test-conformance
We use:

- **ruff** for linting and formatting
- **pyright** for type checking
- **ty** for type checking
- **pytest** for testing

The project follows strict type checking and formatting standards.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ service YourService {

## Development

We use `ruff` for linting and formatting, `pyright` for type checking, and `tombi` for TOML linting and formatting.
We use `ruff` for linting and formatting, `ty` for type checking, and `tombi` for TOML linting and formatting.

We rely on the conformance test suit (in
[./conformance](./conformance)) to verify behavior.
Expand Down
Empty file removed conformance/test/__init__.py
Empty file.
2 changes: 1 addition & 1 deletion conformance/test/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def maybe_patch_args_with_debug(args: list[str]) -> list[str]:
# This invokes internal methods from bundles provided by the IDE
# and may not always work.
try:
from pydevd import ( # pyright:ignore[reportMissingImports] - provided by IDE # noqa: PLC0415
from pydevd import ( # ty: ignore[unresolved-import] - provided by IDE # noqa: PLC0415
_pydev_bundle,
)

Expand Down
3 changes: 1 addition & 2 deletions conformance/test/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
from typing import TYPE_CHECKING

import pytest

from ._util import VERSION_CONFORMANCE, coverage_env, maybe_patch_args_with_debug
from _util import VERSION_CONFORMANCE, coverage_env, maybe_patch_args_with_debug
Copy link
Copy Markdown
Collaborator

@anuraaga anuraaga May 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this doesn't hurt though adding conformance to the search paths would also make sense, I guess any pytest root should be added to it in principle.

Reminds me of the days when conformance was a separate pyproject, with renovate working properly we may consider eventually going back to that to reduce pythonpath worries


if TYPE_CHECKING:
from coverage import Coverage
Expand Down
3 changes: 1 addition & 2 deletions conformance/test/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
from typing import TYPE_CHECKING

import pytest

from ._util import VERSION_CONFORMANCE, coverage_env, maybe_patch_args_with_debug
from _util import VERSION_CONFORMANCE, coverage_env, maybe_patch_args_with_debug

if TYPE_CHECKING:
from coverage import Coverage
Expand Down
2 changes: 1 addition & 1 deletion example/example/eliza_service_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,4 @@ def health_check():

eliza_app = ElizaServiceWSGIApplication(DemoElizaServiceSync())

app.wsgi_app = DispatcherMiddleware(app.wsgi_app, {eliza_app.path: eliza_app})
app.wsgi_app = DispatcherMiddleware(app.wsgi_app, {eliza_app.path: eliza_app}) # ty: ignore[invalid-assignment]
2 changes: 1 addition & 1 deletion poe_tasks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ cmd = "tombi lint"

[tasks.lint-types]
help = "Apply type checking to Python files"
cmd = "pyright"
cmd = "ty check ."

[tasks.test]
help = "Run unit tests"
Expand Down
41 changes: 16 additions & 25 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ dev = [
"gunicorn==25.3.0",
"hypercorn==0.18.0",
"poethepoet==0.45.0",
"pyright[nodejs]==1.1.409",
"pyvoy==0.3.0",
"ruff==0.15.12",
"tombi==0.10.2",
"ty==0.0.34",
"types-grpcio==1.0.0.20260408",
"types-protobuf==7.34.1.20260503",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks - didn't realize there was this hatch for getting protobuf working with it

"typing_extensions==4.15.0",
"uvicorn==0.46.0",
"zstandard==0.25.0",
Expand Down Expand Up @@ -79,30 +81,6 @@ docs = ["mkdocstrings-python==2.0.3", "zensical==0.0.39"]
requires = ["uv_build>=0.11.0,<0.12.0"]
build-backend = "uv_build"

[tool.pyright]
exclude = [
# Defaults.
"**/node_modules",
"**/__pycache__",
"**/.*",
".venv",

# Recent versions of typeshed fail on 5.x gencode.
# https://github.com/python/typeshed/pull/15677
"**/*_pb.py",

# Recent versions of typeshed have improved typing of functions that then expose
# type errors for accessing
"**/*_pb2.py",

# GRPC python files don't typecheck on their own.
# See https://github.com/grpc/grpc/issues/39555
"**/*_pb2_grpc.py",

# TODO: Work out the import issues to allow it to work.
"conformance/**",
]

[tool.pytest]
# Turn all warnings into errors
filterwarnings = ["error"]
Expand Down Expand Up @@ -250,6 +228,19 @@ typing-extensions = false
required-imports = ["from __future__ import annotations"]
split-on-trailing-comma = false

[tool.ty.src]
exclude = [
# gRPC generated code references `grpc.experimental`, which is not exposed by the
# `grpc` package's public API. See https://github.com/grpc/grpc/issues/39555.
"**/*_pb2_grpc.py",
]

[tool.ty.environment]
# Conformance test files import generated code as `from gen.connectrpc...` and
# sibling modules like `_util`/`_cov_embed`. Pytest and the launched server/client
# subprocesses put `conformance/test/` on sys.path; teach ty to do the same.
extra-paths = ["conformance/test"]

[tool.uv]
constraint-dependencies = [
"coverage==7.13.2",
Expand Down
4 changes: 2 additions & 2 deletions src/connectrpc/_asyncio_timeout.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ async def __aexit__(
raise TimeoutError from exc_val
if exc_val is not None:
self._insert_timeout_error(exc_val)
if _HAX_EXCEPTION_GROUP and isinstance(exc_val, ExceptionGroup): # pyright: ignore[reportUndefinedVariable] # noqa: F821
for exc in exc_val.exceptions: # pyright: ignore[reportAttributeAccessIssue]
if _HAX_EXCEPTION_GROUP and isinstance(exc_val, ExceptionGroup): # noqa: F821
for exc in exc_val.exceptions:
self._insert_timeout_error(exc)
elif self._state is _State.ENTERED:
self._state = _State.EXITED
Expand Down
11 changes: 4 additions & 7 deletions src/connectrpc/_client_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import asyncio
import functools
import sys
from asyncio import CancelledError, sleep, wait_for
from typing import TYPE_CHECKING, Any, Protocol, TypeVar
from urllib.parse import urlencode
Expand All @@ -11,7 +12,6 @@
from pyqwest import Headers as HTTPHeaders

from . import _client_shared
from ._asyncio_timeout import timeout as asyncio_timeout
from ._codec import proto_binary_codec
from ._compression import IdentityCompression, _gzip, resolve_compressions
from ._interceptor_async import (
Expand All @@ -30,15 +30,12 @@
from .errors import ConnectError
from .protocol import ProtocolType

try:
from asyncio import (
timeout as asyncio_timeout, # pyright: ignore[reportAttributeAccessIssue]
)
except ImportError:
if sys.version_info >= (3, 11):
from asyncio import timeout as asyncio_timeout
else:
from ._asyncio_timeout import timeout as asyncio_timeout

if TYPE_CHECKING:
import sys
from collections.abc import AsyncIterator, Iterable, Mapping
from types import TracebackType

Expand Down
17 changes: 12 additions & 5 deletions src/connectrpc/_codec.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def decode(self, data: bytes | bytearray, message: U) -> U:


class ProtoBinaryCodec(Codec[Message, V]):
"""Codec for Protocol bytes | bytearrays binary format."""
"""Codec for the Protocol Buffers binary format."""

def name(self) -> str:
return "proto"
Expand All @@ -42,12 +42,16 @@ def encode(self, message: Message) -> bytes:
return message.SerializeToString()

def decode(self, data: bytes | bytearray, message: V) -> V:
message.ParseFromString(data) # pyright: ignore[reportArgumentType] - type is incorrect
# ParseFromString accepts the buffer protocol at runtime, but typeshed
# declares only `bytes`. Skipping the conversion avoids a copy on the
# streaming decode path (see _envelope.py). Tracked upstream in
# https://github.com/python/typeshed/issues/9006.
message.ParseFromString(data) # ty: ignore[invalid-argument-type]
return message


class ProtoJSONCodec(Codec[Message, V]):
"""Codec for Protocol bytes | bytearrays JSON format."""
"""Codec for the Protocol Buffers JSON format."""

def __init__(self, name: str = "json") -> None:
self._name = name
Expand All @@ -59,13 +63,16 @@ def encode(self, message: Message) -> bytes:
return MessageToJson(message).encode()

def decode(self, data: bytes | bytearray, message: V) -> V:
MessageFromJson(data, message) # pyright: ignore[reportArgumentType] - type is incorrect
# google.protobuf.json_format.Parse accepts the buffer protocol at
# runtime, but typeshed declares only `bytes | str`. See
# ProtoBinaryCodec.decode for the upstream tracking issue.
MessageFromJson(data, message) # ty: ignore[invalid-argument-type]
return message


_proto_binary_codec = ProtoBinaryCodec()
_proto_json_codec = ProtoJSONCodec()
_default_codecs = [_proto_binary_codec, _proto_json_codec]
_default_codecs: list[Codec] = [_proto_binary_codec, _proto_json_codec]


def get_default_codecs() -> list[Codec]:
Expand Down
5 changes: 5 additions & 0 deletions src/connectrpc/_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
Sequence,
ValuesView,
)
from typing import cast


class Headers(MutableMapping[str, str]):
Expand All @@ -28,6 +29,10 @@ def __init__(
) -> None:
self._extra = None
if isinstance(items, Mapping):
# ty does not preserve type parameters when narrowing a union via
# `isinstance(x, Mapping)`, so we re-assert `Mapping[str, str]`.
# See https://github.com/astral-sh/ty/issues/456.
items = cast("Mapping[str, str]", items)
self._store = {k.lower(): v for k, v in items.items()}
else:
self._store = {}
Expand Down
2 changes: 1 addition & 1 deletion src/connectrpc/_protocol_grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def end(self, user_trailers: Headers, error: ConnectWireError | None) -> Headers


class GRPCWebEnvelopeWriter(GRPCEnvelopeWriter):
def end(self, user_trailers: Headers, error: ConnectWireError | None) -> bytes: # pyright: ignore[reportIncompatibleMethodOverride]
def end(self, user_trailers: Headers, error: ConnectWireError | None) -> bytes: # ty: ignore[invalid-method-override]
trailers = super().end(user_trailers, error)
data = "".join(f"{k}: {v}\r\n" for k, v in trailers.allitems()).encode()
if self._compression:
Expand Down
Loading
Loading