Skip to content

Commit dc915e7

Browse files
committed
use signature for mapping of dataclass de/serialization
1 parent ed35f51 commit dc915e7

File tree

3 files changed

+67
-28
lines changed

3 files changed

+67
-28
lines changed

robotcode/language_server/common/lsp_types.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
from enum import Enum, IntEnum, IntFlag
55
from typing import Any, Dict, Iterator, List, Literal, Optional, Tuple, Union
66

7-
from ...utils.dataclasses import to_camel_case, to_snake_case, HasCaseEncoder, HasCaseDecoder
7+
from ...utils.dataclasses import to_camel_case, to_snake_case
88

99
ProgressToken = Union[str, int]
1010
DocumentUri = str
1111
URI = str
1212

1313

1414
@dataclass
15-
class Model(HasCaseEncoder, HasCaseDecoder):
15+
class Model:
1616
@classmethod
1717
def _encode_case(cls, s: str) -> str:
1818
return to_camel_case(s)

robotcode/utils/dataclasses.py

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
# pyright: reportMissingTypeArgument=true, reportMissingParameterType=true
12
import dataclasses
23
import enum
4+
import inspect
35
import json
46
import re
57
from typing import (
@@ -149,6 +151,7 @@ def convert_value(value: Any, types: Union[Type[_T], Tuple[Type[_T], ...], None]
149151
match: Optional[Type[_T]] = None
150152
match_same_keys: Optional[List[str]] = None
151153
match_value: Optional[Dict[str, Any]] = None
154+
match_signature: Optional[inspect.Signature] = None
152155
match_type_hints: Optional[Dict[str, Any]] = None
153156

154157
for t in types:
@@ -159,21 +162,40 @@ def convert_value(value: Any, types: Union[Type[_T], Tuple[Type[_T], ...], None]
159162
_get_config(t, HasCaseDecoder)._decode_case(k): v for k, v in value.items() # type: ignore
160163
}
161164
type_hints = get_type_hints(origin or t)
162-
same_keys = [k for k in cased_value.keys() if k in type_hints]
163-
different_keys = [k for k in cased_value.keys() if k not in type_hints]
164-
if same_keys and not different_keys:
165-
if match_same_keys is None or len(match_same_keys) < len(same_keys):
166-
match_same_keys = same_keys
167-
match = t
168-
match_value = cased_value
169-
match_type_hints = type_hints
170-
elif match_same_keys is not None and len(match_same_keys) == len(same_keys):
171-
raise TypeError(
172-
f"Value {repr(value)} matches to more then one types of "
173-
f"{repr(types[0].__name__) if len(types)==1 else ' | '.join(repr(e.__name__) for e in types)}."
174-
)
175-
176-
if match is not None and match_value is not None and match_type_hints is not None:
165+
try:
166+
signature = inspect.signature(origin or t)
167+
except ValueError:
168+
continue
169+
170+
same_keys = [k for k in cased_value.keys() if k in signature.parameters.keys()]
171+
if not same_keys:
172+
continue
173+
174+
different_keys = [k for k in cased_value.keys() if k not in signature.parameters.keys()]
175+
if different_keys:
176+
continue
177+
178+
if not all(k in same_keys for k, v in signature.parameters.items() if v.default == inspect.Parameter.empty):
179+
continue
180+
181+
if match_same_keys is None or len(match_same_keys) < len(same_keys):
182+
match_same_keys = same_keys
183+
match = t
184+
match_value = cased_value
185+
match_signature = signature
186+
match_type_hints = type_hints
187+
elif match_same_keys is not None and len(match_same_keys) == len(same_keys):
188+
raise TypeError(
189+
f"Value {repr(value)} matches to more then one types of "
190+
f"{repr(types[0].__name__) if len(types)==1 else ' | '.join(repr(e.__name__) for e in types)}."
191+
)
192+
193+
if (
194+
match is not None
195+
and match_value is not None
196+
and match_signature is not None
197+
and match_type_hints is not None
198+
):
177199
params: Dict[str, Any] = {k: convert_value(v, match_type_hints[k]) for k, v in match_value.items()}
178200
try:
179201
return match(**params) # type: ignore

tests/robotcode/utils/test_dataclasses.py

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1+
# flake8: noqa E501
12
from dataclasses import dataclass
23
from enum import Enum
34
from typing import Any, Dict, List, Optional, Union
45

56
import pytest
67

7-
from robotcode.utils.dataclasses import as_json, from_json, to_camel_case, to_snake_case
8-
98
from robotcode.language_server.common.lsp_types import (
109
CallHierarchyClientCapabilities,
1110
ClientCapabilities,
@@ -80,6 +79,7 @@
8079
WorkspaceSymbolClientCapabilitiesSymbolKind,
8180
WorkspaceSymbolClientCapabilitiesTagSupport,
8281
)
82+
from robotcode.utils.dataclasses import as_json, from_json, to_camel_case, to_snake_case
8383

8484

8585
class EnumData(Enum):
@@ -316,23 +316,40 @@ def test_decode_with_union_and_some_same_keys() -> None:
316316

317317
def test_decode_with_union_and_same_keys_should_raise_typeerror() -> None:
318318
with pytest.raises(TypeError):
319-
from_json(
320-
'{"a_union_field": {"a": 1, "b":2}}', ComplexItemWithUnionTypeWithSameProperties
321-
) == ComplexItemWithUnionTypeWithSameProperties(SimpleItem2(1, 2, 3))
319+
from_json('{"a_union_field": {"a": 1, "b":2}}', ComplexItemWithUnionTypeWithSameProperties)
322320

323321

324322
def test_decode_with_union_and_no_keys_should_raise_typeerror() -> None:
325323
with pytest.raises(TypeError):
326-
from_json(
327-
'{"a_union_field": {}}', ComplexItemWithUnionTypeWithSameProperties
328-
) == ComplexItemWithUnionTypeWithSameProperties(SimpleItem2(1, 2, 3))
324+
from_json('{"a_union_field": {}}', ComplexItemWithUnionTypeWithSameProperties)
329325

330326

331327
def test_decode_with_union_and_no_match_should_raise_typeerror() -> None:
332328
with pytest.raises(TypeError):
333-
from_json(
334-
'{"a_union_field": {"x": 1, "y":2}}', ComplexItemWithUnionTypeWithSameProperties
335-
) == ComplexItemWithUnionTypeWithSameProperties(SimpleItem2(1, 2, 3))
329+
from_json('{"a_union_field": {"x": 1, "y":2}}', ComplexItemWithUnionTypeWithSameProperties)
330+
331+
332+
@dataclass
333+
class SimpleItem3:
334+
a: int
335+
b: int
336+
c: int
337+
338+
339+
@pytest.mark.parametrize(
340+
("expr", "type", "expected"),
341+
[
342+
('{"a":1, "b": 2}', (SimpleItem, SimpleItem3), SimpleItem(1, 2)),
343+
('{"a":1, "b": 2, "c": 3}', (SimpleItem, SimpleItem3), SimpleItem3(1, 2, 3)),
344+
],
345+
)
346+
def test_decode_with_some_same_fields(expr: Any, type: Any, expected: str) -> None:
347+
assert from_json(expr, type) == expected
348+
349+
350+
def test_decode_with_some_unambigous_fields_should_raise_typeerror() -> None:
351+
with pytest.raises(TypeError):
352+
from_json('{"a":1, "b": 2}', (SimpleItem, SimpleItem2)) # type: ignore
336353

337354

338355
@dataclass

0 commit comments

Comments
 (0)