From 4a5c71c8feb84eb34e5debddaea7bf26df332ff7 Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Fri, 31 Jan 2025 15:32:17 -0800 Subject: [PATCH 01/18] Bumping ruff --- replit_river/codegen/client.py | 12 +++++----- replit_river/session.py | 2 +- tests/test_rate_limiter.py | 18 +++++++------- tests/test_seq_manager.py | 12 +++++----- uv.lock | 44 +++++++++++++++++----------------- 5 files changed, 44 insertions(+), 44 deletions(-) diff --git a/replit_river/codegen/client.py b/replit_river/codegen/client.py index ee1b8030..278c2528 100644 --- a/replit_river/codegen/client.py +++ b/replit_river/codegen/client.py @@ -822,9 +822,9 @@ def __init__(self, client: river.Client[Any]): .validate_python """ - assert ( - init_type is None or render_init_method - ), f"Unable to derive the init encoder from: {input_type}" + assert init_type is None or render_init_method, ( + f"Unable to derive the init encoder from: {input_type}" + ) # Input renderer render_input_method: Optional[str] = None @@ -862,9 +862,9 @@ def __init__(self, client: river.Client[Any]): ): render_input_method = "lambda x: x" - assert ( - render_input_method - ), f"Unable to derive the input encoder from: {input_type}" + assert render_input_method, ( + f"Unable to derive the input encoder from: {input_type}" + ) if output_type == "None": parse_output_method = "lambda x: None" diff --git a/replit_river/session.py b/replit_river/session.py index eb34d9ef..a69f212c 100644 --- a/replit_river/session.py +++ b/replit_river/session.py @@ -474,7 +474,7 @@ async def _open_stream_and_call_handler( handler = self._handlers.get(key, None) if not handler: raise IgnoreMessageException( - f"No handler for {key} handlers : " f"{self._handlers.keys()}" + f"No handler for {key} handlers : {self._handlers.keys()}" ) method_type, handler_func = handler is_streaming_output = method_type in ( diff --git a/tests/test_rate_limiter.py b/tests/test_rate_limiter.py index b0d8990d..511ea03b 100644 --- a/tests/test_rate_limiter.py +++ b/tests/test_rate_limiter.py @@ -32,9 +32,9 @@ async def test_initial_budget(rate_limiter: LeakyBucketRateLimit) -> None: async def test_consume_budget(rate_limiter: LeakyBucketRateLimit) -> None: user: str = "user2" rate_limiter.consume_budget(user) - assert ( - rate_limiter.get_budget_consumed(user) == 1 - ), "Budget consumed should be incremented" + assert rate_limiter.get_budget_consumed(user) == 1, ( + "Budget consumed should be incremented" + ) @pytest.mark.asyncio @@ -43,9 +43,9 @@ async def test_restore_budget(rate_limiter: LeakyBucketRateLimit) -> None: rate_limiter.consume_budget(user) rate_limiter.start_restoring_budget(user) await asyncio.sleep(0.3) # Wait more than budget restore interval - assert ( - rate_limiter.get_budget_consumed(user) == 0 - ), "Budget should be restored after interval" + assert rate_limiter.get_budget_consumed(user) == 0, ( + "Budget should be restored after interval" + ) @pytest.mark.asyncio @@ -58,9 +58,9 @@ async def consume_budget() -> None: await asyncio.sleep(0.01) # simulate some delay await asyncio.gather(consume_budget(), consume_budget()) - assert ( - rate_limiter.get_budget_consumed(user) == 10 - ), "Concurrent access should be handled correctly" + assert rate_limiter.get_budget_consumed(user) == 10, ( + "Concurrent access should be handled correctly" + ) def test_close(rate_limiter: LeakyBucketRateLimit) -> None: diff --git a/tests/test_seq_manager.py b/tests/test_seq_manager.py index 748f4fc6..cf53a3d7 100644 --- a/tests/test_seq_manager.py +++ b/tests/test_seq_manager.py @@ -65,10 +65,10 @@ async def test_concurrent_access_to_sequence(no_logging_error: NoErrors) -> None manager = SeqManager() tasks = [manager.get_seq_and_increment() for _ in range(10)] results = await asyncio.gather(*tasks) - assert ( - len(set(results)) == 10 - ), "Each increment call should return a unique sequence number" - assert ( - await manager.get_seq() == 10 - ), "Final sequence number should be 10 after 10 increments" + assert len(set(results)) == 10, ( + "Each increment call should return a unique sequence number" + ) + assert await manager.get_seq() == 10, ( + "Final sequence number should be 10 after 10 increments" + ) no_logging_error() diff --git a/uv.lock b/uv.lock index 9793044f..b1803e78 100644 --- a/uv.lock +++ b/uv.lock @@ -28,7 +28,7 @@ name = "click" version = "8.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } wheels = [ @@ -624,27 +624,27 @@ dev = [ [[package]] name = "ruff" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/51/231bb3790e5b0b9fd4131f9a231d73d061b3667522e3f406fd9b63334d0e/ruff-0.7.2.tar.gz", hash = "sha256:2b14e77293380e475b4e3a7a368e14549288ed2931fce259a6f99978669e844f", size = 3210036 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/56/0caa2b5745d66a39aa239c01059f6918fc76ed8380033d2f44bf297d141d/ruff-0.7.2-py3-none-linux_armv6l.whl", hash = "sha256:b73f873b5f52092e63ed540adefc3c36f1f803790ecf2590e1df8bf0a9f72cb8", size = 10373973 }, - { url = "https://files.pythonhosted.org/packages/1a/33/cad6ff306731f335d481c50caa155b69a286d5b388e87ff234cd2a4b3557/ruff-0.7.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5b813ef26db1015953daf476202585512afd6a6862a02cde63f3bafb53d0b2d4", size = 10171140 }, - { url = "https://files.pythonhosted.org/packages/97/f5/6a2ca5c9ba416226eac9cf8121a1baa6f06655431937e85f38ffcb9d0d01/ruff-0.7.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:853277dbd9675810c6826dad7a428d52a11760744508340e66bf46f8be9701d9", size = 9809333 }, - { url = "https://files.pythonhosted.org/packages/16/83/e3e87f13d1a1dc205713632978cd7bc287a59b08bc95780dbe359b9aefcb/ruff-0.7.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21aae53ab1490a52bf4e3bf520c10ce120987b047c494cacf4edad0ba0888da2", size = 10622987 }, - { url = "https://files.pythonhosted.org/packages/22/16/97ccab194480e99a2e3c77ae132b3eebfa38c2112747570c403a4a13ba3a/ruff-0.7.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ccc7e0fc6e0cb3168443eeadb6445285abaae75142ee22b2b72c27d790ab60ba", size = 10184640 }, - { url = "https://files.pythonhosted.org/packages/97/1b/82ff05441b036f68817296c14f24da47c591cb27acfda473ee571a5651ac/ruff-0.7.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd77877a4e43b3a98e5ef4715ba3862105e299af0c48942cc6d51ba3d97dc859", size = 11210203 }, - { url = "https://files.pythonhosted.org/packages/a6/96/7ecb30a7ef7f942e2d8e0287ad4c1957dddc6c5097af4978c27cfc334f97/ruff-0.7.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e00163fb897d35523c70d71a46fbaa43bf7bf9af0f4534c53ea5b96b2e03397b", size = 11870894 }, - { url = "https://files.pythonhosted.org/packages/06/6a/c716bb126218227f8e604a9c484836257708a05ee3d2ebceb666ff3d3867/ruff-0.7.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3c54b538633482dc342e9b634d91168fe8cc56b30a4b4f99287f4e339103e88", size = 11449533 }, - { url = "https://files.pythonhosted.org/packages/e6/2f/3a5f9f9478904e5ae9506ea699109070ead1e79aac041e872cbaad8a7458/ruff-0.7.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b792468e9804a204be221b14257566669d1db5c00d6bb335996e5cd7004ba80", size = 12607919 }, - { url = "https://files.pythonhosted.org/packages/a0/57/4642e57484d80d274750dcc872ea66655bbd7e66e986fede31e1865b463d/ruff-0.7.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dba53ed84ac19ae4bfb4ea4bf0172550a2285fa27fbb13e3746f04c80f7fa088", size = 11016915 }, - { url = "https://files.pythonhosted.org/packages/4d/6d/59be6680abee34c22296ae3f46b2a3b91662b8b18ab0bf388b5eb1355c97/ruff-0.7.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b19fafe261bf741bca2764c14cbb4ee1819b67adb63ebc2db6401dcd652e3748", size = 10625424 }, - { url = "https://files.pythonhosted.org/packages/82/e7/f6a643683354c9bc7879d2f228ee0324fea66d253de49273a0814fba1927/ruff-0.7.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28bd8220f4d8f79d590db9e2f6a0674f75ddbc3847277dd44ac1f8d30684b828", size = 10233692 }, - { url = "https://files.pythonhosted.org/packages/d7/48/b4e02fc835cd7ed1ee7318d9c53e48bcf6b66301f55925a7dcb920e45532/ruff-0.7.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9fd67094e77efbea932e62b5d2483006154794040abb3a5072e659096415ae1e", size = 10751825 }, - { url = "https://files.pythonhosted.org/packages/1e/06/6c5ee6ab7bb4cbad9e8bb9b2dd0d818c759c90c1c9e057c6ed70334b97f4/ruff-0.7.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:576305393998b7bd6c46018f8104ea3a9cb3fa7908c21d8580e3274a3b04b691", size = 11074811 }, - { url = "https://files.pythonhosted.org/packages/a1/16/8969304f25bcd0e4af1778342e63b715e91db8a2dbb51807acd858cba915/ruff-0.7.2-py3-none-win32.whl", hash = "sha256:fa993cfc9f0ff11187e82de874dfc3611df80852540331bc85c75809c93253a8", size = 8650268 }, - { url = "https://files.pythonhosted.org/packages/d9/18/c4b00d161def43fe5968e959039c8f6ce60dca762cec4a34e4e83a4210a0/ruff-0.7.2-py3-none-win_amd64.whl", hash = "sha256:dd8800cbe0254e06b8fec585e97554047fb82c894973f7ff18558eee33d1cb88", size = 9433693 }, - { url = "https://files.pythonhosted.org/packages/7f/7b/c920673ac01c19814dd15fc617c02301c522f3d6812ca2024f4588ed4549/ruff-0.7.2-py3-none-win_arm64.whl", hash = "sha256:bb8368cd45bba3f57bb29cbb8d64b4a33f8415d0149d2655c5c8539452ce7760", size = 8735845 }, +version = "0.9.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/17/529e78f49fc6f8076f50d985edd9a2cf011d1dbadb1cdeacc1d12afc1d26/ruff-0.9.4.tar.gz", hash = "sha256:6907ee3529244bb0ed066683e075f09285b38dd5b4039370df6ff06041ca19e7", size = 3599458 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/f8/3fafb7804d82e0699a122101b5bee5f0d6e17c3a806dcbc527bb7d3f5b7a/ruff-0.9.4-py3-none-linux_armv6l.whl", hash = "sha256:64e73d25b954f71ff100bb70f39f1ee09e880728efb4250c632ceed4e4cdf706", size = 11668400 }, + { url = "https://files.pythonhosted.org/packages/2e/a6/2efa772d335da48a70ab2c6bb41a096c8517ca43c086ea672d51079e3d1f/ruff-0.9.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6ce6743ed64d9afab4fafeaea70d3631b4d4b28b592db21a5c2d1f0ef52934bf", size = 11628395 }, + { url = "https://files.pythonhosted.org/packages/dc/d7/cd822437561082f1c9d7225cc0d0fbb4bad117ad7ac3c41cd5d7f0fa948c/ruff-0.9.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:54499fb08408e32b57360f6f9de7157a5fec24ad79cb3f42ef2c3f3f728dfe2b", size = 11090052 }, + { url = "https://files.pythonhosted.org/packages/9e/67/3660d58e893d470abb9a13f679223368ff1684a4ef40f254a0157f51b448/ruff-0.9.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37c892540108314a6f01f105040b5106aeb829fa5fb0561d2dcaf71485021137", size = 11882221 }, + { url = "https://files.pythonhosted.org/packages/79/d1/757559995c8ba5f14dfec4459ef2dd3fcea82ac43bc4e7c7bf47484180c0/ruff-0.9.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de9edf2ce4b9ddf43fd93e20ef635a900e25f622f87ed6e3047a664d0e8f810e", size = 11424862 }, + { url = "https://files.pythonhosted.org/packages/c0/96/7915a7c6877bb734caa6a2af424045baf6419f685632469643dbd8eb2958/ruff-0.9.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87c90c32357c74f11deb7fbb065126d91771b207bf9bfaaee01277ca59b574ec", size = 12626735 }, + { url = "https://files.pythonhosted.org/packages/0e/cc/dadb9b35473d7cb17c7ffe4737b4377aeec519a446ee8514123ff4a26091/ruff-0.9.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56acd6c694da3695a7461cc55775f3a409c3815ac467279dfa126061d84b314b", size = 13255976 }, + { url = "https://files.pythonhosted.org/packages/5f/c3/ad2dd59d3cabbc12df308cced780f9c14367f0321e7800ca0fe52849da4c/ruff-0.9.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0c93e7d47ed951b9394cf352d6695b31498e68fd5782d6cbc282425655f687a", size = 12752262 }, + { url = "https://files.pythonhosted.org/packages/c7/17/5f1971e54bd71604da6788efd84d66d789362b1105e17e5ccc53bba0289b/ruff-0.9.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d4c8772670aecf037d1bf7a07c39106574d143b26cfe5ed1787d2f31e800214", size = 14401648 }, + { url = "https://files.pythonhosted.org/packages/30/24/6200b13ea611b83260501b6955b764bb320e23b2b75884c60ee7d3f0b68e/ruff-0.9.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfc5f1d7afeda8d5d37660eeca6d389b142d7f2b5a1ab659d9214ebd0e025231", size = 12414702 }, + { url = "https://files.pythonhosted.org/packages/34/cb/f5d50d0c4ecdcc7670e348bd0b11878154bc4617f3fdd1e8ad5297c0d0ba/ruff-0.9.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faa935fc00ae854d8b638c16a5f1ce881bc3f67446957dd6f2af440a5fc8526b", size = 11859608 }, + { url = "https://files.pythonhosted.org/packages/d6/f4/9c8499ae8426da48363bbb78d081b817b0f64a9305f9b7f87eab2a8fb2c1/ruff-0.9.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a6c634fc6f5a0ceae1ab3e13c58183978185d131a29c425e4eaa9f40afe1e6d6", size = 11485702 }, + { url = "https://files.pythonhosted.org/packages/18/59/30490e483e804ccaa8147dd78c52e44ff96e1c30b5a95d69a63163cdb15b/ruff-0.9.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:433dedf6ddfdec7f1ac7575ec1eb9844fa60c4c8c2f8887a070672b8d353d34c", size = 12067782 }, + { url = "https://files.pythonhosted.org/packages/3d/8c/893fa9551760b2f8eb2a351b603e96f15af167ceaf27e27ad873570bc04c/ruff-0.9.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d612dbd0f3a919a8cc1d12037168bfa536862066808960e0cc901404b77968f0", size = 12483087 }, + { url = "https://files.pythonhosted.org/packages/23/15/f6751c07c21ca10e3f4a51ea495ca975ad936d780c347d9808bcedbd7182/ruff-0.9.4-py3-none-win32.whl", hash = "sha256:db1192ddda2200671f9ef61d9597fcef89d934f5d1705e571a93a67fb13a4402", size = 9852302 }, + { url = "https://files.pythonhosted.org/packages/12/41/2d2d2c6a72e62566f730e49254f602dfed23019c33b5b21ea8f8917315a1/ruff-0.9.4-py3-none-win_amd64.whl", hash = "sha256:05bebf4cdbe3ef75430d26c375773978950bbf4ee3c95ccb5448940dc092408e", size = 10850051 }, + { url = "https://files.pythonhosted.org/packages/c6/e6/3d6ec3bc3d254e7f005c543a661a41c3e788976d0e52a1ada195bd664344/ruff-0.9.4-py3-none-win_arm64.whl", hash = "sha256:585792f1e81509e38ac5123492f8875fbc36f3ede8185af0a26df348e5154f41", size = 10078251 }, ] [[package]] From 42630bdc0ab7bb78f373befa59e67801a186ff52 Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Thu, 30 Jan 2025 15:17:07 -0800 Subject: [PATCH 02/18] Moving source into src/ --- pyproject.toml | 3 +++ {replit_river => src/replit_river}/__init__.py | 0 {replit_river => src/replit_river}/client.py | 0 {replit_river => src/replit_river}/client_session.py | 0 {replit_river => src/replit_river}/client_transport.py | 0 {replit_river => src/replit_river}/codegen/__init__.py | 0 {replit_river => src/replit_river}/codegen/__main__.py | 0 {replit_river => src/replit_river}/codegen/client.py | 0 {replit_river => src/replit_river}/codegen/format.py | 0 {replit_river => src/replit_river}/codegen/run.py | 0 {replit_river => src/replit_river}/codegen/schema.py | 0 {replit_river => src/replit_river}/codegen/server.py | 0 {replit_river => src/replit_river}/codegen/typing.py | 0 {replit_river => src/replit_river}/error_schema.py | 0 {replit_river => src/replit_river}/message_buffer.py | 0 {replit_river => src/replit_river}/messages.py | 0 {replit_river => src/replit_river}/py.typed | 0 {replit_river => src/replit_river}/rate_limiter.py | 0 {replit_river => src/replit_river}/rpc.py | 0 {replit_river => src/replit_river}/seq_manager.py | 0 {replit_river => src/replit_river}/server.py | 0 {replit_river => src/replit_river}/server_transport.py | 0 {replit_river => src/replit_river}/session.py | 0 {replit_river => src/replit_river}/task_manager.py | 0 {replit_river => src/replit_river}/transport.py | 0 {replit_river => src/replit_river}/transport_options.py | 0 {replit_river => src/replit_river}/websocket_wrapper.py | 0 27 files changed, 3 insertions(+) rename {replit_river => src/replit_river}/__init__.py (100%) rename {replit_river => src/replit_river}/client.py (100%) rename {replit_river => src/replit_river}/client_session.py (100%) rename {replit_river => src/replit_river}/client_transport.py (100%) rename {replit_river => src/replit_river}/codegen/__init__.py (100%) rename {replit_river => src/replit_river}/codegen/__main__.py (100%) rename {replit_river => src/replit_river}/codegen/client.py (100%) rename {replit_river => src/replit_river}/codegen/format.py (100%) rename {replit_river => src/replit_river}/codegen/run.py (100%) rename {replit_river => src/replit_river}/codegen/schema.py (100%) rename {replit_river => src/replit_river}/codegen/server.py (100%) rename {replit_river => src/replit_river}/codegen/typing.py (100%) rename {replit_river => src/replit_river}/error_schema.py (100%) rename {replit_river => src/replit_river}/message_buffer.py (100%) rename {replit_river => src/replit_river}/messages.py (100%) rename {replit_river => src/replit_river}/py.typed (100%) rename {replit_river => src/replit_river}/rate_limiter.py (100%) rename {replit_river => src/replit_river}/rpc.py (100%) rename {replit_river => src/replit_river}/seq_manager.py (100%) rename {replit_river => src/replit_river}/server.py (100%) rename {replit_river => src/replit_river}/server_transport.py (100%) rename {replit_river => src/replit_river}/session.py (100%) rename {replit_river => src/replit_river}/task_manager.py (100%) rename {replit_river => src/replit_river}/transport.py (100%) rename {replit_river => src/replit_river}/transport_options.py (100%) rename {replit_river => src/replit_river}/websocket_wrapper.py (100%) diff --git a/pyproject.toml b/pyproject.toml index 7ff26651..f6db8650 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,3 +91,6 @@ ignore_missing_imports = true [build-system] requires = ["hatchling"] build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/replit_river"] diff --git a/replit_river/__init__.py b/src/replit_river/__init__.py similarity index 100% rename from replit_river/__init__.py rename to src/replit_river/__init__.py diff --git a/replit_river/client.py b/src/replit_river/client.py similarity index 100% rename from replit_river/client.py rename to src/replit_river/client.py diff --git a/replit_river/client_session.py b/src/replit_river/client_session.py similarity index 100% rename from replit_river/client_session.py rename to src/replit_river/client_session.py diff --git a/replit_river/client_transport.py b/src/replit_river/client_transport.py similarity index 100% rename from replit_river/client_transport.py rename to src/replit_river/client_transport.py diff --git a/replit_river/codegen/__init__.py b/src/replit_river/codegen/__init__.py similarity index 100% rename from replit_river/codegen/__init__.py rename to src/replit_river/codegen/__init__.py diff --git a/replit_river/codegen/__main__.py b/src/replit_river/codegen/__main__.py similarity index 100% rename from replit_river/codegen/__main__.py rename to src/replit_river/codegen/__main__.py diff --git a/replit_river/codegen/client.py b/src/replit_river/codegen/client.py similarity index 100% rename from replit_river/codegen/client.py rename to src/replit_river/codegen/client.py diff --git a/replit_river/codegen/format.py b/src/replit_river/codegen/format.py similarity index 100% rename from replit_river/codegen/format.py rename to src/replit_river/codegen/format.py diff --git a/replit_river/codegen/run.py b/src/replit_river/codegen/run.py similarity index 100% rename from replit_river/codegen/run.py rename to src/replit_river/codegen/run.py diff --git a/replit_river/codegen/schema.py b/src/replit_river/codegen/schema.py similarity index 100% rename from replit_river/codegen/schema.py rename to src/replit_river/codegen/schema.py diff --git a/replit_river/codegen/server.py b/src/replit_river/codegen/server.py similarity index 100% rename from replit_river/codegen/server.py rename to src/replit_river/codegen/server.py diff --git a/replit_river/codegen/typing.py b/src/replit_river/codegen/typing.py similarity index 100% rename from replit_river/codegen/typing.py rename to src/replit_river/codegen/typing.py diff --git a/replit_river/error_schema.py b/src/replit_river/error_schema.py similarity index 100% rename from replit_river/error_schema.py rename to src/replit_river/error_schema.py diff --git a/replit_river/message_buffer.py b/src/replit_river/message_buffer.py similarity index 100% rename from replit_river/message_buffer.py rename to src/replit_river/message_buffer.py diff --git a/replit_river/messages.py b/src/replit_river/messages.py similarity index 100% rename from replit_river/messages.py rename to src/replit_river/messages.py diff --git a/replit_river/py.typed b/src/replit_river/py.typed similarity index 100% rename from replit_river/py.typed rename to src/replit_river/py.typed diff --git a/replit_river/rate_limiter.py b/src/replit_river/rate_limiter.py similarity index 100% rename from replit_river/rate_limiter.py rename to src/replit_river/rate_limiter.py diff --git a/replit_river/rpc.py b/src/replit_river/rpc.py similarity index 100% rename from replit_river/rpc.py rename to src/replit_river/rpc.py diff --git a/replit_river/seq_manager.py b/src/replit_river/seq_manager.py similarity index 100% rename from replit_river/seq_manager.py rename to src/replit_river/seq_manager.py diff --git a/replit_river/server.py b/src/replit_river/server.py similarity index 100% rename from replit_river/server.py rename to src/replit_river/server.py diff --git a/replit_river/server_transport.py b/src/replit_river/server_transport.py similarity index 100% rename from replit_river/server_transport.py rename to src/replit_river/server_transport.py diff --git a/replit_river/session.py b/src/replit_river/session.py similarity index 100% rename from replit_river/session.py rename to src/replit_river/session.py diff --git a/replit_river/task_manager.py b/src/replit_river/task_manager.py similarity index 100% rename from replit_river/task_manager.py rename to src/replit_river/task_manager.py diff --git a/replit_river/transport.py b/src/replit_river/transport.py similarity index 100% rename from replit_river/transport.py rename to src/replit_river/transport.py diff --git a/replit_river/transport_options.py b/src/replit_river/transport_options.py similarity index 100% rename from replit_river/transport_options.py rename to src/replit_river/transport_options.py diff --git a/replit_river/websocket_wrapper.py b/src/replit_river/websocket_wrapper.py similarity index 100% rename from replit_river/websocket_wrapper.py rename to src/replit_river/websocket_wrapper.py From 73b68ff2bc0454a0d7349e79a422e21af08befae Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Thu, 30 Jan 2025 15:42:02 -0800 Subject: [PATCH 03/18] Bubbling out file writer --- src/replit_river/codegen/client.py | 5 ++++- src/replit_river/codegen/run.py | 12 +++++++++++- tests/codegen/stream/test_stream.py | 8 +++++++- tests/codegen/test_rpc.py | 7 +++++++ 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/replit_river/codegen/client.py b/src/replit_river/codegen/client.py index 278c2528..7b993f19 100644 --- a/src/replit_river/codegen/client.py +++ b/src/replit_river/codegen/client.py @@ -5,6 +5,7 @@ from textwrap import dedent from typing import ( Any, + Callable, Dict, List, Literal, @@ -12,6 +13,7 @@ OrderedDict, Sequence, Set, + TextIO, Tuple, Union, cast, @@ -1094,6 +1096,7 @@ def schema_to_river_client_codegen( target_path: str, client_name: str, typed_dict_inputs: bool, + file_opener: Callable[[Path], TextIO], ) -> None: """Generates the lines of a River module.""" with open(schema_path) as f: @@ -1103,7 +1106,7 @@ def schema_to_river_client_codegen( ).items(): module_path = Path(target_path).joinpath(subpath) module_path.parent.mkdir(mode=0o755, parents=True, exist_ok=True) - with open(module_path, "w") as f: + with file_opener(module_path) as f: try: popen = subprocess.Popen( ["ruff", "format", "-"], stdin=subprocess.PIPE, stdout=f diff --git a/src/replit_river/codegen/run.py b/src/replit_river/codegen/run.py index 66f04d39..5eac8ff8 100644 --- a/src/replit_river/codegen/run.py +++ b/src/replit_river/codegen/run.py @@ -1,5 +1,7 @@ import argparse import os.path +from pathlib import Path +from typing import TextIO from .client import schema_to_river_client_codegen from .schema import proto_to_river_schema_codegen @@ -50,8 +52,16 @@ def main() -> None: elif args.command == "client": schema_path = os.path.abspath(args.schema) target_path = os.path.abspath(args.output) + + def file_opener(path: Path) -> TextIO: + return open(path, "w") + schema_to_river_client_codegen( - schema_path, target_path, args.client_name, args.typed_dict_inputs + schema_path, + target_path, + args.client_name, + args.typed_dict_inputs, + file_opener, ) else: raise NotImplementedError(f"Unknown command {args.command}") diff --git a/tests/codegen/stream/test_stream.py b/tests/codegen/stream/test_stream.py index 5a9fce55..34e8ffa5 100644 --- a/tests/codegen/stream/test_stream.py +++ b/tests/codegen/stream/test_stream.py @@ -1,6 +1,7 @@ import importlib import shutil -from typing import AsyncIterable +from pathlib import Path +from typing import AsyncIterable, TextIO import pytest @@ -18,11 +19,16 @@ def generate_stream_client() -> None: import tests.codegen.stream.generated shutil.rmtree("tests/codegen/stream/generated") + + def file_opener(path: Path) -> TextIO: + return open(path, "w") + schema_to_river_client_codegen( "tests/codegen/stream/schema.json", "tests/codegen/stream/generated", "StreamClient", True, + file_opener, ) importlib.reload(tests.codegen.stream.generated) diff --git a/tests/codegen/test_rpc.py b/tests/codegen/test_rpc.py index 63f5fd54..885d723a 100644 --- a/tests/codegen/test_rpc.py +++ b/tests/codegen/test_rpc.py @@ -2,6 +2,8 @@ import importlib import shutil from datetime import timedelta +from pathlib import Path +from typing import TextIO import grpc import grpc.aio @@ -20,11 +22,16 @@ def generate_rpc_client() -> None: import tests.codegen.rpc.generated shutil.rmtree("tests/codegen/rpc/generated") + + def file_opener(path: Path) -> TextIO: + return open(path, "w") + schema_to_river_client_codegen( "tests/codegen/rpc/schema.json", "tests/codegen/rpc/generated", "RpcClient", True, + file_opener, ) importlib.reload(tests.codegen.rpc.generated) From 100553747d151720b5768aaaab97db3c1a040c01 Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Thu, 30 Jan 2025 16:02:39 -0800 Subject: [PATCH 04/18] Breaking out "read_schema" as well --- src/replit_river/codegen/client.py | 4 ++-- src/replit_river/codegen/run.py | 2 +- tests/codegen/stream/test_stream.py | 2 +- tests/codegen/test_rpc.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/replit_river/codegen/client.py b/src/replit_river/codegen/client.py index 7b993f19..f75f6a20 100644 --- a/src/replit_river/codegen/client.py +++ b/src/replit_river/codegen/client.py @@ -1092,14 +1092,14 @@ def generate_river_client_module( def schema_to_river_client_codegen( - schema_path: str, + read_schema: Callable[[], TextIO], target_path: str, client_name: str, typed_dict_inputs: bool, file_opener: Callable[[Path], TextIO], ) -> None: """Generates the lines of a River module.""" - with open(schema_path) as f: + with read_schema() as f: schemas = RiverSchemaFile(json.load(f)) for subpath, contents in generate_river_client_module( client_name, schemas.root, typed_dict_inputs diff --git a/src/replit_river/codegen/run.py b/src/replit_river/codegen/run.py index 5eac8ff8..dd041483 100644 --- a/src/replit_river/codegen/run.py +++ b/src/replit_river/codegen/run.py @@ -57,7 +57,7 @@ def file_opener(path: Path) -> TextIO: return open(path, "w") schema_to_river_client_codegen( - schema_path, + lambda: open(schema_path), target_path, args.client_name, args.typed_dict_inputs, diff --git a/tests/codegen/stream/test_stream.py b/tests/codegen/stream/test_stream.py index 34e8ffa5..b093a297 100644 --- a/tests/codegen/stream/test_stream.py +++ b/tests/codegen/stream/test_stream.py @@ -24,7 +24,7 @@ def file_opener(path: Path) -> TextIO: return open(path, "w") schema_to_river_client_codegen( - "tests/codegen/stream/schema.json", + lambda: open("tests/codegen/stream/schema.json"), "tests/codegen/stream/generated", "StreamClient", True, diff --git a/tests/codegen/test_rpc.py b/tests/codegen/test_rpc.py index 885d723a..9c2a5d8e 100644 --- a/tests/codegen/test_rpc.py +++ b/tests/codegen/test_rpc.py @@ -27,7 +27,7 @@ def file_opener(path: Path) -> TextIO: return open(path, "w") schema_to_river_client_codegen( - "tests/codegen/rpc/schema.json", + lambda: open("tests/codegen/rpc/schema.json"), "tests/codegen/rpc/generated", "RpcClient", True, From a1b2f6d3e08677c56d1c866c264ae851f62f65b4 Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Thu, 30 Jan 2025 16:24:41 -0800 Subject: [PATCH 05/18] Make ruff formatting compatible with StringIO --- src/replit_river/codegen/client.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/replit_river/codegen/client.py b/src/replit_river/codegen/client.py index f75f6a20..9cc4ab10 100644 --- a/src/replit_river/codegen/client.py +++ b/src/replit_river/codegen/client.py @@ -1109,9 +1109,12 @@ def schema_to_river_client_codegen( with file_opener(module_path) as f: try: popen = subprocess.Popen( - ["ruff", "format", "-"], stdin=subprocess.PIPE, stdout=f + ["ruff", "format", "-"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, ) - popen.communicate(contents.encode()) + stdout, _ = popen.communicate(contents.encode()) + f.write(stdout.decode("utf-8")) except: f.write(contents) raise From 5116b8cb1879831fb4eebd97eef0679c3c38388f Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Thu, 30 Jan 2025 15:17:16 -0800 Subject: [PATCH 06/18] Adding snapshot testing --- pyproject.toml | 3 ++- uv.lock | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f6db8650..ad0f3aa3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,11 +48,12 @@ dev-dependencies = [ "types-protobuf>=4.24.0.20240311", "types-nanoid>=2.0.0.20240601", "pyright>=1.1.389", + "pytest-snapshot>=0.9.0", ] [tool.ruff] lint.select = ["F", "E", "W", "I001"] -exclude = ["*/generated/*"] +exclude = ["*/generated/*", "*/snapshots/*"] # Should be kept in sync with mypy.ini in the project root. # The VSCode mypy extension can only read /mypy.ini. diff --git a/uv.lock b/uv.lock index b1803e78..bc14a653 100644 --- a/uv.lock +++ b/uv.lock @@ -555,6 +555,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, ] +[[package]] +name = "pytest-snapshot" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/7b/ab8f1fc1e687218aa66acec1c3674d9c443f6a2dc8cb6a50f464548ffa34/pytest-snapshot-0.9.0.tar.gz", hash = "sha256:c7013c3abc3e860f9feff899f8b4debe3708650d8d8242a61bf2625ff64db7f3", size = 19877 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/29/518f32faf6edad9f56d6e0107217f7de6b79f297a47170414a2bd4be7f01/pytest_snapshot-0.9.0-py3-none-any.whl", hash = "sha256:4b9fe1c21c868fe53a545e4e3184d36bc1c88946e3f5c1d9dd676962a9b3d4ab", size = 10715 }, +] + [[package]] name = "replit-river" version = "0.0.0a0" @@ -585,6 +597,7 @@ dev = [ { name = "pytest-cov" }, { name = "pytest-env" }, { name = "pytest-mock" }, + { name = "pytest-snapshot" }, { name = "ruff" }, { name = "types-nanoid" }, { name = "types-protobuf" }, @@ -617,6 +630,7 @@ dev = [ { name = "pytest-cov", specifier = ">=4.1.0" }, { name = "pytest-env", specifier = ">=1.1.5" }, { name = "pytest-mock", specifier = ">=3.11.1" }, + { name = "pytest-snapshot", specifier = ">=0.9.0" }, { name = "ruff", specifier = ">=0.0.278" }, { name = "types-nanoid", specifier = ">=2.0.0.20240601" }, { name = "types-protobuf", specifier = ">=4.24.0.20240311" }, From 54f6e2092ff130a17a432ae7d20797f99b3f2a1c Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Thu, 30 Jan 2025 16:35:15 -0800 Subject: [PATCH 07/18] Adding a snapshot test for unknown enum generation --- .../snapshots/test_unknown_enum/__init__.py | 13 +++ .../test_unknown_enum/enumService/__init__.py | 41 +++++++++ .../enumService/needsEnum.py | 28 +++++++ tests/codegen/snapshot/test_enum.py | 84 +++++++++++++++++++ 4 files changed, 166 insertions(+) create mode 100644 tests/codegen/snapshot/snapshots/test_unknown_enum/__init__.py create mode 100644 tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/__init__.py create mode 100644 tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/needsEnum.py create mode 100644 tests/codegen/snapshot/test_enum.py diff --git a/tests/codegen/snapshot/snapshots/test_unknown_enum/__init__.py b/tests/codegen/snapshot/snapshots/test_unknown_enum/__init__.py new file mode 100644 index 00000000..80b79b04 --- /dev/null +++ b/tests/codegen/snapshot/snapshots/test_unknown_enum/__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 .enumService import EnumserviceService + + +class foo: + def __init__(self, client: river.Client[Literal[None]]): + self.enumService = EnumserviceService(client) diff --git a/tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/__init__.py b/tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/__init__.py new file mode 100644 index 00000000..cc120f3c --- /dev/null +++ b/tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/__init__.py @@ -0,0 +1,41 @@ +# 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 +import replit_river as river + + +from .needsEnum import ( + NeedsenumErrors, + encode_NeedsenumInput, + NeedsenumInput, + NeedsenumOutput, +) + + +class EnumserviceService: + def __init__(self, client: river.Client[Any]): + self.client = client + + async def needsEnum( + self, + input: NeedsenumInput, + timeout: datetime.timedelta, + ) -> NeedsenumOutput: + return await self.client.send_rpc( + "enumService", + "needsEnum", + input, + lambda x: x, + lambda x: TypeAdapter(NeedsenumOutput).validate_python( + x # type: ignore[arg-type] + ), + lambda x: TypeAdapter(NeedsenumErrors).validate_python( + x # type: ignore[arg-type] + ), + timeout, + ) diff --git a/tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/needsEnum.py b/tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/needsEnum.py new file mode 100644 index 00000000..c379f593 --- /dev/null +++ b/tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/needsEnum.py @@ -0,0 +1,28 @@ +# ruff: noqa +# Code generated by river.codegen. DO NOT EDIT. +from collections.abc import AsyncIterable, AsyncIterator +import datetime +from typing import ( + Any, + Callable, + Dict, + List, + Literal, + Optional, + Mapping, + NotRequired, + Union, + Tuple, + TypedDict, +) + +from pydantic import BaseModel, Field, TypeAdapter +from replit_river.error_schema import RiverError + +import replit_river as river + + +NeedsenumInput = Literal["in_first"] | Literal["in_second"] +encode_NeedsenumInput: Callable[["NeedsenumInput"], Any] = lambda x: x +NeedsenumOutput = Literal["out_first"] | Literal["out_second"] +NeedsenumErrors = Literal["err_first"] | Literal["err_second"] diff --git a/tests/codegen/snapshot/test_enum.py b/tests/codegen/snapshot/test_enum.py new file mode 100644 index 00000000..3659d754 --- /dev/null +++ b/tests/codegen/snapshot/test_enum.py @@ -0,0 +1,84 @@ +from io import StringIO +from pathlib import Path +from typing import TextIO + +from pytest_snapshot.plugin import Snapshot + +from replit_river.codegen.client import schema_to_river_client_codegen + +test_unknown_enum_schema: str = """ +{ + "services": { + "enumService": { + "procedures": { + "needsEnum": { + "type": "rpc", + "input": { + "anyOf": [ + { + "type": "string", + "const": "in_first" + }, + { + "type": "string", + "const": "in_second" + } + ] + }, + "output": { + "anyOf": [ + { + "type": "string", + "const": "out_first" + }, + { + "type": "string", + "const": "out_second" + } + ] + }, + "errors": { + "anyOf": [ + { + "type": "string", + "const": "err_first" + }, + { + "type": "string", + "const": "err_second" + } + ] + } + } + } + } + } +} + """ + + +class UnclosableStringIO(StringIO): + def close(self) -> None: + pass + + +def test_unknown_enum(snapshot: Snapshot) -> None: + snapshot.snapshot_dir = "tests/codegen/snapshot/snapshots" + files: dict[Path, UnclosableStringIO] = {} + + def file_opener(path: Path) -> TextIO: + buffer = UnclosableStringIO() + assert path not in files, "Codegen attempted to write to the same file twice!" + files[path] = buffer + return buffer + + schema_to_river_client_codegen( + read_schema=lambda: StringIO(test_unknown_enum_schema), + target_path="test_unknown_enum", + client_name="foo", + file_opener=file_opener, + typed_dict_inputs=True, + ) + for path, file in files.items(): + file.seek(0) + snapshot.assert_match(file.read(), Path(snapshot.snapshot_dir, path)) From 9ea55defc83eae243b51b012ae33b55f55db45a9 Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Thu, 30 Jan 2025 17:09:21 -0800 Subject: [PATCH 08/18] Threading through "permits_unknown_members" --- src/replit_river/codegen/client.py | 38 ++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/replit_river/codegen/client.py b/src/replit_river/codegen/client.py index 9cc4ab10..e611e26a 100644 --- a/src/replit_river/codegen/client.py +++ b/src/replit_river/codegen/client.py @@ -162,6 +162,7 @@ def encode_type( prefix: TypeName, base_model: str, in_module: list[ModuleName], + permit_unknown_members: bool, ) -> Tuple[TypeExpression, list[ModuleName], list[FileContents], set[TypeName]]: encoder_name: Optional[str] = None # defining this up here to placate mypy chunks: List[FileContents] = [] @@ -258,6 +259,7 @@ def flatten_union(tpe: RiverType) -> list[RiverType]: TypeName(f"{pfx}{i}"), base_model, in_module, + permit_unknown_members=permit_unknown_members, ) one_of.append(type_name) chunks.extend(contents) @@ -285,7 +287,11 @@ def flatten_union(tpe: RiverType) -> list[RiverType]: else: oneof_t = oneof_ts[0] type_name, _, contents, _ = encode_type( - oneof_t, TypeName(pfx), base_model, in_module + oneof_t, + TypeName(pfx), + base_model, + in_module, + permit_unknown_members=permit_unknown_members, ) one_of.append(type_name) chunks.extend(contents) @@ -338,7 +344,11 @@ def flatten_union(tpe: RiverType) -> list[RiverType]: typeddict_encoder = [] for i, t in enumerate(type.anyOf): type_name, _, contents, _ = encode_type( - t, TypeName(f"{prefix}AnyOf_{i}"), base_model, in_module + t, + TypeName(f"{prefix}AnyOf_{i}"), + base_model, + in_module, + permit_unknown_members=permit_unknown_members, ) any_of.append(type_name) chunks.extend(contents) @@ -406,6 +416,7 @@ def extract_props(tpe: RiverType) -> list[dict[str, RiverType]]: prefix, base_model, in_module, + permit_unknown_members=permit_unknown_members, ) elif isinstance(type, RiverConcreteType): typeddict_encoder = list[str]() @@ -448,7 +459,11 @@ def extract_props(tpe: RiverType) -> list[dict[str, RiverType]]: return (TypeName("datetime.datetime"), [], [], set()) elif type.type == "array" and type.items: type_name, module_info, type_chunks, encoder_names = encode_type( - type.items, prefix, base_model, in_module + type.items, + prefix, + base_model, + in_module, + permit_unknown_members=permit_unknown_members, ) typeddict_encoder.append("TODO: dstewart") return (ListTypeExpr(type_name), module_info, type_chunks, encoder_names) @@ -462,6 +477,7 @@ def extract_props(tpe: RiverType) -> list[dict[str, RiverType]]: prefix, base_model, in_module, + permit_unknown_members=permit_unknown_members, ) # TODO(dstewart): This structure changed since we were incorrectly leaking # ListTypeExprs into codegen. This generated code is @@ -496,7 +512,11 @@ def extract_props(tpe: RiverType) -> list[dict[str, RiverType]]: ) in sorted(list(type.properties.items()), key=lambda xs: xs[0]): typeddict_encoder.append(f"{repr(name)}:") type_name, _, contents, _ = encode_type( - prop, TypeName(prefix + name.title()), base_model, in_module + prop, + TypeName(prefix + name.title()), + base_model, + in_module, + permit_unknown_members=permit_unknown_members, ) encoder_name = None chunks.extend(contents) @@ -734,6 +754,7 @@ def __init__(self, client: river.Client[Any]): TypeName(f"{name.title()}Init"), input_base_class, module_names, + permit_unknown_members=False, ) serdes.append( ( @@ -747,6 +768,7 @@ def __init__(self, client: river.Client[Any]): TypeName(f"{name.title()}Input"), input_base_class, module_names, + permit_unknown_members=False, ) serdes.append( ( @@ -760,6 +782,7 @@ def __init__(self, client: river.Client[Any]): TypeName(f"{name.title()}Output"), "BaseModel", module_names, + permit_unknown_members=True, ) serdes.append( ( @@ -774,6 +797,7 @@ def __init__(self, client: river.Client[Any]): TypeName(f"{name.title()}Errors"), "RiverError", module_names, + permit_unknown_members=True, ) if error_type == "None": error_type = TypeName("RiverError") @@ -1065,7 +1089,11 @@ def generate_river_client_module( handshake_chunks: list[str] = [] if schema_root.handshakeSchema is not None: _handshake_type, _, contents, _ = encode_type( - schema_root.handshakeSchema, TypeName("HandshakeSchema"), "BaseModel", [] + schema_root.handshakeSchema, + TypeName("HandshakeSchema"), + "BaseModel", + [], + permit_unknown_members=False, ) handshake_chunks.extend(contents) handshake_type = HandshakeType(render_type_expr(_handshake_type)) From 7b6a90b74de2ba322c66c131ded86de0b5db6152 Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Thu, 30 Jan 2025 21:00:35 -0800 Subject: [PATCH 09/18] Restructuring to assert_never --- src/replit_river/codegen/typing.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/replit_river/codegen/typing.py b/src/replit_river/codegen/typing.py index 634068cf..bb0b533d 100644 --- a/src/replit_river/codegen/typing.py +++ b/src/replit_river/codegen/typing.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import NewType +from typing import NewType, assert_never TypeName = NewType("TypeName", str) ModuleName = NewType("ModuleName", str) @@ -45,8 +45,10 @@ def render_type_expr(value: TypeExpression) -> str: return f"Literal[{repr(inner)}]" case UnionTypeExpr(inner): return " | ".join(render_type_expr(x) for x in inner) + case str(name): + return TypeName(name) case other: - return other + assert_never(other) def extract_inner_type(value: TypeExpression) -> TypeName: @@ -61,8 +63,10 @@ def extract_inner_type(value: TypeExpression) -> TypeName: raise ValueError( f"Attempting to extract from a union, currently not possible: {value}" ) + case str(name): + return TypeName(name) case other: - return other + assert_never(other) def ensure_literal_type(value: TypeExpression) -> TypeName: @@ -83,5 +87,7 @@ def ensure_literal_type(value: TypeExpression) -> TypeName: raise ValueError( f"Unexpected expression when expecting a type name: {value}" ) + case str(name): + return TypeName(name) case other: - return other + assert_never(other) From 23c7fd98f1052c776ff87f3eb6304c5e67547eb7 Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Thu, 30 Jan 2025 21:01:00 -0800 Subject: [PATCH 10/18] Adding UnknownTypeExpr --- src/replit_river/codegen/typing.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/replit_river/codegen/typing.py b/src/replit_river/codegen/typing.py index bb0b533d..af9ae7fa 100644 --- a/src/replit_river/codegen/typing.py +++ b/src/replit_river/codegen/typing.py @@ -30,8 +30,18 @@ class UnionTypeExpr: nested: list["TypeExpression"] +@dataclass +class UnknownTypeExpr: + name: TypeName + + TypeExpression = ( - TypeName | DictTypeExpr | ListTypeExpr | LiteralTypeExpr | UnionTypeExpr + TypeName + | DictTypeExpr + | ListTypeExpr + | LiteralTypeExpr + | UnionTypeExpr + | UnknownTypeExpr ) @@ -47,6 +57,8 @@ def render_type_expr(value: TypeExpression) -> str: return " | ".join(render_type_expr(x) for x in inner) case str(name): return TypeName(name) + case UnknownTypeExpr(name): + return TypeName(name) case other: assert_never(other) @@ -65,6 +77,8 @@ def extract_inner_type(value: TypeExpression) -> TypeName: ) case str(name): return TypeName(name) + case UnknownTypeExpr(name): + return name case other: assert_never(other) @@ -89,5 +103,7 @@ def ensure_literal_type(value: TypeExpression) -> TypeName: ) case str(name): return TypeName(name) + case UnknownTypeExpr(name): + return name case other: assert_never(other) From 48bcf1e958de85a71acba33a5bde973b980f4474 Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Thu, 30 Jan 2025 22:24:18 -0800 Subject: [PATCH 11/18] Fixing tests to reflect NotRequired --- tests/codegen/rpc/generated/test_service/rpc_method.py | 1 + tests/codegen/stream/generated/test_service/stream_method.py | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/codegen/rpc/generated/test_service/rpc_method.py b/tests/codegen/rpc/generated/test_service/rpc_method.py index 53a25d2f..ee58db0e 100644 --- a/tests/codegen/rpc/generated/test_service/rpc_method.py +++ b/tests/codegen/rpc/generated/test_service/rpc_method.py @@ -10,6 +10,7 @@ Literal, Optional, Mapping, + NotRequired, Union, Tuple, TypedDict, diff --git a/tests/codegen/stream/generated/test_service/stream_method.py b/tests/codegen/stream/generated/test_service/stream_method.py index 9e9c62b4..a0af6b6c 100644 --- a/tests/codegen/stream/generated/test_service/stream_method.py +++ b/tests/codegen/stream/generated/test_service/stream_method.py @@ -10,6 +10,7 @@ Literal, Optional, Mapping, + NotRequired, Union, Tuple, TypedDict, From f73b1d46324822559325553d93fecb3b01ebf3fa Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Thu, 30 Jan 2025 21:07:01 -0800 Subject: [PATCH 12/18] Adding branches for unknown enumeration members --- src/replit_river/codegen/client.py | 16 ++++++++++++++++ .../rpc/generated/test_service/rpc_method.py | 1 + .../generated/test_service/stream_method.py | 1 + 3 files changed, 18 insertions(+) diff --git a/src/replit_river/codegen/client.py b/src/replit_river/codegen/client.py index e611e26a..e42b8c0e 100644 --- a/src/replit_river/codegen/client.py +++ b/src/replit_river/codegen/client.py @@ -34,6 +34,7 @@ TypeExpression, TypeName, UnionTypeExpr, + UnknownTypeExpr, ensure_literal_type, extract_inner_type, render_type_expr, @@ -82,6 +83,7 @@ Literal, Optional, Mapping, + NewType, NotRequired, Union, Tuple, @@ -309,6 +311,14 @@ def flatten_union(tpe: RiverType) -> list[RiverType]: else """, ) + if permit_unknown_members: + unknown_name = TypeName(f"{prefix}AnyOf__Unknown") + chunks.append( + FileContents( + f"{unknown_name} = NewType({repr(unknown_name)}, object)" + ) + ) + one_of.append(UnknownTypeExpr(unknown_name)) chunks.append( FileContents( f"{prefix} = {render_type_expr(UnionTypeExpr(one_of))}" @@ -375,6 +385,12 @@ def flatten_union(tpe: RiverType) -> list[RiverType]: typeddict_encoder.append( f"encode_{ensure_literal_type(other)}(x)" ) + if permit_unknown_members: + unknown_name = TypeName(f"{prefix}AnyOf__Unknown") + chunks.append( + FileContents(f"{unknown_name} = NewType({repr(unknown_name)}, object)") + ) + any_of.append(UnknownTypeExpr(unknown_name)) if is_literal(type): typeddict_encoder = ["x"] chunks.append( diff --git a/tests/codegen/rpc/generated/test_service/rpc_method.py b/tests/codegen/rpc/generated/test_service/rpc_method.py index ee58db0e..d6dc64f4 100644 --- a/tests/codegen/rpc/generated/test_service/rpc_method.py +++ b/tests/codegen/rpc/generated/test_service/rpc_method.py @@ -10,6 +10,7 @@ Literal, Optional, Mapping, + NewType, NotRequired, Union, Tuple, diff --git a/tests/codegen/stream/generated/test_service/stream_method.py b/tests/codegen/stream/generated/test_service/stream_method.py index a0af6b6c..d66aff55 100644 --- a/tests/codegen/stream/generated/test_service/stream_method.py +++ b/tests/codegen/stream/generated/test_service/stream_method.py @@ -10,6 +10,7 @@ Literal, Optional, Mapping, + NewType, NotRequired, Union, Tuple, From b0a2d06baac84b558456f7f648ddb3b6f33e336e Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Thu, 30 Jan 2025 22:22:45 -0800 Subject: [PATCH 13/18] Adding a structured object enum fallback --- .../test_unknown_enum/enumService/__init__.py | 25 ++++ .../enumService/needsEnum.py | 11 +- .../enumService/needsEnumObject.py | 126 ++++++++++++++++++ tests/codegen/snapshot/test_enum.py | 99 +++++++++++++- 4 files changed, 257 insertions(+), 4 deletions(-) create mode 100644 tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/needsEnumObject.py diff --git a/tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/__init__.py b/tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/__init__.py index cc120f3c..70617dbd 100644 --- a/tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/__init__.py +++ b/tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/__init__.py @@ -15,6 +15,12 @@ NeedsenumInput, NeedsenumOutput, ) +from .needsEnumObject import ( + encode_NeedsenumobjectInput, + NeedsenumobjectOutput, + NeedsenumobjectInput, + NeedsenumobjectErrors, +) class EnumserviceService: @@ -39,3 +45,22 @@ async def needsEnum( ), timeout, ) + + async def needsEnumObject( + self, + input: NeedsenumobjectInput, + timeout: datetime.timedelta, + ) -> NeedsenumobjectOutput: + return await self.client.send_rpc( + "enumService", + "needsEnumObject", + input, + encode_NeedsenumobjectInput, + lambda x: TypeAdapter(NeedsenumobjectOutput).validate_python( + x # type: ignore[arg-type] + ), + lambda x: TypeAdapter(NeedsenumobjectErrors).validate_python( + x # type: ignore[arg-type] + ), + timeout, + ) diff --git a/tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/needsEnum.py b/tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/needsEnum.py index c379f593..4633dfd6 100644 --- a/tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/needsEnum.py +++ b/tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/needsEnum.py @@ -10,6 +10,7 @@ Literal, Optional, Mapping, + NewType, NotRequired, Union, Tuple, @@ -24,5 +25,11 @@ NeedsenumInput = Literal["in_first"] | Literal["in_second"] encode_NeedsenumInput: Callable[["NeedsenumInput"], Any] = lambda x: x -NeedsenumOutput = Literal["out_first"] | Literal["out_second"] -NeedsenumErrors = Literal["err_first"] | Literal["err_second"] +NeedsenumOutputAnyOf__Unknown = NewType("NeedsenumOutputAnyOf__Unknown", object) +NeedsenumOutput = ( + Literal["out_first"] | Literal["out_second"] | NeedsenumOutputAnyOf__Unknown +) +NeedsenumErrorsAnyOf__Unknown = NewType("NeedsenumErrorsAnyOf__Unknown", object) +NeedsenumErrors = ( + Literal["err_first"] | Literal["err_second"] | NeedsenumErrorsAnyOf__Unknown +) diff --git a/tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/needsEnumObject.py b/tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/needsEnumObject.py new file mode 100644 index 00000000..2fe76987 --- /dev/null +++ b/tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/needsEnumObject.py @@ -0,0 +1,126 @@ +# ruff: noqa +# Code generated by river.codegen. DO NOT EDIT. +from collections.abc import AsyncIterable, AsyncIterator +import datetime +from typing import ( + Any, + Callable, + Dict, + List, + Literal, + Optional, + Mapping, + NewType, + NotRequired, + Union, + Tuple, + TypedDict, +) + +from pydantic import BaseModel, Field, TypeAdapter +from replit_river.error_schema import RiverError + +import replit_river as river + + +encode_NeedsenumobjectInputOneOf_in_first: Callable[ + ["NeedsenumobjectInputOneOf_in_first"], Any +] = lambda x: { + k: v + for (k, v) in ( + { + "$kind": x.get("kind"), + "value": x.get("value"), + } + ).items() + if v is not None +} + + +class NeedsenumobjectInputOneOf_in_first(TypedDict): + kind: Literal["in_first"] + value: str + + +encode_NeedsenumobjectInputOneOf_in_second: Callable[ + ["NeedsenumobjectInputOneOf_in_second"], Any +] = lambda x: { + k: v + for (k, v) in ( + { + "$kind": x.get("kind"), + "bleep": x.get("bleep"), + } + ).items() + if v is not None +} + + +class NeedsenumobjectInputOneOf_in_second(TypedDict): + kind: Literal["in_second"] + bleep: int + + +NeedsenumobjectInput = ( + NeedsenumobjectInputOneOf_in_first | NeedsenumobjectInputOneOf_in_second +) + +encode_NeedsenumobjectInput: Callable[["NeedsenumobjectInput"], Any] = ( + lambda x: encode_NeedsenumobjectInputOneOf_in_first(x) + if x["kind"] == "in_first" + else encode_NeedsenumobjectInputOneOf_in_second(x) +) + + +class NeedsenumobjectOutputFooOneOf_out_first(BaseModel): + kind: Literal["out_first"] = Field( + "out_first", + alias="$kind", # type: ignore + ) + + foo: int + + +class NeedsenumobjectOutputFooOneOf_out_second(BaseModel): + kind: Literal["out_second"] = Field( + "out_second", + alias="$kind", # type: ignore + ) + + bar: int + + +NeedsenumobjectOutputFooAnyOf__Unknown = NewType( + "NeedsenumobjectOutputFooAnyOf__Unknown", object +) +NeedsenumobjectOutputFoo = ( + NeedsenumobjectOutputFooOneOf_out_first + | NeedsenumobjectOutputFooOneOf_out_second + | NeedsenumobjectOutputFooAnyOf__Unknown +) + + +class NeedsenumobjectOutput(BaseModel): + foo: Optional[NeedsenumobjectOutputFoo] = None + + +class NeedsenumobjectErrorsFooAnyOf_0(RiverError): + beep: Optional[Literal["err_first"]] = None + + +class NeedsenumobjectErrorsFooAnyOf_1(RiverError): + borp: Optional[Literal["err_second"]] = None + + +NeedsenumobjectErrorsFooAnyOf__Unknown = NewType( + "NeedsenumobjectErrorsFooAnyOf__Unknown", object +) +NeedsenumobjectErrorsFoo = ( + NeedsenumobjectErrorsFooAnyOf_0 + | NeedsenumobjectErrorsFooAnyOf_1 + | NeedsenumobjectErrorsFooAnyOf__Unknown +) + + +class NeedsenumobjectErrors(RiverError): + foo: Optional[NeedsenumobjectErrorsFoo] = None diff --git a/tests/codegen/snapshot/test_enum.py b/tests/codegen/snapshot/test_enum.py index 3659d754..e3ccfc0d 100644 --- a/tests/codegen/snapshot/test_enum.py +++ b/tests/codegen/snapshot/test_enum.py @@ -6,7 +6,7 @@ from replit_river.codegen.client import schema_to_river_client_codegen -test_unknown_enum_schema: str = """ +test_unknown_enum_schema = """ { "services": { "enumService": { @@ -49,12 +49,107 @@ } ] } + }, + "needsEnumObject": { + "type": "rpc", + "input": { + "anyOf": [ + { + "type": "object", + "properties": { + "$kind": { + "const": "in_first", + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": ["$kind", "value"] + }, + { + "type": "object", + "properties": { + "$kind": { + "const": "in_second", + "type": "string" + }, + "bleep": { + "type": "integer" + } + }, + "required": ["$kind", "bleep"] + } + ] + }, + "output": { + "type": "object", + "properties": { + "foo": { + "anyOf": [ + { + "type": "object", + "properties": { + "$kind": { + "const": "out_first", + "type": "string" + }, + "foo": { + "type": "integer" + } + }, + "required": ["$kind", "foo"] + }, + { + "type": "object", + "properties": { + "$kind": { + "const": "out_second", + "type": "string" + }, + "bar": { + "type": "integer" + } + }, + "required": ["$kind", "bar"] + } + ] + } + } + }, + "errors": { + "type": "object", + "properties": { + "foo": { + "anyOf": [ + { + "type": "object", + "properties": { + "beep": { + "type": "string", + "const": "err_first" + } + } + }, + { + "type": "object", + "properties": { + "borp": { + "type": "string", + "const": "err_second" + } + } + } + ] + } + } + } } } } } } - """ +""" class UnclosableStringIO(StringIO): From 655b7ac5cd3ddb95a1cc8127eca3a70771cc472c Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Fri, 31 Jan 2025 15:26:15 -0800 Subject: [PATCH 14/18] Ignore pytest-snapshot missing import See: - https://github.com/python/typeshed/pull/13448 - https://github.com/joseph-roitman/pytest-snapshot/pull/59 --- mypy.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mypy.ini b/mypy.ini index 67a94b67..50822251 100644 --- a/mypy.ini +++ b/mypy.ini @@ -12,5 +12,8 @@ ignore_missing_imports = True [mypy-pyd.*] ignore_missing_imports = True +[mypy-pytest_snapshot.*] +ignore_missing_imports = True + [mypy-tyd.*] ignore_missing_imports = True From 53af10d027c165d8911f6041be972eedaf3cabc2 Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Fri, 31 Jan 2025 16:00:00 -0800 Subject: [PATCH 15/18] Scoping down --- Makefile | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 7b15491d..41959647 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,10 @@ lint: - uv run ruff format --check . - uv run ruff check . - uv run mypy . - uv run pyright-python . - uv run deptry . + uv run ruff format --check src tests + uv run ruff check src tests + uv run mypy src tests + uv run pyright-python src tests + uv run deptry src tests format: - uv run ruff format . - uv run ruff check . --fix + uv run ruff format src tests + uv run ruff check src tests --fix From 3f028f25cb0342ac4a667b5eeeaaad725124b1b0 Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Fri, 31 Jan 2025 16:00:40 -0800 Subject: [PATCH 16/18] Moving msgpack-types to dev deps --- pyproject.toml | 2 +- uv.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ad0f3aa3..acc0b3a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,6 @@ dependencies = [ "aiochannel>=1.2.1", "grpcio-tools>=1.59.3", "grpcio>=1.59.3", - "msgpack-types>=0.3.0", "msgpack>=1.0.7", "nanoid>=2.0.0", "protobuf>=5.28.3", @@ -37,6 +36,7 @@ dependencies = [ [tool.uv] dev-dependencies = [ "deptry>=0.14.0", + "msgpack-types>=0.3.0", "mypy>=1.4.0", "mypy-protobuf>=3.5.0", "pytest>=7.4.0", diff --git a/uv.lock b/uv.lock index bc14a653..ec9229fb 100644 --- a/uv.lock +++ b/uv.lock @@ -28,7 +28,7 @@ name = "click" version = "8.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "platform_system == 'Windows'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } wheels = [ @@ -576,7 +576,6 @@ dependencies = [ { name = "grpcio" }, { name = "grpcio-tools" }, { name = "msgpack" }, - { name = "msgpack-types" }, { name = "nanoid" }, { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, @@ -589,6 +588,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "deptry" }, + { name = "msgpack-types" }, { name = "mypy" }, { name = "mypy-protobuf" }, { name = "pyright" }, @@ -609,7 +609,6 @@ requires-dist = [ { name = "grpcio", specifier = ">=1.59.3" }, { name = "grpcio-tools", specifier = ">=1.59.3" }, { name = "msgpack", specifier = ">=1.0.7" }, - { name = "msgpack-types", specifier = ">=0.3.0" }, { name = "nanoid", specifier = ">=2.0.0" }, { name = "opentelemetry-api", specifier = ">=1.28.2" }, { name = "opentelemetry-sdk", specifier = ">=1.28.2" }, @@ -622,6 +621,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "deptry", specifier = ">=0.14.0" }, + { name = "msgpack-types", specifier = ">=0.3.0" }, { name = "mypy", specifier = ">=1.4.0" }, { name = "mypy-protobuf", specifier = ">=3.5.0" }, { name = "pyright", specifier = ">=1.1.389" }, From 28129a4d2c2e0d98699310a2fc7bc51d992fcdb6 Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Fri, 31 Jan 2025 16:06:25 -0800 Subject: [PATCH 17/18] Sorting members --- src/replit_river/codegen/client.py | 2 +- .../snapshots/test_unknown_enum/enumService/__init__.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/replit_river/codegen/client.py b/src/replit_river/codegen/client.py index e42b8c0e..4c3bc340 100644 --- a/src/replit_river/codegen/client.py +++ b/src/replit_river/codegen/client.py @@ -1080,7 +1080,7 @@ async def {name}( emitted_files[file_path] = FileContents("\n".join([existing] + contents)) rendered_imports = [ - f"from .{dotted_modules} import {', '.join(names)}" + f"from .{dotted_modules} import {', '.join(sorted(names))}" for dotted_modules, names in imports.items() ] diff --git a/tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/__init__.py b/tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/__init__.py index 70617dbd..7477adb8 100644 --- a/tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/__init__.py +++ b/tests/codegen/snapshot/snapshots/test_unknown_enum/enumService/__init__.py @@ -11,15 +11,15 @@ from .needsEnum import ( NeedsenumErrors, - encode_NeedsenumInput, NeedsenumInput, NeedsenumOutput, + encode_NeedsenumInput, ) from .needsEnumObject import ( - encode_NeedsenumobjectInput, - NeedsenumobjectOutput, - NeedsenumobjectInput, NeedsenumobjectErrors, + NeedsenumobjectInput, + NeedsenumobjectOutput, + encode_NeedsenumobjectInput, ) From b7349ad189f1d11d4b5ec6b62c55c7f69eaebfef Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Fri, 31 Jan 2025 16:06:33 -0800 Subject: [PATCH 18/18] Sorting these too --- src/replit_river/codegen/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/replit_river/codegen/client.py b/src/replit_river/codegen/client.py index 4c3bc340..c58f745c 100644 --- a/src/replit_river/codegen/client.py +++ b/src/replit_river/codegen/client.py @@ -723,7 +723,7 @@ def generate_common_client( chunks.extend( [ f"from .{model_name} import {class_name}" - for model_name, class_name in modules + for model_name, class_name in sorted(modules, key=lambda kv: kv[1]) ] ) chunks.extend(handshake_chunks)