From 6edec216909b42394f3e8df21de07f9eb3da79be Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Mon, 18 Aug 2025 11:11:47 -0700 Subject: [PATCH 1/6] encode special fields --- src/replit_river/codegen/client.py | 28 ++- .../codegen/rpc/input-collision-schema.json | 29 ++++ .../rpc/input-special-chars-schema.json | 41 +++++ tests/v1/codegen/test_input_special_chars.py | 159 ++++++++++++++++++ 4 files changed, 248 insertions(+), 9 deletions(-) create mode 100644 tests/v1/codegen/rpc/input-collision-schema.json create mode 100644 tests/v1/codegen/rpc/input-special-chars-schema.json create mode 100644 tests/v1/codegen/test_input_special_chars.py diff --git a/src/replit_river/codegen/client.py b/src/replit_river/codegen/client.py index d89359d3..4e07090e 100644 --- a/src/replit_river/codegen/client.py +++ b/src/replit_river/codegen/client.py @@ -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": + specialized_name = normalize_special_chars(name) + safe_name = specialized_name.lstrip("_") if name != specialized_name else name + else: + safe_name = name if prop.type == "object" and not prop.patternProperties: encoder_name = TypeName( f"encode_{render_literal_type(type_name)}" @@ -675,14 +681,18 @@ 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.lstrip("_") + 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.lstrip("_") + else: + # For RiverError (which extends BaseModel), use alias like BaseModel + effective_name = specialized_name.lstrip("_") + extras.append(f"alias={repr(name)}") effective_field_names[effective_name].append(name) diff --git a/tests/v1/codegen/rpc/input-collision-schema.json b/tests/v1/codegen/rpc/input-collision-schema.json new file mode 100644 index 00000000..8c58913d --- /dev/null +++ b/tests/v1/codegen/rpc/input-collision-schema.json @@ -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" + } + } + } + } +} diff --git a/tests/v1/codegen/rpc/input-special-chars-schema.json b/tests/v1/codegen/rpc/input-special-chars-schema.json new file mode 100644 index 00000000..6c94b567 --- /dev/null +++ b/tests/v1/codegen/rpc/input-special-chars-schema.json @@ -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" + } + } + } + } +} diff --git a/tests/v1/codegen/test_input_special_chars.py b/tests/v1/codegen/test_input_special_chars.py new file mode 100644 index 00000000..648fd83b --- /dev/null +++ b/tests/v1/codegen/test_input_special_chars.py @@ -0,0 +1,159 @@ +from io import StringIO + +import pytest + +from replit_river.codegen.client import schema_to_river_client_codegen + + +def test_input_special_chars_basemodel() -> None: + """Test that codegen handles special characters in input field names for BaseModel.""" + + # Test should pass without raising an exception + schema_to_river_client_codegen( + read_schema=lambda: open("tests/v1/codegen/rpc/input-special-chars-schema.json"), + target_path="tests/v1/codegen/rpc/generated_input_special", + client_name="InputSpecialClient", + typed_dict_inputs=False, # BaseModel inputs + file_opener=lambda _: StringIO(), + method_filter=None, + protocol_version="v1.1", + ) + + +def test_input_special_chars_typeddict() -> None: + """Test that codegen handles special characters in input field names for TypedDict.""" + + # Test should pass without raising an exception + schema_to_river_client_codegen( + read_schema=lambda: open("tests/v1/codegen/rpc/input-special-chars-schema.json"), + target_path="tests/v1/codegen/rpc/generated_input_special_td", + client_name="InputSpecialTDClient", + typed_dict_inputs=True, # TypedDict inputs + file_opener=lambda _: StringIO(), + method_filter=None, + protocol_version="v1.1", + ) + + +def test_input_collision_error_basemodel() -> None: + """Test that codegen raises ValueError for input field name collisions with BaseModel.""" + + with pytest.raises(ValueError) as exc_info: + schema_to_river_client_codegen( + read_schema=lambda: open("tests/v1/codegen/rpc/input-collision-schema.json"), + target_path="tests/v1/codegen/rpc/generated_input_collision", + client_name="InputCollisionClient", + typed_dict_inputs=False, # BaseModel inputs + file_opener=lambda _: StringIO(), + method_filter=None, + protocol_version="v1.1", + ) + + # Check that the error message matches the expected format for field name collision + error_message = str(exc_info.value) + assert "Field name collision" in error_message + assert "data-3" in error_message + assert "data:3" in error_message + assert "all normalize to the same effective name 'data_3'" in error_message + + +def test_input_collision_error_typeddict() -> None: + """Test that codegen raises ValueError for input field name collisions with TypedDict.""" + + with pytest.raises(ValueError) as exc_info: + schema_to_river_client_codegen( + read_schema=lambda: open("tests/v1/codegen/rpc/input-collision-schema.json"), + target_path="tests/v1/codegen/rpc/generated_input_collision_td", + client_name="InputCollisionTDClient", + typed_dict_inputs=True, # TypedDict inputs + file_opener=lambda _: StringIO(), + method_filter=None, + protocol_version="v1.1", + ) + + # Check that the error message matches the expected format for field name collision + error_message = str(exc_info.value) + assert "Field name collision" in error_message + assert "data-3" in error_message + assert "data:3" in error_message + assert "all normalize to the same effective name 'data_3'" in error_message + + +def test_init_special_chars_basemodel() -> None: + """Test that codegen handles special characters in init field names for BaseModel.""" + + init_schema = { + "services": { + "test_service": { + "procedures": { + "stream_method": { + "init": { + "type": "object", + "properties": { + "init-field1": {"type": "string"}, + "init:field2": {"type": "number"}, + "init.field3": {"type": "boolean"} + }, + "required": ["init-field1"] + }, + "output": {"type": "boolean"}, + "errors": {"not": {}}, + "type": "stream" + } + } + } + } + } + + import json + + # Test should pass without raising an exception + schema_to_river_client_codegen( + read_schema=lambda: StringIO(json.dumps(init_schema)), + target_path="tests/v1/codegen/rpc/generated_init_special", + client_name="InitSpecialClient", + typed_dict_inputs=False, # BaseModel inputs + file_opener=lambda _: StringIO(), + method_filter=None, + protocol_version="v2.0", + ) + + +def test_init_special_chars_typeddict() -> None: + """Test that codegen handles special characters in init field names for TypedDict.""" + + init_schema = { + "services": { + "test_service": { + "procedures": { + "stream_method": { + "init": { + "type": "object", + "properties": { + "init-field1": {"type": "string"}, + "init:field2": {"type": "number"}, + "init.field3": {"type": "boolean"} + }, + "required": ["init-field1"] + }, + "output": {"type": "boolean"}, + "errors": {"not": {}}, + "type": "stream" + } + } + } + } + } + + import json + + # Test should pass without raising an exception + schema_to_river_client_codegen( + read_schema=lambda: StringIO(json.dumps(init_schema)), + target_path="tests/v1/codegen/rpc/generated_init_special_td", + client_name="InitSpecialTDClient", + typed_dict_inputs=True, # TypedDict inputs + file_opener=lambda _: StringIO(), + method_filter=None, + protocol_version="v2.0", + ) From a1b0dede7806ad52622b9521b388d48fb2775cee Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Mon, 18 Aug 2025 11:33:08 -0700 Subject: [PATCH 2/6] lint, address review comment --- src/replit_river/codegen/client.py | 20 +++---- src/replit_river/codegen/typing.py | 2 +- tests/v1/codegen/test_input_special_chars.py | 57 +++++++++++--------- 3 files changed, 44 insertions(+), 35 deletions(-) diff --git a/src/replit_river/codegen/client.py b/src/replit_river/codegen/client.py index 4e07090e..95aa3304 100644 --- a/src/replit_river/codegen/client.py +++ b/src/replit_river/codegen/client.py @@ -575,11 +575,11 @@ def extract_props(tpe: RiverType) -> list[dict[str, RiverType]]: if name == "$kind": safe_name = "kind" else: - # For TypedDict encoder, use normalized name to access the TypedDict field - # but the output dictionary key should use the original 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": - specialized_name = normalize_special_chars(name) - safe_name = specialized_name.lstrip("_") if name != specialized_name else name + safe_name = normalize_special_chars(name) else: safe_name = name if prop.type == "object" and not prop.patternProperties: @@ -683,15 +683,17 @@ def extract_props(tpe: RiverType) -> list[dict[str, RiverType]]: if name != specialized_name: if base_model == "BaseModel": # Pydantic doesn't allow leading underscores in field names - effective_name = specialized_name.lstrip("_") + 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.lstrip("_") + # 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.lstrip("_") + # 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) diff --git a/src/replit_river/codegen/typing.py b/src/replit_river/codegen/typing.py index 626b2180..5968a66d 100644 --- a/src/replit_river/codegen/typing.py +++ b/src/replit_river/codegen/typing.py @@ -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: diff --git a/tests/v1/codegen/test_input_special_chars.py b/tests/v1/codegen/test_input_special_chars.py index 648fd83b..86ffa617 100644 --- a/tests/v1/codegen/test_input_special_chars.py +++ b/tests/v1/codegen/test_input_special_chars.py @@ -6,11 +6,12 @@ def test_input_special_chars_basemodel() -> None: - """Test that codegen handles special characters in input field names for BaseModel.""" - - # Test should pass without raising an exception + """Handles special characters in input field names for BaseModel.""" + schema_to_river_client_codegen( - read_schema=lambda: open("tests/v1/codegen/rpc/input-special-chars-schema.json"), + read_schema=lambda: open( + "tests/v1/codegen/rpc/input-special-chars-schema.json" + ), # noqa: E501 target_path="tests/v1/codegen/rpc/generated_input_special", client_name="InputSpecialClient", typed_dict_inputs=False, # BaseModel inputs @@ -21,11 +22,13 @@ def test_input_special_chars_basemodel() -> None: def test_input_special_chars_typeddict() -> None: - """Test that codegen handles special characters in input field names for TypedDict.""" - + """Handles special characters in input field names for TypedDict.""" + # Test should pass without raising an exception schema_to_river_client_codegen( - read_schema=lambda: open("tests/v1/codegen/rpc/input-special-chars-schema.json"), + read_schema=lambda: open( + "tests/v1/codegen/rpc/input-special-chars-schema.json" + ), # noqa: E501 target_path="tests/v1/codegen/rpc/generated_input_special_td", client_name="InputSpecialTDClient", typed_dict_inputs=True, # TypedDict inputs @@ -36,11 +39,13 @@ def test_input_special_chars_typeddict() -> None: def test_input_collision_error_basemodel() -> None: - """Test that codegen raises ValueError for input field name collisions with BaseModel.""" + """Raises ValueError for input field name collisions with BaseModel.""" with pytest.raises(ValueError) as exc_info: schema_to_river_client_codegen( - read_schema=lambda: open("tests/v1/codegen/rpc/input-collision-schema.json"), + read_schema=lambda: open( + "tests/v1/codegen/rpc/input-collision-schema.json" + ), # noqa: E501 target_path="tests/v1/codegen/rpc/generated_input_collision", client_name="InputCollisionClient", typed_dict_inputs=False, # BaseModel inputs @@ -58,11 +63,13 @@ def test_input_collision_error_basemodel() -> None: def test_input_collision_error_typeddict() -> None: - """Test that codegen raises ValueError for input field name collisions with TypedDict.""" + """Raises ValueError for input field name collisions with TypedDict.""" with pytest.raises(ValueError) as exc_info: schema_to_river_client_codegen( - read_schema=lambda: open("tests/v1/codegen/rpc/input-collision-schema.json"), + read_schema=lambda: open( + "tests/v1/codegen/rpc/input-collision-schema.json" + ), # noqa: E501 target_path="tests/v1/codegen/rpc/generated_input_collision_td", client_name="InputCollisionTDClient", typed_dict_inputs=True, # TypedDict inputs @@ -80,8 +87,8 @@ def test_input_collision_error_typeddict() -> None: def test_init_special_chars_basemodel() -> None: - """Test that codegen handles special characters in init field names for BaseModel.""" - + """Handles special characters in init field names for BaseModel.""" + init_schema = { "services": { "test_service": { @@ -92,21 +99,21 @@ def test_init_special_chars_basemodel() -> None: "properties": { "init-field1": {"type": "string"}, "init:field2": {"type": "number"}, - "init.field3": {"type": "boolean"} + "init.field3": {"type": "boolean"}, }, - "required": ["init-field1"] + "required": ["init-field1"], }, "output": {"type": "boolean"}, "errors": {"not": {}}, - "type": "stream" + "type": "stream", } } } } } - + import json - + # Test should pass without raising an exception schema_to_river_client_codegen( read_schema=lambda: StringIO(json.dumps(init_schema)), @@ -120,8 +127,8 @@ def test_init_special_chars_basemodel() -> None: def test_init_special_chars_typeddict() -> None: - """Test that codegen handles special characters in init field names for TypedDict.""" - + """Handles special characters in init field names for TypedDict.""" + init_schema = { "services": { "test_service": { @@ -132,21 +139,21 @@ def test_init_special_chars_typeddict() -> None: "properties": { "init-field1": {"type": "string"}, "init:field2": {"type": "number"}, - "init.field3": {"type": "boolean"} + "init.field3": {"type": "boolean"}, }, - "required": ["init-field1"] + "required": ["init-field1"], }, "output": {"type": "boolean"}, "errors": {"not": {}}, - "type": "stream" + "type": "stream", } } } } } - + import json - + # Test should pass without raising an exception schema_to_river_client_codegen( read_schema=lambda: StringIO(json.dumps(init_schema)), From 10be03c51e56d3f9d397ed62f818bb17a2e56824 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Mon, 18 Aug 2025 12:05:52 -0700 Subject: [PATCH 3/6] another e2e test --- tests/v1/codegen/test_rpc.py | 98 +++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/tests/v1/codegen/test_rpc.py b/tests/v1/codegen/test_rpc.py index e9cd0699..e9cc18d4 100644 --- a/tests/v1/codegen/test_rpc.py +++ b/tests/v1/codegen/test_rpc.py @@ -38,10 +38,31 @@ def file_opener(path: Path) -> TextIO: @pytest.fixture(scope="session", autouse=True) -def reload_rpc_import(generate_rpc_client: None) -> None: +def generate_special_chars_client() -> None: + shutil.rmtree("tests/v1/codegen/rpc/generated_special_chars", ignore_errors=True) + os.makedirs("tests/v1/codegen/rpc/generated_special_chars") + + def file_opener(path: Path) -> TextIO: + return open(path, "w") + + schema_to_river_client_codegen( + read_schema=lambda: open("tests/v1/codegen/rpc/input-special-chars-schema.json"), # noqa: E501 + target_path="tests/v1/codegen/rpc/generated_special_chars", + client_name="SpecialCharsClient", + typed_dict_inputs=True, + file_opener=file_opener, + method_filter=None, + protocol_version="v1.1", + ) + + +@pytest.fixture(scope="session", autouse=True) +def reload_rpc_import(generate_rpc_client: None, generate_special_chars_client: None) -> None: # noqa: E501 import tests.v1.codegen.rpc.generated + import tests.v1.codegen.rpc.generated_special_chars importlib.reload(tests.v1.codegen.rpc.generated) + importlib.reload(tests.v1.codegen.rpc.generated_special_chars) @pytest.mark.asyncio @@ -74,6 +95,50 @@ async def rpc_timeout_handler(request: str, context: grpc.aio.ServicerContext) - } +def deserialize_special_chars_request(request: dict) -> dict: + """Deserialize request for special chars test - pass through the full dict.""" + return request + + +def serialize_special_chars_response(response: bool) -> dict: + """Serialize response for special chars test.""" + return response + + +async def special_chars_handler(request: dict, context: grpc.aio.ServicerContext) -> bool: # noqa: E501 + """Handler that processes input with special character field names.""" + # The request comes with original field names (with special characters) + # as they are sent over the wire before normalization + + # Verify we received the required fields with their original names + required_fields = ["data-field1", "data:field2"] + + for field in required_fields: + if field not in request: + raise ValueError(f"Missing required field: {field}. Available keys: {list(request.keys())}") # noqa: E501 + + # Verify the values are of expected types + if not isinstance(request["data-field1"], str): + raise ValueError("data-field1 should be a string") + if not isinstance(request["data:field2"], (int, float)): + raise ValueError("data:field2 should be a number") + + # Return True if all expected data is present and valid + return True + + +special_chars_method: HandlerMapping = { + ("test_service", "rpc_method"): ( + "rpc", + rpc_method_handler( + special_chars_handler, + deserialize_special_chars_request, + serialize_special_chars_response, + ), + ) +} + + @pytest.mark.asyncio @pytest.mark.parametrize("handlers", [{**rpc_timeout_method}]) async def test_rpc_timeout(client: Client) -> None: @@ -84,3 +149,34 @@ async def test_rpc_timeout(client: Client) -> None: {"data": "feep", "data2": datetime.now(timezone.utc)}, timedelta(milliseconds=200), ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("handlers", [{**special_chars_method}]) +async def test_special_chars_rpc(client: Client) -> None: + """Test RPC method with special characters in field names.""" + from tests.v1.codegen.rpc.generated_special_chars import SpecialCharsClient + + # Test with all fields including optional ones + result = await SpecialCharsClient(client).test_service.rpc_method( + { + "data_field1": "test_value1", # Required: data-field1 -> data_field1 + "data_field2": 42.5, # Required: data:field2 -> data_field2 + "data_field3": True, # Optional: data.field3 -> data_field3 + "data_field4": "test_value4", # Optional: data/field4 -> data_field4 + "data_field5": 123, # Optional: data@field5 -> data_field5 + "data_field6": "test_value6", # Optional: data field6 -> data_field6 + }, + timedelta(seconds=5), + ) + assert result is True + + # Test with only required fields + result = await SpecialCharsClient(client).test_service.rpc_method( + { + "data_field1": "required_value", + "data_field2": 99.9, + }, + timedelta(seconds=5), + ) + assert result is True From fefc07a26ecaadf04b68ae96418ee179df170c3f Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Mon, 18 Aug 2025 12:08:30 -0700 Subject: [PATCH 4/6] format --- tests/v1/codegen/test_rpc.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/tests/v1/codegen/test_rpc.py b/tests/v1/codegen/test_rpc.py index e9cc18d4..49583a87 100644 --- a/tests/v1/codegen/test_rpc.py +++ b/tests/v1/codegen/test_rpc.py @@ -46,7 +46,9 @@ def file_opener(path: Path) -> TextIO: return open(path, "w") schema_to_river_client_codegen( - read_schema=lambda: open("tests/v1/codegen/rpc/input-special-chars-schema.json"), # noqa: E501 + read_schema=lambda: open( + "tests/v1/codegen/rpc/input-special-chars-schema.json" + ), # noqa: E501 target_path="tests/v1/codegen/rpc/generated_special_chars", client_name="SpecialCharsClient", typed_dict_inputs=True, @@ -57,7 +59,9 @@ def file_opener(path: Path) -> TextIO: @pytest.fixture(scope="session", autouse=True) -def reload_rpc_import(generate_rpc_client: None, generate_special_chars_client: None) -> None: # noqa: E501 +def reload_rpc_import( + generate_rpc_client: None, generate_special_chars_client: None +) -> None: # noqa: E501 import tests.v1.codegen.rpc.generated import tests.v1.codegen.rpc.generated_special_chars @@ -105,7 +109,9 @@ def serialize_special_chars_response(response: bool) -> dict: return response -async def special_chars_handler(request: dict, context: grpc.aio.ServicerContext) -> bool: # noqa: E501 +async def special_chars_handler( + request: dict, context: grpc.aio.ServicerContext +) -> bool: # noqa: E501 """Handler that processes input with special character field names.""" # The request comes with original field names (with special characters) # as they are sent over the wire before normalization @@ -115,7 +121,9 @@ async def special_chars_handler(request: dict, context: grpc.aio.ServicerContext for field in required_fields: if field not in request: - raise ValueError(f"Missing required field: {field}. Available keys: {list(request.keys())}") # noqa: E501 + raise ValueError( + f"Missing required field: {field}. Available keys: {list(request.keys())}" + ) # noqa: E501 # Verify the values are of expected types if not isinstance(request["data-field1"], str): @@ -161,11 +169,11 @@ async def test_special_chars_rpc(client: Client) -> None: result = await SpecialCharsClient(client).test_service.rpc_method( { "data_field1": "test_value1", # Required: data-field1 -> data_field1 - "data_field2": 42.5, # Required: data:field2 -> data_field2 - "data_field3": True, # Optional: data.field3 -> data_field3 - "data_field4": "test_value4", # Optional: data/field4 -> data_field4 - "data_field5": 123, # Optional: data@field5 -> data_field5 - "data_field6": "test_value6", # Optional: data field6 -> data_field6 + "data_field2": 42.5, # Required: data:field2 -> data_field2 + "data_field3": True, # Optional: data.field3 -> data_field3 + "data_field4": "test_value4", # Optional: data/field4 -> data_field4 + "data_field5": 123, # Optional: data@field5 -> data_field5 + "data_field6": "test_value6", # Optional: data field6 -> data_field6 }, timedelta(seconds=5), ) From 2d914ddd523f45d0454a807ae9058b58f8e18994 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Mon, 18 Aug 2025 12:09:34 -0700 Subject: [PATCH 5/6] fix noqa --- tests/v1/codegen/test_rpc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/v1/codegen/test_rpc.py b/tests/v1/codegen/test_rpc.py index 49583a87..07c36b90 100644 --- a/tests/v1/codegen/test_rpc.py +++ b/tests/v1/codegen/test_rpc.py @@ -122,8 +122,8 @@ async def special_chars_handler( for field in required_fields: if field not in request: raise ValueError( - f"Missing required field: {field}. Available keys: {list(request.keys())}" - ) # noqa: E501 + f"Missing required field: {field}. Available keys: {list(request.keys())}" # noqa: E501 + ) # Verify the values are of expected types if not isinstance(request["data-field1"], str): From e424466ba64836e1461188cacd6e5826eabb84fc Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Mon, 18 Aug 2025 12:17:11 -0700 Subject: [PATCH 6/6] put the right files --- pyproject.toml | 2 +- .../rpc/generated_special_chars/__init__.py | 13 +++++ .../test_service/__init__.py | 38 ++++++++++++++ .../test_service/rpc_method.py | 50 +++++++++++++++++++ tests/v1/codegen/test_rpc.py | 4 +- 5 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 tests/v1/codegen/rpc/generated_special_chars/__init__.py create mode 100644 tests/v1/codegen/rpc/generated_special_chars/test_service/__init__.py create mode 100644 tests/v1/codegen/rpc/generated_special_chars/test_service/rpc_method.py diff --git a/pyproject.toml b/pyproject.toml index 2f049a3c..1948c776 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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. diff --git a/tests/v1/codegen/rpc/generated_special_chars/__init__.py b/tests/v1/codegen/rpc/generated_special_chars/__init__.py new file mode 100644 index 00000000..dbff569a --- /dev/null +++ b/tests/v1/codegen/rpc/generated_special_chars/__init__.py @@ -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) diff --git a/tests/v1/codegen/rpc/generated_special_chars/test_service/__init__.py b/tests/v1/codegen/rpc/generated_special_chars/test_service/__init__.py new file mode 100644 index 00000000..c5717c0d --- /dev/null +++ b/tests/v1/codegen/rpc/generated_special_chars/test_service/__init__.py @@ -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, + ) diff --git a/tests/v1/codegen/rpc/generated_special_chars/test_service/rpc_method.py b/tests/v1/codegen/rpc/generated_special_chars/test_service/rpc_method.py new file mode 100644 index 00000000..77efaee5 --- /dev/null +++ b/tests/v1/codegen/rpc/generated_special_chars/test_service/rpc_method.py @@ -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] diff --git a/tests/v1/codegen/test_rpc.py b/tests/v1/codegen/test_rpc.py index 07c36b90..2c1e796a 100644 --- a/tests/v1/codegen/test_rpc.py +++ b/tests/v1/codegen/test_rpc.py @@ -104,8 +104,8 @@ def deserialize_special_chars_request(request: dict) -> dict: return request -def serialize_special_chars_response(response: bool) -> dict: - """Serialize response for special chars test.""" +def serialize_special_chars_response(response: bool) -> bool: + """Serialize response for special chars test - pass through boolean.""" return response