Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 1 addition & 4 deletions src/connectrpc/_client_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,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 @@ -31,9 +30,7 @@
from .protocol import ProtocolType

try:
from asyncio import (
timeout as asyncio_timeout, # pyright: ignore[reportAttributeAccessIssue]
)
from asyncio import timeout as asyncio_timeout # ty: ignore[unresolved-import]
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.

Using ImportError is a very old habit of mine for shims, but ever since version_info tuple I guess using that may be more idiomatic (typing-extensions uses it extensively), can consider rewriting to that to be properly type aware

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

switched in f3b7d18!

except ImportError:
from ._asyncio_timeout import timeout as asyncio_timeout

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