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

Generate server option #68

Merged
merged 3 commits into from
Apr 20, 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
46 changes: 41 additions & 5 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ You can also use it from `grpcio-tools`:
pip install grpcio-tools
```


## Install `betterproto2_compiler`

It is possible to install `betterproto2_compiler` using pip:
Expand All @@ -19,10 +18,6 @@ It is possible to install `betterproto2_compiler` using pip:
pip install betterproto2_compiler
```

!!! warning
The compiler needs Python 3.10, 3.11 or 3.12. Don't worry! The generated code will be compatible with all Python versions from Python 3.8 to Python 3.13.


## Compile a proto file

Create the following `example.proto` file.
Expand All @@ -35,6 +30,10 @@ package helloworld;
message HelloWorld {
string message = 1;
}

service HelloService {
rpc SayHello (HelloWorld) returns (HelloWorld);
}
```

You should now be able to compile it using:
Expand All @@ -50,3 +49,40 @@ If you installed `protoc` with `grpc-tools`, the command will be:
mkdir lib
python -m grpc.tools.protoc -I . --python_betterproto2_out=lib example.proto
```

### Service compilation

#### Clients

By default, for each service, betterproto will generate a synchronous client. Both synchronous and asynchronous clients
are supported.

- Synchronous clients rely on the `grpcio` package. Make sure to enable the `grpcio` extra package when installing
betterproto2 to use them.
- Asynchronous clients rely on the `grpclib` package. Make sure to enable the `grpclib` extra package when installing
betterproto2 to use them.

To choose which clients to generate, use the `client_generation` option of betterproto. It supports the following
values:

- `none`: Clients are not generated.
- `sync`: Only synchronous clients are generated.
- `async`: Only asynchronous clients are generated.
- `sync_async`: Both synchronous and asynchronous clients are generated.
Asynchronous clients are generated with the Async suffix.
- `async_sync`: Both synchronous and asynchronous clients are generated.
Synchronous clients are generated with the Sync suffix.
- `sync_async_no_default`: Both synchronous and asynchronous clients are generated.
Synchronous clients are generated with the Sync suffix, and asynchronous clients are generated with the Async
suffix.

For example, `protoc -I . --python_betterproto2_out=lib example.proto --python_betterproto2_opt=client_generation=async`
will only generate asynchronous clients.

#### Servers

By default, betterproto will not generate server base classes. To enable them, set the `server_generation` option to
`async` with `--python_betterproto2_opt=server_generation=async`.

These base classes will be asynchronous and rely on `grpclib`. To use them, make sure to install `betterproto2` with the
`grpclib` extra package.
15 changes: 10 additions & 5 deletions 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 ClientGeneration, Settings
from betterproto2_compiler.settings import ClientGeneration, ServerGeneration, Settings

from .compiler import outputfile_compiler
from .models import (
Expand Down Expand Up @@ -62,22 +62,27 @@ def _traverse(
def get_settings(plugin_options: list[str]) -> Settings:
# Synchronous clients are suitable for most users
client_generation = ClientGeneration.SYNC
server_generation = ServerGeneration.NONE

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}")

if opt.startswith("server_generation="):
name = opt.split("=")[1]
try:
server_generation = ServerGeneration(name)
except ValueError:
raise ValueError(f"Invalid server_generation option: {name}")

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


Expand Down
12 changes: 9 additions & 3 deletions src/betterproto2_compiler/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@ class ClientGeneration(StrEnum):
SYNC_ASYNC = "sync_async"
"""Both synchronous and asynchronous clients are generated.

The asynchronous client is generated with the Async suffix."""
Asynchronous clients are 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."""
Synchronous clients are 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
Synchronous clients are generated with the Sync suffix, and asynchronous clients are generated with the Async
suffix."""

@property
Expand Down Expand Up @@ -60,8 +60,14 @@ def is_async_prefixed(self) -> bool:
return self in {ClientGeneration.SYNC_ASYNC, ClientGeneration.SYNC_ASYNC_NO_DEFAULT}


class ServerGeneration(StrEnum):
NONE = "none"
ASYNC = "async"


@dataclass
class Settings:
pydantic_dataclasses: bool

client_generation: ClientGeneration
server_generation: ServerGeneration
2 changes: 2 additions & 0 deletions src/betterproto2_compiler/templates/template.py.j2
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ default_message_pool.register_message("{{ output_file.package }}", "{{ message.p
{{ i }}
{% endfor %}

{% if output_file.settings.server_generation == "async" %}
{% for _, service in output_file.services|dictsort(by="key") %}
class {{ service.py_name }}Base(ServiceBase):
{% if service.comment %}
Expand Down Expand Up @@ -170,3 +171,4 @@ class {{ service.py_name }}Base(ServiceBase):
}

{% endfor %}
{% endif %}
7 changes: 1 addition & 6 deletions tests/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,7 @@ def get_directories(path):
yield from directories


async def protoc(
path: str | Path,
output_dir: str | Path,
reference: bool = False,
pydantic_dataclasses: bool = False,
):
async def protoc(path: str | Path, output_dir: str | Path, reference: bool = False, pydantic_dataclasses: bool = False):
path: Path = Path(path).resolve()
output_dir: Path = Path(output_dir).resolve()
python_out_option: str = "python_betterproto2_out" if not reference else "python_out"
Expand Down
Loading