Skip to content

Commit c47f0a1

Browse files
committed
Migrate from apischema to cattrs, because apischema doesn't support Python 3.14 and is not actively maintained.
1 parent f92465a commit c47f0a1

15 files changed

Lines changed: 113 additions & 100 deletions

File tree

finecode_extension_runner/pyproject.toml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,19 @@ dynamic = ["version"]
44
description = "Extension runner component for FineCode"
55
authors = [{ name = "Vladyslav Hnatiuk", email = "aders1234@gmail.com" }]
66
readme = "README.md"
7-
requires-python = ">=3.11, <= 3.14"
7+
requires-python = ">=3.11, < 3.15"
88
dependencies = [
99
"loguru==0.7.*",
1010
"click==8.1.*",
1111
"finecode_extension_api~=0.4.0a0",
1212
"deepmerge==2.0.*",
1313
"debugpy==1.8.*",
1414
"ordered-set==4.1.*",
15-
"apischema==0.19.*",
15+
"cattrs>=24.1",
1616
]
1717

1818
[dependency-groups]
19-
# file_python_import_linter is temporary disabled, because it isn't ported to the new finecode_extension_api yet
20-
# "fine_python_import_linter @ git+https://github.com/finecode-dev/finecode.git#subdirectory=extensions/fine_python_import_linter"
2119
dev_workspace = [
22-
"build==1.2.2.post1",
2320
"finecode~=0.4.0a0",
2421
"finecode_dev_common_preset~=0.3.0a0",
2522
]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import cattrs
2+
3+
converter = cattrs.Converter()

finecode_extension_runner/src/finecode_extension_runner/_services/merge_results.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import dataclasses
22

3-
import apischema
43
from loguru import logger
54

65
from finecode_extension_api import code_action
76
from finecode_extension_runner import global_state, run_utils
7+
from finecode_extension_runner._converter import converter as _converter
88

99

1010
async def merge_results(action_name: str, results: list[dict]) -> dict:
@@ -37,7 +37,7 @@ async def merge_results(action_name: str, results: list[dict]) -> dict:
3737

3838
merged: code_action.RunActionResult | None = None
3939
for result_dict in non_empty:
40-
typed = apischema.deserialize(result_type, result_dict)
40+
typed = _converter.structure(result_dict, result_type)
4141
if merged is None:
4242
merged = typed
4343
else:

finecode_extension_runner/src/finecode_extension_runner/_services/run_action.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
import time
66
import typing
77

8-
import apischema
8+
import cattrs
99
import deepmerge
1010
from loguru import logger
1111

12+
from finecode_extension_runner._converter import converter as _converter
13+
1214
from finecode_extension_api import code_action, textstyler, service
1315
from finecode_extension_api.interfaces import iprojectactionrunner
1416
from finecode_extension_runner import (
@@ -620,7 +622,7 @@ async def run_action_raw(
620622
if action_exec_info.payload_type is not None:
621623
payload = typing.cast(
622624
code_action.RunActionPayload,
623-
apischema.deserialize(action_exec_info.payload_type, request.params),
625+
_converter.structure(request.params, action_exec_info.payload_type),
624626
)
625627

626628
wal_run_id = getattr(options, "wal_run_id", None)
@@ -754,7 +756,7 @@ async def run_handlers_raw(
754756
if action_exec_info.payload_type is not None and request.params:
755757
payload = typing.cast(
756758
code_action.RunActionPayload,
757-
apischema.deserialize(action_exec_info.payload_type, request.params),
759+
_converter.structure(request.params, action_exec_info.payload_type),
758760
)
759761

760762
wal_run_id = getattr(options, "wal_run_id", None)
@@ -906,8 +908,8 @@ async def ensure_handler_instantiated(
906908

907909
def get_handler_config(param_type):
908910
try:
909-
return apischema.deserialize(param_type, handler_raw_config)
910-
except apischema.ValidationError as exception:
911+
return _converter.structure(handler_raw_config, param_type)
912+
except cattrs.ClassValidationError as exception:
911913
raise ActionFailedException(str(exception)) from exception
912914

913915
def get_process_executor(param_type):
@@ -1090,9 +1092,9 @@ def get_run_context(param_type):
10901092
if stream_result is None:
10911093
stream_result = typing.cast(
10921094
code_action.RunActionResult,
1093-
apischema.deserialize(
1095+
_converter.structure(
1096+
_converter.unstructure(partial_result),
10941097
type(partial_result),
1095-
dataclasses.asdict(partial_result),
10961098
),
10971099
)
10981100
else:
@@ -1205,10 +1207,7 @@ async def run_subresult_coros_concurrently(
12051207
action_subresult_dict = dataclasses.asdict(coro_result)
12061208
action_subresult = typing.cast(
12071209
code_action.RunActionResult,
1208-
apischema.deserialize(
1209-
action_subresult_type,
1210-
action_subresult_dict,
1211-
),
1210+
_converter.structure(action_subresult_dict, action_subresult_type),
12121211
)
12131212
else:
12141213
action_subresult.update(coro_result)

finecode_extension_runner/src/finecode_extension_runner/er_server.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import threading
2626
import typing
2727

28-
import apischema
2928
from loguru import logger
3029
from lsprotocol import converters as lsp_converters
3130
from lsprotocol import types
@@ -34,6 +33,7 @@
3433
from finecode_extension_api import code_action
3534
from finecode_extension_api.interfaces import ifileeditor
3635
from finecode_extension_runner import er_wal, global_state, schemas, services
36+
from finecode_extension_runner._converter import converter as _converter
3737
from finecode_extension_runner._services import merge_results as merge_results_service
3838
from finecode_extension_runner._services import run_action as run_action_service
3939
from finecode_extension_runner.di import resolver
@@ -446,9 +446,8 @@ async def run_action(server: ErServer, params: dict | None) -> dict:
446446
)
447447

448448
request = schemas.RunActionRequest(action_name=action_name, params=action_params)
449-
options_schema = apischema.deserialize(
450-
schemas.RunActionOptions,
451-
options if options is not None else {},
449+
options_schema = _converter.structure(
450+
options if options is not None else {}, schemas.RunActionOptions
452451
)
453452
status: str = "success"
454453

@@ -553,9 +552,8 @@ async def run_handlers(server: ErServer, params: dict | None) -> dict:
553552
params=action_params,
554553
previous_result=previous_result,
555554
)
556-
options_schema = apischema.deserialize(
557-
schemas.RunActionOptions,
558-
options if options is not None else {},
555+
options_schema = _converter.structure(
556+
options if options is not None else {}, schemas.RunActionOptions
559557
)
560558
status: str = "success"
561559

finecode_extension_runner/src/finecode_extension_runner/impls/project_action_runner.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
import typing
77
from typing import Any, Awaitable, Callable
88

9-
import apischema
109
from loguru import logger
1110

1211
from finecode_extension_api import code_action
1312
from finecode_extension_api.interfaces import iprojectactionrunner
1413
from finecode_extension_runner import domain, run_utils
14+
from finecode_extension_runner._converter import converter as _converter
1515

1616

1717
PayloadT = typing.TypeVar("PayloadT", bound=code_action.RunActionPayload)
@@ -117,7 +117,7 @@ def _build_result(
117117
) -> ResultT:
118118
return typing.cast(
119119
ResultT,
120-
apischema.deserialize(action_type.RESULT_TYPE, raw_result),
120+
_converter.structure(raw_result, action_type.RESULT_TYPE),
121121
)
122122

123123
async def run_action(

finecode_extension_runner/src/finecode_extension_runner/impls/workspace_action_runner.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
import typing
66
from typing import Any, Awaitable, Callable
77

8-
import apischema
98
from finecode_extension_api import code_action
109
from finecode_extension_api.interfaces import iworkspaceactionrunner
10+
from finecode_extension_runner._converter import converter as _converter
1111

1212
PayloadT = typing.TypeVar("PayloadT", bound=code_action.RunActionPayload)
1313
ResultT = typing.TypeVar("ResultT", bound=code_action.RunActionResult)
@@ -46,8 +46,8 @@ async def run_action_in_projects(
4646
)
4747
results_by_project: dict = raw["resultsByProject"]
4848
return {
49-
pathlib.Path(k): apischema.deserialize(
50-
action_type.RESULT_TYPE, next(iter(v.values()), {}) # type: ignore[attr-defined]
49+
pathlib.Path(k): _converter.structure(
50+
next(iter(v.values()), {}), action_type.RESULT_TYPE
5151
)
5252
for k, v in results_by_project.items()
5353
}

finecode_jsonrpc/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ version = "0.1.0a1"
44
description = "JSON-RPC client implementation for FineCode"
55
authors = [{ name = "Vladyslav Hnatiuk", email = "aders1234@gmail.com" }]
66
readme = "README.md"
7-
requires-python = ">=3.11, <= 3.14"
7+
requires-python = ">=3.11, < 3.15"
88
dependencies = [
99
"loguru==0.7.*",
1010
"culsans==0.11.*",
11-
"apischema==0.19.*",
11+
"cattrs>=24.1",
1212
"finecode_extension_api~=0.4.0a0",
1313
]
1414

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import dataclasses
2+
3+
import cattrs
4+
from cattrs.gen import make_dict_structure_fn, make_dict_unstructure_fn, override
5+
6+
7+
def _to_camel(name: str) -> str:
8+
parts = name.split("_")
9+
return parts[0] + "".join(p.title() for p in parts[1:])
10+
11+
12+
def _camel_structure_fn(cls, conv):
13+
overrides = {
14+
f.name: override(rename=_to_camel(f.name))
15+
for f in dataclasses.fields(cls)
16+
if _to_camel(f.name) != f.name
17+
}
18+
return make_dict_structure_fn(cls, conv, **overrides)
19+
20+
21+
def _camel_unstructure_fn(cls, conv):
22+
overrides = {
23+
f.name: override(rename=_to_camel(f.name))
24+
for f in dataclasses.fields(cls)
25+
if _to_camel(f.name) != f.name
26+
}
27+
return make_dict_unstructure_fn(cls, conv, **overrides)
28+
29+
30+
_is_dc = lambda t: dataclasses.is_dataclass(t) and isinstance(t, type)
31+
32+
converter = cattrs.Converter()
33+
converter.register_structure_hook_factory(_is_dc, _camel_structure_fn)
34+
converter.register_unstructure_hook_factory(_is_dc, _camel_unstructure_fn)

finecode_jsonrpc/src/finecode_jsonrpc/client.py

Lines changed: 18 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
import uuid
1818
from pathlib import Path
1919

20-
import apischema
20+
import cattrs
2121
import culsans
22+
from finecode_jsonrpc._converter import converter as _converter
2223
from finecode_jsonrpc import _io_thread
2324
from loguru import logger
2425

@@ -370,9 +371,7 @@ def notify(self, method: str, params: typing.Any | None = None) -> None:
370371
raise ValueError(f"Type of notification params for {method} not found")
371372

372373
if notification_params_type is not None:
373-
notification_params_dict = apischema.serialize(
374-
notification_params_type, params, aliaser=apischema.utils.to_camel_case
375-
)
374+
notification_params_dict = _converter.unstructure(params)
376375
else:
377376
notification_params_dict = None
378377

@@ -408,9 +407,7 @@ def send_request_sync(
408407
)
409408

410409
if request_params_type is not None:
411-
request_params_dict = apischema.serialize(
412-
request_params_type, params, aliaser=apischema.utils.to_camel_case
413-
)
410+
request_params_dict = _converter.unstructure(params)
414411
else:
415412
request_params_dict = None
416413

@@ -469,9 +466,7 @@ async def send_request(
469466
)
470467

471468
if request_params_type is not None:
472-
request_params_dict = apischema.serialize(
473-
request_params_type, params, aliaser=apischema.utils.to_camel_case
474-
)
469+
request_params_dict = _converter.unstructure(params)
475470
else:
476471
request_params_dict = None
477472

@@ -567,13 +562,9 @@ async def handle_message(self, message: dict[str, typing.Any]) -> None:
567562
return
568563

569564
try:
570-
response_error = apischema.deserialize(
571-
ResponseError,
572-
data=message["error"],
573-
aliaser=apischema.utils.to_camel_case,
574-
)
575-
except apischema.ValidationError as error:
576-
exception = InvalidResponse(". ".join(error.messages))
565+
response_error = _converter.structure(message["error"], ResponseError)
566+
except cattrs.ClassValidationError as error:
567+
exception = InvalidResponse(str(error))
577568

578569
# avoid race condition: request is sent, then cancelled and the server
579570
# sends the response before processing the cancel notification
@@ -609,10 +600,8 @@ async def handle_message(self, message: dict[str, typing.Any]) -> None:
609600
return
610601

611602
try:
612-
request = apischema.deserialize(
613-
request_type, message, aliaser=apischema.utils.to_camel_case
614-
)
615-
except apischema.ValidationError as error:
603+
request = _converter.structure(message, request_type)
604+
except cattrs.ClassValidationError as error:
616605
# Invalid request parameters - send 'Invalid params' error
617606
logger.warning(
618607
f"Invalid params for method {message.get('method')}: {error.messages} | {self.readable_id}"
@@ -666,10 +655,8 @@ async def handle_message(self, message: dict[str, typing.Any]) -> None:
666655

667656
result_type = self._expected_result_type_by_msg_id[message_id]
668657
try:
669-
response = apischema.deserialize(
670-
result_type, message, aliaser=apischema.utils.to_camel_case
671-
)
672-
except apischema.ValidationError as error:
658+
response = _converter.structure(message, result_type)
659+
except cattrs.ClassValidationError as error:
673660
logger.error("errro")
674661
logger.exception(error)
675662
exception = InvalidResponse(". ".join(error.messages))
@@ -714,10 +701,8 @@ async def handle_message(self, message: dict[str, typing.Any]) -> None:
714701

715702
try:
716703
notification_type = self.message_types[method][0]
717-
notification = apischema.deserialize(
718-
notification_type, message, aliaser=apischema.utils.to_camel_case
719-
)
720-
except (KeyError, apischema.ValidationError) as error:
704+
notification = _converter.structure(message, notification_type)
705+
except (KeyError, cattrs.ClassValidationError) as error:
721706
logger.warning(
722707
f"Failed to deserialize notification {method}: {error} | {self.readable_id}"
723708
)
@@ -741,9 +726,7 @@ async def run_feature_impl(
741726
response_dict = {
742727
"jsonrpc": self.VERSION,
743728
"id": message_id,
744-
"result": apischema.serialize(
745-
result_type, result, aliaser=apischema.utils.to_camel_case
746-
),
729+
"result": _converter.unstructure(result),
747730
}
748731

749732
response_str = json.dumps(response_dict)
@@ -1223,13 +1206,9 @@ async def read_messages_from_reader(
12231206
continue
12241207

12251208
try:
1226-
result = apischema.deserialize(
1227-
result_type,
1228-
raw_result,
1229-
aliaser=apischema.utils.to_camel_case,
1230-
)
1231-
except apischema.ValidationError as error:
1232-
exception = InvalidResponse(". ".join(error.messages))
1209+
result = _converter.structure(raw_result, result_type)
1210+
except cattrs.ClassValidationError as error:
1211+
exception = InvalidResponse(str(error))
12331212
if not future.cancelled():
12341213
future.set_exception(exception)
12351214
continue

0 commit comments

Comments
 (0)