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 pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ lint = { workspace = true }

[tool.ruff]
lint.select = ["F", "E", "W", "I001"]
exclude = ["*/generated/*", "*/snapshots/*"]
exclude = ["*/generated/*", "*/snapshots/*", "*/generated_special_chars/*"]

# Should be kept in sync with mypy.ini in the project root.
# The VSCode mypy extension can only read /mypy.ini.
Expand Down
30 changes: 21 additions & 9 deletions src/replit_river/codegen/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,13 @@ def extract_props(tpe: RiverType) -> list[dict[str, RiverType]]:
if name == "$kind":
safe_name = "kind"
else:
safe_name = name
# For TypedDict encoder, use normalized name to access
# the TypedDict field but the output dictionary key should
# use the original name
if base_model == "TypedDict":
safe_name = normalize_special_chars(name)
else:
safe_name = name
if prop.type == "object" and not prop.patternProperties:
encoder_name = TypeName(
f"encode_{render_literal_type(type_name)}"
Expand Down Expand Up @@ -675,14 +681,20 @@ def extract_props(tpe: RiverType) -> list[dict[str, RiverType]]:
effective_name = name
extras = []
if name != specialized_name:
if base_model != "BaseModel":
# TODO: alias support for TypedDict
raise ValueError(
f"Field {name} is not a valid Python identifier, but it is in the schema" # noqa: E501
)
# Pydantic doesn't allow leading underscores in field names
effective_name = specialized_name.lstrip("_")
extras.append(f"alias={repr(name)}")
if base_model == "BaseModel":
# Pydantic doesn't allow leading underscores in field names
effective_name = specialized_name
extras.append(f"alias={repr(name)}")
elif base_model == "TypedDict":
# For TypedDict, we use the normalized name directly
# TypedDict doesn't support aliases, so we normalize
# the field name
effective_name = specialized_name
else:
# For RiverError (which extends BaseModel), use alias
# like BaseModel
effective_name = specialized_name
extras.append(f"alias={repr(name)}")

effective_field_names[effective_name].append(name)

Expand Down
2 changes: 1 addition & 1 deletion src/replit_river/codegen/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def work(
def normalize_special_chars(value: str) -> str:
for char in SPECIAL_CHARS:
value = value.replace(char, "_")
return value
return value.lstrip("_")


def render_type_expr(value: TypeExpression) -> str:
Expand Down
13 changes: 13 additions & 0 deletions tests/v1/codegen/rpc/generated_special_chars/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Code generated by river.codegen. DO NOT EDIT.
from pydantic import BaseModel
from typing import Literal

import replit_river as river


from .test_service import Test_ServiceService


class SpecialCharsClient:
def __init__(self, client: river.Client[Literal[None]]):
self.test_service = Test_ServiceService(client)
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Code generated by river.codegen. DO NOT EDIT.
from collections.abc import AsyncIterable, AsyncIterator
from typing import Any
import datetime

from pydantic import TypeAdapter

from replit_river.error_schema import RiverError, RiverErrorTypeAdapter
import replit_river as river


from .rpc_method import Rpc_MethodInput, encode_Rpc_MethodInput

boolTypeAdapter: TypeAdapter[bool] = TypeAdapter(bool)


class Test_ServiceService:
def __init__(self, client: river.Client[Any]):
self.client = client

async def rpc_method(
self,
input: Rpc_MethodInput,
timeout: datetime.timedelta,
) -> bool:
return await self.client.send_rpc(
"test_service",
"rpc_method",
input,
encode_Rpc_MethodInput,
lambda x: boolTypeAdapter.validate_python(
x # type: ignore[arg-type]
),
lambda x: RiverErrorTypeAdapter.validate_python(
x # type: ignore[arg-type]
),
timeout,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Code generated by river.codegen. DO NOT EDIT.
from collections.abc import AsyncIterable, AsyncIterator
import datetime
from typing import (
Any,
Literal,
Mapping,
NotRequired,
TypedDict,
)
from typing_extensions import Annotated

from pydantic import BaseModel, Field, TypeAdapter, WrapValidator
from replit_river.error_schema import RiverError
from replit_river.client import (
RiverUnknownError,
translate_unknown_error,
RiverUnknownValue,
translate_unknown_value,
)

import replit_river as river


def encode_Rpc_MethodInput(
x: "Rpc_MethodInput",
) -> Any:
return {
k: v
for (k, v) in (
{
"data field6": x.get("data_field6"),
"data-field1": x.get("data_field1"),
"data.field3": x.get("data_field3"),
"data/field4": x.get("data_field4"),
"data:field2": x.get("data_field2"),
"data@field5": x.get("data_field5"),
}
).items()
if v is not None
}


class Rpc_MethodInput(TypedDict):
data_field6: NotRequired[str | None]
data_field1: str
data_field3: NotRequired[bool | None]
data_field4: NotRequired[str | None]
data_field2: float
data_field5: NotRequired[int | None]
29 changes: 29 additions & 0 deletions tests/v1/codegen/rpc/input-collision-schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"services": {
"test_service": {
"procedures": {
"rpc_method": {
"input": {
"type": "object",
"properties": {
"data-3": {
"type": "string"
},
"data:3": {
"type": "number"
}
},
"required": ["data-3", "data:3"]
},
"output": {
"type": "boolean"
},
"errors": {
"not": {}
},
"type": "rpc"
}
}
}
}
}
41 changes: 41 additions & 0 deletions tests/v1/codegen/rpc/input-special-chars-schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"services": {
"test_service": {
"procedures": {
"rpc_method": {
"input": {
"type": "object",
"properties": {
"data-field1": {
"type": "string"
},
"data:field2": {
"type": "number"
},
"data.field3": {
"type": "boolean"
},
"data/field4": {
"type": "string"
},
"data@field5": {
"type": "integer"
},
"data field6": {
"type": "string"
}
},
"required": ["data-field1", "data:field2"]
},
"output": {
"type": "boolean"
},
"errors": {
"not": {}
},
"type": "rpc"
}
}
}
}
}
Loading
Loading