Metadata-Version: 2.4
Name: fastproto
Version: 0.1.1
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Requires-Dist: protobuf>=6,<8 ; extra == 'plugin'
Provides-Extra: plugin
License-File: LICENSE.md
Summary: FastProto is fast and efficient protobuf library for Python, built on the top of Rust.
Home-Page: https://github.com/pkozhem/fastproto
Author-email: pkozhem <pkozhem@gmail.com>
License: MIT
Requires-Python: >=3.14
Description-Content-Type: text/markdown

# FastProto

FastProto is a fast, ergonomic Protocol Buffers library for Python. Messages
are plain, readable `@dataclass` types — no generated getters/setters, no
`Message` reflection API to learn — while all encoding and decoding happens in
a compiled Rust core.

- **Idiomatic messages.** Generated code is a `@dataclass`, annotated with
  plain Python types (`str`, `int`, `list[...]`, `dict[...]`, `| None`).
  Autocomplete, type checkers, and `repr()` all just work.
- **Rust-powered wire codec.** Encoding and decoding are implemented in Rust
  via [PyO3](https://pyo3.rs), avoiding the overhead of pure-Python protobuf
  implementations.
- **Wire-compatible.** Bytes produced by FastProto are read correctly by
  Google's reference `protobuf` runtime, and vice versa.
- **Drop-in `protoc` plugin.** Reuses the standard `.proto` toolchain — run
  `protoc` with `--fastproto_out` to generate typed dataclasses.

## Installation

```bash
pip install fastproto
```

Generating code from `.proto` files also requires the `protoc` compiler and
the plugin's `protobuf` dependency:

```bash
pip install "fastproto[plugin]"
```

`protoc` itself isn't installed by pip — grab it from your package manager
(`apt install protobuf-compiler`, `brew install protobuf`, ...) or the
[official releases](https://github.com/protocolbuffers/protobuf/releases).

## Quick start

**1. Write a `.proto` file** (`user.proto`):

```protobuf
syntax = "proto3";
package example;

enum Role {
  ROLE_UNSPECIFIED = 0;
  ROLE_ADMIN = 1;
  ROLE_USER = 2;
}

message Address {
  string city = 1;
  string street = 2;
}

message User {
  int64 id = 1;
  string name = 2;
  optional string email = 3;
  Role role = 4;
  repeated string tags = 5;
  Address address = 6;
  map<string, int32> counters = 7;
}
```

**2. Generate the Python module** with `protoc`, using the `fastproto` plugin:

```bash
protoc --proto_path=. --fastproto_out=. user.proto
```

This produces `user_pb.py` — a plain, readable dataclass module:

```python
# @generated by fastproto. DO NOT EDIT.
# source: user.proto
from dataclasses import dataclass, field
from enum import IntEnum

from fastproto import Message, Scalar, message


class Role(IntEnum):
    ROLE_UNSPECIFIED = 0
    ROLE_ADMIN = 1
    ROLE_USER = 2


@message(_ADDRESS_DESCRIPTOR)
@dataclass(slots=True)
class Address(Message):
    city: Scalar.String = ""
    street: Scalar.String = ""


@message(_USER_DESCRIPTOR)
@dataclass(slots=True)
class User(Message):
    id: Scalar.Int64 = 0
    name: Scalar.String = ""
    email: Scalar.String | None = None
    role: Role = Role(0)
    tags: list[Scalar.String] = field(default_factory=list)
    address: "Address | None" = None
    counters: dict[Scalar.String, Scalar.Int32] = field(default_factory=dict)
```

**3. Use it** like any other Python dataclass:

```python
from user_pb import Address, Role, User

user = User(
    id=42,
    name="Ada",
    email="ada@example.com",
    role=Role.ROLE_ADMIN,
    tags=["vip", "beta"],
    address=Address(city="London", street="Baker St"),
    counters={"logins": 7},
)

# Serialize to protobuf wire bytes.
data = user.to_bytes()

# Deserialize back into a `User` instance.
same_user = User.from_bytes(data)
assert same_user == user
```

That's it — no `SerializeToString()` / `ParseFromString()` ceremony, no
`ListFields()` reflection, just `to_bytes()` / `from_bytes()` on a dataclass
you can construct, compare, and pretty-print directly.

## Field type mapping

Every proto scalar type has a corresponding alias under `fastproto.Scalar`.
Each alias is just the underlying Python type (`int`, `str`, ...) tagged with
an `Annotated[...]` marker, so it type-checks exactly as you'd expect while
still documenting the precise wire type:

| proto type | Python annotation    |
|------------|-----------------------|
| `double`   | `Scalar.Double`        |
| `float`    | `Scalar.Float`         |
| `int32`    | `Scalar.Int32`         |
| `int64`    | `Scalar.Int64`         |
| `uint32`   | `Scalar.UInt32`        |
| `uint64`   | `Scalar.UInt64`        |
| `sint32`   | `Scalar.SInt32`        |
| `sint64`   | `Scalar.SInt64`        |
| `fixed32`  | `Scalar.Fixed32`       |
| `fixed64`  | `Scalar.Fixed64`       |
| `sfixed32` | `Scalar.SFixed32`      |
| `sfixed64` | `Scalar.SFixed64`      |
| `bool`     | `Scalar.Bool`          |
| `string`   | `Scalar.String`        |
| `bytes`    | `Scalar.Bytes`         |

Composite fields map the way you'd hope:

- `repeated T` → `list[T]`
- `map<K, V>` → `dict[K, V]`
- `optional T` / `oneof` members → `T | None`
- nested/enum messages → the generated class or `IntEnum`, referenced by name

## Enums, nested messages, and `oneof`

Enums become `IntEnum` subclasses; message fields hold real instances of the
generated dataclass (or `None` when unset); `oneof` groups are represented as
plain optional fields, and FastProto enforces "at most one set" at encode
time:

```python
from user_pb import User

# Setting more than one oneof member raises when you try to serialize it.
User(phone="123", telegram="abc").to_bytes()  # raises ValueError: ... oneof ...
```

## Presence semantics

FastProto follows proto3 field presence rules:

- Plain scalar fields (`string`, `int32`, ...) use their zero value as the
  default and are *not* nullable — they always round-trip to a concrete value.
- `optional` scalar fields, message fields, and `oneof` members are nullable
  (`T | None`) and track explicit presence, matching proto3 semantics exactly
  (an explicitly-set empty string is distinguishable from an unset field).

```python
from user_pb import User

empty = User()
assert empty.to_bytes() == b""      # all-default messages encode to zero bytes
assert User.from_bytes(b"") == empty
assert empty.email is None           # optional, unset
```

## How linking works

Generated dataclasses reference sibling messages and enums by name (as
forward references), since a message can reference a type defined later in
the same file, or itself (recursively). FastProto resolves these references
lazily, on first `to_bytes()` / `from_bytes()` call, by looking them up in the
generated module's namespace — you never need to call anything yourself.

## Development

The project is a mixed Rust/Python codebase built with
[maturin](https://www.maturin.rs/), managed with [uv](https://docs.astral.sh/uv/).

```bash
git clone https://github.com/pkozhem/fastproto
cd fastproto
uv sync                        # installs dev dependencies and builds the extension
uv run maturin develop         # rebuild the Rust extension in-place after changes
uv run pytest                  # run the test suite
uv run ruff check .            # lint
uv run ty check                # type-check
```

Test fixtures under `tests/generated/` are themselves `@generated` output,
committed so the plugin's golden tests can diff against them. After editing a
`.proto` file under `tests/protos/` or the plugin itself, regenerate them
with:

```bash
uv run python scripts/regen.py
```

## License

MIT

