Skip to content
This repository was archived by the owner on Jun 9, 2025. It is now read-only.

Add synchronous stubs #14

Merged
merged 15 commits into from
Mar 29, 2025
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
35 changes: 27 additions & 8 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ packages = [

[tool.poetry.dependencies]
python = "^3.10"
betterproto2 = "^0.3.1"
betterproto2 = { version = "^0.3.1", extras = ["grpc-async"] }
# betterproto2 = { git="https://github.com/betterproto/python-betterproto2" }
# The Ruff version is pinned. To update it, also update it in .pre-commit-config.yaml
ruff = "~0.9.3"
grpclib = "^0.4.1"
jinja2 = ">=3.0.3"
typing-extensions = "^4.7.1"
strenum = [
{version = "^0.4.15", python = "=3.10"},
]

[tool.poetry.group.dev.dependencies]
pre-commit = "^2.17.0"
Expand Down
18 changes: 17 additions & 1 deletion src/betterproto2_compiler/plugin/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
CodeGeneratorResponseFeature,
CodeGeneratorResponseFile,
)
from betterproto2_compiler.settings import Settings
from betterproto2_compiler.settings import ClientGeneration, Settings

from .compiler import outputfile_compiler
from .models import (
Expand Down Expand Up @@ -60,8 +60,24 @@ def _traverse(


def get_settings(plugin_options: list[str]) -> Settings:
# Synchronous clients are suitable for most users
client_generation = ClientGeneration.SYNC

for opt in plugin_options:
if opt.startswith("client_generation="):
name = opt.split("=")[1]

# print(ClientGeneration.__members__, file=sys.stderr)
# print([member.value for member in ClientGeneration])

try:
client_generation = ClientGeneration(name)
except ValueError:
raise ValueError(f"Invalid client_generation option: {name}")

return Settings(
pydantic_dataclasses="pydantic_dataclasses" in plugin_options,
client_generation=client_generation,
)


Expand Down
61 changes: 61 additions & 0 deletions src/betterproto2_compiler/settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,67 @@
import sys
from dataclasses import dataclass

if sys.version_info >= (3, 11):
from enum import StrEnum
else:
from strenum import StrEnum


class ClientGeneration(StrEnum):
NONE = "none"
"""Clients are not generated."""

SYNC = "sync"
"""Only synchronous clients are generated."""

ASYNC = "async"
"""Only asynchronous clients are generated."""

SYNC_ASYNC = "sync_async"
"""Both synchronous and asynchronous clients are generated.

The asynchronous client is generated with the Async suffix."""

ASYNC_SYNC = "async_sync"
"""Both synchronous and asynchronous clients are generated.

The synchronous client is generated with the Sync suffix."""

SYNC_ASYNC_NO_DEFAULT = "sync_async_no_default"
"""Both synchronous and asynchronous clients are generated.

The synchronous client is generated with the Sync suffix, and the asynchronous client is generated with the Async
suffix."""

@property
def is_sync_generated(self) -> bool:
return self in {
ClientGeneration.SYNC,
ClientGeneration.SYNC_ASYNC,
ClientGeneration.ASYNC_SYNC,
ClientGeneration.SYNC_ASYNC_NO_DEFAULT,
}

@property
def is_async_generated(self) -> bool:
return self in {
ClientGeneration.ASYNC,
ClientGeneration.SYNC_ASYNC,
ClientGeneration.ASYNC_SYNC,
ClientGeneration.SYNC_ASYNC_NO_DEFAULT,
}

@property
def is_sync_prefixed(self) -> bool:
return self in {ClientGeneration.ASYNC_SYNC, ClientGeneration.SYNC_ASYNC_NO_DEFAULT}

@property
def is_async_prefixed(self) -> bool:
return self in {ClientGeneration.SYNC_ASYNC, ClientGeneration.SYNC_ASYNC_NO_DEFAULT}


@dataclass
class Settings:
pydantic_dataclasses: bool

client_generation: ClientGeneration
5 changes: 2 additions & 3 deletions src/betterproto2_compiler/templates/header.py.j2
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ __all__ = (
import builtins
import datetime
import warnings
from collections.abc import AsyncIterable, AsyncIterator, Iterable
from collections.abc import AsyncIterable, AsyncIterator, Iterable, Iterator
import typing
from typing import TYPE_CHECKING

Expand All @@ -33,10 +33,9 @@ from dataclasses import dataclass
{% endif %}

import betterproto2
{% if output_file.services %}
from betterproto2.grpc.grpclib_server import ServiceBase
import grpc
import grpclib
{% endif %}

{# Import the message pool of the generated code. #}
{% if output_file.package %}
Expand Down
32 changes: 32 additions & 0 deletions src/betterproto2_compiler/templates/service_stub.py.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
class {% block class_name %}{% endblock %}({% block inherit_from %}{% endblock %}):
{% block service_docstring scoped %}
{% if service.comment %}
"""
{{ service.comment | indent(4) }}
"""
{% elif not service.methods %}
pass
{% endif %}
{% endblock %}

{% block class_content %}{% endblock %}

{% for method in service.methods %}
{% block method_definition scoped required %}{% endblock %}
{% block method_docstring scoped %}
{% if method.comment %}
"""
{{ method.comment | indent(8) }}
"""
{% endif %}
{% endblock %}

{% block deprecation_warning scoped %}
{% if method.deprecated %}
warnings.warn("{{ service.py_name }}.{{ method.py_name }} is deprecated", DeprecationWarning)
{% endif %}
{% endblock %}

{% block method_body scoped required %}{% endblock %}

{% endfor %}
86 changes: 86 additions & 0 deletions src/betterproto2_compiler/templates/service_stub_async.py.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{% extends "service_stub.py.j2" %}

{# Class definition #}
{% block class_name %}{{ service.py_name }}{% if output_file.settings.client_generation.is_async_prefixed %}Async{% endif %}Stub{% endblock %}
{% block inherit_from %}betterproto2.ServiceStub{% endblock %}

{# Methods definition #}
{% block method_definition %}
async def {{ method.py_name }}(self
{%- if not method.client_streaming -%}
, message:
{%- if method.is_input_msg_empty -%}
"{{ method.py_input_message_type }} | None" = None
{%- else -%}
"{{ method.py_input_message_type }}"
{%- endif -%}
{%- else -%}
{# Client streaming: need a request iterator instead #}
, messages: "AsyncIterable[{{ method.py_input_message_type }}] | Iterable[{{ method.py_input_message_type }}]"
{%- endif -%}
,
*
, timeout: "float | None" = None
, deadline: "Deadline | None" = None
, metadata: "MetadataLike | None" = None
) -> "{% if method.server_streaming %}AsyncIterator[{{ method.py_output_message_type }}]{% else %}{{ method.py_output_message_type }}{% endif %}":
{% endblock %}

{% block method_body %}
{% if method.server_streaming %}
{% if method.client_streaming %}
async for response in self._stream_stream(
"{{ method.route }}",
messages,
{{ method.py_input_message_type }},
{{ method.py_output_message_type }},
timeout=timeout,
deadline=deadline,
metadata=metadata,
):
yield response
{% else %}
{% if method.is_input_msg_empty %}
if message is None:
message = {{ method.py_input_message_type }}()

{% endif %}
async for response in self._unary_stream(
"{{ method.route }}",
message,
{{ method.py_output_message_type }},
timeout=timeout,
deadline=deadline,
metadata=metadata,
):
yield response

{% endif %}
{% else %}
{% if method.client_streaming %}
return await self._stream_unary(
"{{ method.route }}",
messages,
{{ method.py_input_message_type }},
{{ method.py_output_message_type }},
timeout=timeout,
deadline=deadline,
metadata=metadata,
)
{% else %}
{% if method.is_input_msg_empty %}
if message is None:
message = {{ method.py_input_message_type }}()

{% endif %}
return await self._unary_unary(
"{{ method.route }}",
message,
{{ method.py_output_message_type }},
timeout=timeout,
deadline=deadline,
metadata=metadata,
)
{% endif %}
{% endif %}
{% endblock %}
Loading
Loading