Skip to content

Commit a0269d5

Browse files
committed
Add support of 'Self'
1 parent 5285a28 commit a0269d5

File tree

4 files changed

+102
-12
lines changed

4 files changed

+102
-12
lines changed

src/implicitdict/__init__.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@
44
from dataclasses import dataclass
55
from datetime import datetime as datetime_type
66
from types import UnionType
7-
from typing import Literal, Optional, Union, get_args, get_origin, get_type_hints # pyright:ignore[reportDeprecated]
7+
from typing import ( # pyright:ignore[reportDeprecated]
8+
Literal,
9+
Optional,
10+
Self,
11+
Union,
12+
get_args,
13+
get_origin,
14+
get_type_hints,
15+
)
816

917
import arrow
1018
import pytimeparse
@@ -101,7 +109,7 @@ def parse(cls, source: dict, parse_type: type):
101109
if key in hints:
102110
# This entry has an explicit type
103111
try:
104-
kwargs[key] = _parse_value(value, hints[key])
112+
kwargs[key] = _parse_value(value, hints[key], parse_type)
105113
except _PARSING_ERRORS as e:
106114
raise _bubble_up_parse_error(e, key)
107115
else:
@@ -175,7 +183,7 @@ def has_field_with_value(self, field_name: str) -> bool:
175183
return field_name in self and self[field_name] is not None
176184

177185

178-
def _parse_value(value, value_type: type):
186+
def _parse_value(value, value_type: type, root_type: type):
179187
generic_type = get_origin(value_type)
180188
if generic_type:
181189
# Type is generic
@@ -192,7 +200,7 @@ def _parse_value(value, value_type: type):
192200
result = []
193201
for i, v in enumerate(value_list):
194202
try:
195-
result.append(_parse_value(v, arg_types[0]))
203+
result.append(_parse_value(v, arg_types[0], root_type))
196204
except _PARSING_ERRORS as e:
197205
raise _bubble_up_parse_error(e, f"[{i}]")
198206
return result
@@ -201,9 +209,9 @@ def _parse_value(value, value_type: type):
201209
# value is a dict of some kind
202210
result = {}
203211
for k, v in value.items():
204-
parsed_key = k if arg_types[0] is str else _parse_value(k, arg_types[0])
212+
parsed_key = k if arg_types[0] is str else _parse_value(k, arg_types[0], root_type)
205213
try:
206-
parsed_value = _parse_value(v, arg_types[1])
214+
parsed_value = _parse_value(v, arg_types[1], root_type)
207215
except _PARSING_ERRORS as e:
208216
raise _bubble_up_parse_error(e, k)
209217
result[parsed_key] = parsed_value
@@ -220,7 +228,7 @@ def _parse_value(value, value_type: type):
220228
# omitting the field's value
221229
return None
222230
else:
223-
return _parse_value(value, arg_types[0])
231+
return _parse_value(value, arg_types[0], root_type)
224232

225233
elif generic_type is Literal and len(arg_types) == 1:
226234
# Type is a Literal (parsed value must match specified value)
@@ -231,12 +239,15 @@ def _parse_value(value, value_type: type):
231239
else:
232240
raise ValueError(f"Automatic parsing of {value_type} type is not yet implemented")
233241

242+
elif value_type == Self:
243+
# value is outself type
244+
return ImplicitDict.parse(value, root_type)
234245
elif issubclass(value_type, ImplicitDict):
235246
# value is an ImplicitDict
236247
return ImplicitDict.parse(value, value_type)
237248

238249
if hasattr(value_type, "__orig_bases__") and value_type.__orig_bases__:
239-
return value_type(_parse_value(value, value_type.__orig_bases__[0]))
250+
return value_type(_parse_value(value, value_type.__orig_bases__[0], root_type))
240251

241252
else:
242253
# value is a non-generic type that is not an ImplicitDict

src/implicitdict/jsonschema.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from dataclasses import dataclass
77
from datetime import datetime
88
from types import UnionType
9-
from typing import Literal, TypeAlias, Union, cast, get_args, get_origin, get_type_hints
9+
from typing import Literal, Self, TypeAlias, Union, cast, get_args, get_origin, get_type_hints
1010

1111
from . import ImplicitDict, StringBasedDateTime, StringBasedTimeDelta, _fullname, _get_fields
1212

@@ -170,6 +170,12 @@ def _schema_for(
170170

171171
schema_vars = schema_vars_resolver(value_type)
172172

173+
if value_type == Self:
174+
if not schema_vars.path_to:
175+
raise NotImplementedError(f"SchemaVarsResolver for {value_type} didn't returned a path_to function")
176+
177+
return {"$ref": schema_vars.path_to(context, context)}, False
178+
173179
if issubclass(value_type, ImplicitDict):
174180
make_json_schema(value_type, schema_vars_resolver, schema_repository)
175181

tests/test_jsonschema.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from typing import Self
23

34
import jsonschema
45

@@ -8,11 +9,13 @@
89

910
from .test_types import (
1011
ContainerData,
12+
HiddenReferencingSelf,
1113
InheritanceData,
1214
NestedDefinitionsData,
1315
NormalUsageData,
1416
OptionalData,
1517
PropertiesData,
18+
ReferencingSelf,
1619
SpecialSubclassesContainer,
1720
SpecialTypesData,
1821
)
@@ -28,10 +31,26 @@ def path_to(t_dest: type, t_src: type) -> str:
2831

2932

3033
def _verify_schema_validation(obj, obj_type: type[ImplicitDict]) -> None:
34+
def _root_resolver(t: type) -> SchemaVars:
35+
"""Special resolver that references '#' for Self at root of the schema"""
36+
37+
if t == Self:
38+
39+
def path_to(t_dest: type, t_src: type) -> str:
40+
if t_src == obj_type:
41+
return "#"
42+
else:
43+
return "#/definitions/" + t_dest.__module__ + t_dest.__qualname__
44+
45+
full_name = t.__module__ + t.__qualname__
46+
return SchemaVars(name=full_name, path_to=path_to)
47+
48+
return _resolver(t)
49+
3150
repo = {}
32-
implicitdict.jsonschema.make_json_schema(obj_type, _resolver, repo)
51+
implicitdict.jsonschema.make_json_schema(obj_type, _root_resolver, repo)
3352

34-
name = _resolver(obj_type).name
53+
name = _root_resolver(obj_type).name
3554
schema = repo[name]
3655
del repo[name]
3756
if repo:
@@ -113,3 +132,13 @@ def test_special_types():
113132
def test_nested_definitions():
114133
data = NestedDefinitionsData.example_value()
115134
_verify_schema_validation(data, NestedDefinitionsData)
135+
136+
137+
def test_self():
138+
data = ReferencingSelf.example_value()
139+
_verify_schema_validation(data, ReferencingSelf)
140+
141+
142+
def test_hidden_self():
143+
data = HiddenReferencingSelf.example_value()
144+
_verify_schema_validation(data, HiddenReferencingSelf)

tests/test_types.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# comments)
33
import enum
44
from datetime import UTC, datetime
5-
from typing import List, Optional # noqa UP035
5+
from typing import List, Optional, Self # noqa UP035
66

77
from implicitdict import ImplicitDict, StringBasedDateTime, StringBasedTimeDelta
88

@@ -210,3 +210,47 @@ def example_value():
210210
},
211211
NestedDefinitionsData,
212212
)
213+
214+
215+
class ReferencingSelf(ImplicitDict):
216+
foo: str
217+
bar: Self | None
218+
219+
@staticmethod
220+
def example_value():
221+
return ImplicitDict.parse(
222+
{
223+
"foo": "foo",
224+
"bar": {
225+
"foo": "subfoo",
226+
},
227+
},
228+
ReferencingSelf,
229+
)
230+
231+
232+
class HiddenReferencingSelf(ImplicitDict):
233+
baz: ReferencingSelf
234+
bazs: list[ReferencingSelf]
235+
236+
@staticmethod
237+
def example_value():
238+
return ImplicitDict.parse(
239+
{
240+
"baz": {
241+
"foo": "foo",
242+
"bar": {
243+
"foo": "subfoo",
244+
},
245+
},
246+
"bazs": [
247+
{
248+
"foo": "foo",
249+
"bar": {
250+
"foo": "subfoo",
251+
},
252+
}
253+
],
254+
},
255+
HiddenReferencingSelf,
256+
)

0 commit comments

Comments
 (0)