Skip to content

Commit d3bf487

Browse files
authored
encode special fields (#164)
Why === extension of #163 for encoder types What changed ============ fix the encoder by using the specialized_name with alias Test plan ========= wrote test
1 parent ea06e01 commit d3bf487

File tree

10 files changed

+465
-12
lines changed

10 files changed

+465
-12
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ lint = { workspace = true }
6060

6161
[tool.ruff]
6262
lint.select = ["F", "E", "W", "I001"]
63-
exclude = ["*/generated/*", "*/snapshots/*"]
63+
exclude = ["*/generated/*", "*/snapshots/*", "*/generated_special_chars/*"]
6464

6565
# Should be kept in sync with mypy.ini in the project root.
6666
# The VSCode mypy extension can only read /mypy.ini.

src/replit_river/codegen/client.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,13 @@ def extract_props(tpe: RiverType) -> list[dict[str, RiverType]]:
575575
if name == "$kind":
576576
safe_name = "kind"
577577
else:
578-
safe_name = name
578+
# For TypedDict encoder, use normalized name to access
579+
# the TypedDict field but the output dictionary key should
580+
# use the original name
581+
if base_model == "TypedDict":
582+
safe_name = normalize_special_chars(name)
583+
else:
584+
safe_name = name
579585
if prop.type == "object" and not prop.patternProperties:
580586
encoder_name = TypeName(
581587
f"encode_{render_literal_type(type_name)}"
@@ -675,14 +681,20 @@ def extract_props(tpe: RiverType) -> list[dict[str, RiverType]]:
675681
effective_name = name
676682
extras = []
677683
if name != specialized_name:
678-
if base_model != "BaseModel":
679-
# TODO: alias support for TypedDict
680-
raise ValueError(
681-
f"Field {name} is not a valid Python identifier, but it is in the schema" # noqa: E501
682-
)
683-
# Pydantic doesn't allow leading underscores in field names
684-
effective_name = specialized_name.lstrip("_")
685-
extras.append(f"alias={repr(name)}")
684+
if base_model == "BaseModel":
685+
# Pydantic doesn't allow leading underscores in field names
686+
effective_name = specialized_name
687+
extras.append(f"alias={repr(name)}")
688+
elif base_model == "TypedDict":
689+
# For TypedDict, we use the normalized name directly
690+
# TypedDict doesn't support aliases, so we normalize
691+
# the field name
692+
effective_name = specialized_name
693+
else:
694+
# For RiverError (which extends BaseModel), use alias
695+
# like BaseModel
696+
effective_name = specialized_name
697+
extras.append(f"alias={repr(name)}")
686698

687699
effective_field_names[effective_name].append(name)
688700

src/replit_river/codegen/typing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ def work(
165165
def normalize_special_chars(value: str) -> str:
166166
for char in SPECIAL_CHARS:
167167
value = value.replace(char, "_")
168-
return value
168+
return value.lstrip("_")
169169

170170

171171
def render_type_expr(value: TypeExpression) -> str:
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Code generated by river.codegen. DO NOT EDIT.
2+
from pydantic import BaseModel
3+
from typing import Literal
4+
5+
import replit_river as river
6+
7+
8+
from .test_service import Test_ServiceService
9+
10+
11+
class SpecialCharsClient:
12+
def __init__(self, client: river.Client[Literal[None]]):
13+
self.test_service = Test_ServiceService(client)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Code generated by river.codegen. DO NOT EDIT.
2+
from collections.abc import AsyncIterable, AsyncIterator
3+
from typing import Any
4+
import datetime
5+
6+
from pydantic import TypeAdapter
7+
8+
from replit_river.error_schema import RiverError, RiverErrorTypeAdapter
9+
import replit_river as river
10+
11+
12+
from .rpc_method import Rpc_MethodInput, encode_Rpc_MethodInput
13+
14+
boolTypeAdapter: TypeAdapter[bool] = TypeAdapter(bool)
15+
16+
17+
class Test_ServiceService:
18+
def __init__(self, client: river.Client[Any]):
19+
self.client = client
20+
21+
async def rpc_method(
22+
self,
23+
input: Rpc_MethodInput,
24+
timeout: datetime.timedelta,
25+
) -> bool:
26+
return await self.client.send_rpc(
27+
"test_service",
28+
"rpc_method",
29+
input,
30+
encode_Rpc_MethodInput,
31+
lambda x: boolTypeAdapter.validate_python(
32+
x # type: ignore[arg-type]
33+
),
34+
lambda x: RiverErrorTypeAdapter.validate_python(
35+
x # type: ignore[arg-type]
36+
),
37+
timeout,
38+
)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Code generated by river.codegen. DO NOT EDIT.
2+
from collections.abc import AsyncIterable, AsyncIterator
3+
import datetime
4+
from typing import (
5+
Any,
6+
Literal,
7+
Mapping,
8+
NotRequired,
9+
TypedDict,
10+
)
11+
from typing_extensions import Annotated
12+
13+
from pydantic import BaseModel, Field, TypeAdapter, WrapValidator
14+
from replit_river.error_schema import RiverError
15+
from replit_river.client import (
16+
RiverUnknownError,
17+
translate_unknown_error,
18+
RiverUnknownValue,
19+
translate_unknown_value,
20+
)
21+
22+
import replit_river as river
23+
24+
25+
def encode_Rpc_MethodInput(
26+
x: "Rpc_MethodInput",
27+
) -> Any:
28+
return {
29+
k: v
30+
for (k, v) in (
31+
{
32+
"data field6": x.get("data_field6"),
33+
"data-field1": x.get("data_field1"),
34+
"data.field3": x.get("data_field3"),
35+
"data/field4": x.get("data_field4"),
36+
"data:field2": x.get("data_field2"),
37+
"data@field5": x.get("data_field5"),
38+
}
39+
).items()
40+
if v is not None
41+
}
42+
43+
44+
class Rpc_MethodInput(TypedDict):
45+
data_field6: NotRequired[str | None]
46+
data_field1: str
47+
data_field3: NotRequired[bool | None]
48+
data_field4: NotRequired[str | None]
49+
data_field2: float
50+
data_field5: NotRequired[int | None]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"services": {
3+
"test_service": {
4+
"procedures": {
5+
"rpc_method": {
6+
"input": {
7+
"type": "object",
8+
"properties": {
9+
"data-3": {
10+
"type": "string"
11+
},
12+
"data:3": {
13+
"type": "number"
14+
}
15+
},
16+
"required": ["data-3", "data:3"]
17+
},
18+
"output": {
19+
"type": "boolean"
20+
},
21+
"errors": {
22+
"not": {}
23+
},
24+
"type": "rpc"
25+
}
26+
}
27+
}
28+
}
29+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"services": {
3+
"test_service": {
4+
"procedures": {
5+
"rpc_method": {
6+
"input": {
7+
"type": "object",
8+
"properties": {
9+
"data-field1": {
10+
"type": "string"
11+
},
12+
"data:field2": {
13+
"type": "number"
14+
},
15+
"data.field3": {
16+
"type": "boolean"
17+
},
18+
"data/field4": {
19+
"type": "string"
20+
},
21+
"data@field5": {
22+
"type": "integer"
23+
},
24+
"data field6": {
25+
"type": "string"
26+
}
27+
},
28+
"required": ["data-field1", "data:field2"]
29+
},
30+
"output": {
31+
"type": "boolean"
32+
},
33+
"errors": {
34+
"not": {}
35+
},
36+
"type": "rpc"
37+
}
38+
}
39+
}
40+
}
41+
}

0 commit comments

Comments
 (0)