Skip to content

Commit 92e84a4

Browse files
committed
converters: Pass contexts to all load/dump operations
The API version determines how data is loaded/dumped, so it needs to be available or all the load/dump methods. Currently the naucse API version is two numbers, expected to increase linearly, but I could imagine e.g. branching versioning in the future (for example with "preview feature" tags, like what GitHub does in its API). That's why I don't use "version" directly, but wrap it in an abstract "context". The top-level dump() function now requires a version, and creates a context. The load() methods should not be called top-level, but always via the top-level load() function (which reads the API version from passed-in data, and creates a context). Adjust models and tests.
1 parent 86f8ab8 commit 92e84a4

7 files changed

Lines changed: 122 additions & 79 deletions

File tree

naucse/converters.py

Lines changed: 67 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,12 @@ class BaseConverter:
8080
def _naucse__converter(self):
8181
return self
8282

83-
def load(self, data, **kwargs):
83+
def load(self, data, context, **kwargs):
8484
"""Convert a JSON-compatible data to a Python value.
8585
86+
`context` is a `LoadContext`, which holds options (like the API
87+
version) for loading an entire tree of objects.
88+
8689
`kwargs` are extra keyword arguments passed to `__init__`.
8790
The Converter's `load_arg_names` attribute specifies which kwargs
8891
are supported.
@@ -91,9 +94,12 @@ def load(self, data, **kwargs):
9194
"""
9295
return data
9396

94-
def dump(self, value):
97+
def dump(self, value, context):
9598
"""Convert a Python value to JSON-compatible data.
9699
100+
`context` is a `DumpContext`, which holds options (like the API
101+
version) for dumping an entire tree of objects.
102+
97103
The base implementation returns `value` unchanged.
98104
"""
99105
return value
@@ -125,15 +131,15 @@ def get_schema(self, context):
125131

126132

127133
class IntegerConverter(BaseConverter):
128-
def load(self, data):
134+
def load(self, data, context):
129135
return int(data)
130136

131137
def get_schema(self, context):
132138
return {'type': 'integer'}
133139

134140

135141
class FloatConverter(BaseConverter):
136-
def load(self, data):
142+
def load(self, data, context):
137143
return float(data)
138144

139145
def get_schema(self, context):
@@ -173,16 +179,16 @@ def __init__(self, item_converter, *, index_arg=None):
173179
self.load_arg_names = self.item_converter.load_arg_names
174180
self.index_arg = index_arg
175181

176-
def load(self, data, **kwargs):
182+
def load(self, data, context, **kwargs):
177183
result = []
178184
for index, d in enumerate(data):
179185
if self.index_arg:
180186
kwargs[self.index_arg] = index
181-
result.append(self.item_converter.load(d, **kwargs))
187+
result.append(self.item_converter.load(d, context, **kwargs))
182188
return result
183189

184-
def dump(self, value):
185-
return [self.item_converter.dump(v) for v in value]
190+
def dump(self, value, context):
191+
return [self.item_converter.dump(v, context) for v in value]
186192

187193
def get_schema(self, context):
188194
return {
@@ -207,16 +213,19 @@ def __init__(self, item_converter, *, key_arg=None, required=()):
207213
self.key_arg = key_arg
208214
self.required = required
209215

210-
def load(self, data, **kwargs):
216+
def load(self, data, context, **kwargs):
211217
result = {}
212218
for k, v in data.items():
213219
if self.key_arg:
214220
kwargs[self.key_arg] = k
215-
result[k] = self.item_converter.load(v, **kwargs)
221+
result[k] = self.item_converter.load(v, context, **kwargs)
216222
return result
217223

218-
def dump(self, value):
219-
return {str(k): self.item_converter.dump(v) for k, v in value.items()}
224+
def dump(self, value, context):
225+
return {
226+
str(k): self.item_converter.dump(v, context)
227+
for k, v in value.items()
228+
}
220229

221230
def get_schema(self, context):
222231
schema = {
@@ -243,17 +252,17 @@ def __init__(self, item_converter, *, key_attr, index_arg=None):
243252
self.index_arg = index_arg
244253
self.load_arg_names = set(self.item_converter.load_arg_names)
245254

246-
def load(self, data, **kwargs):
255+
def load(self, data, context, **kwargs):
247256
result = {}
248257
for index, value in enumerate(data):
249258
if self.index_arg:
250259
kwargs[self.index_arg] = index
251-
item = self.item_converter.load(value, **kwargs)
260+
item = self.item_converter.load(value, context, **kwargs)
252261
result[getattr(item, self.key_attr)] = item
253262
return result
254263

255-
def dump(self, value):
256-
return [self.item_converter.dump(v) for k, v in value.items()]
264+
def dump(self, value, context):
265+
return [self.item_converter.dump(v, context) for k, v in value.items()]
257266

258267
def get_schema(self, context):
259268
return {
@@ -327,7 +336,7 @@ def __set_name__(self, cls, name):
327336
self.name = name
328337
self.data_key = self.data_key or self.name
329338

330-
def load_into(self, instance, data, **kwargs):
339+
def load_into(self, instance, data, context, **kwargs):
331340
"""Load this field's data into the given Python object.
332341
333342
`instance` is the Python object being initialized.
@@ -357,10 +366,10 @@ def load_into(self, instance, data, **kwargs):
357366
n: v for n, v in kwargs.items()
358367
if n in self.converter.load_arg_names
359368
}
360-
value = self.converter.load(item_data, **kwargs)
369+
value = self.converter.load(item_data, context, **kwargs)
361370
setattr(instance, self.name, value)
362371
for func in self._after_load_hooks:
363-
func(instance)
372+
func(instance, context)
364373

365374
def _get_default(self, instance):
366375
"""Return the default value (for optional fields).
@@ -369,7 +378,7 @@ def _get_default(self, instance):
369378
"""
370379
return None
371380

372-
def dump_into(self, instance, data):
381+
def dump_into(self, instance, data, context):
373382
"""Dump the given Python object into the given JSON-compatible dict
374383
375384
If the field is not marked `output`, or is optional and has the default
@@ -380,7 +389,7 @@ def dump_into(self, instance, data):
380389
value = getattr(instance, self.name)
381390
if self.optional and value == self.default:
382391
return
383-
data[self.data_key] = self.converter.dump(value)
392+
data[self.data_key] = self.converter.dump(value, context)
384393

385394
def put_schema_into(self, object_schema, context):
386395
if context.is_input and not self.input:
@@ -462,16 +471,16 @@ def __init__(
462471
def __repr__(self):
463472
return f'<{_classname(type(self))} for {_classname(self.cls)}>'
464473

465-
def load(self, data, **kwargs):
474+
def load(self, data, context, **kwargs):
466475
result = self.cls(**kwargs)
467476
for field in self.fields.values():
468-
field.load_into(result, data, parent=result)
477+
field.load_into(result, data, context, parent=result)
469478
return result
470479

471-
def dump(self, value):
480+
def dump(self, value, context):
472481
result = {}
473482
for field in self.fields.values():
474-
field.dump_into(value, result)
483+
field.dump_into(value, result, context)
475484
return result
476485

477486
def get_schema(self, context):
@@ -487,16 +496,37 @@ def get_schema(self, context):
487496
return schema
488497

489498

499+
class LoadContext:
500+
"""Holds "global" options for loading data
501+
502+
`version` is the API version, as a tuple of ints (major, minor).
503+
"""
504+
def __init__(self, version):
505+
self.version = version
506+
507+
508+
class DumpContext:
509+
"""Holds "global" options for dumping data
510+
511+
`version` is the API version, as a tuple of ints (major, minor).
512+
"""
513+
def __init__(self, version):
514+
self.version = version
515+
516+
490517
class SchemaContext:
491518
"""Holds "global" definitions and options for getting a context
492519
493520
`is_input` determines whether schema for input (data from forks) or output
494521
(naucse's exported API).
522+
523+
`version` is the API version, as a tuple of ints (major, minor).
495524
"""
496-
def __init__(self, *, is_input):
525+
def __init__(self, *, is_input, version):
497526
self.definition_refs = {}
498527
self.definitions = {}
499528
self.is_input = is_input
529+
self.version = version
500530

501531
def get_schema(self, converter):
502532
"""Get schema for the given converter
@@ -516,10 +546,10 @@ def get_schema(self, converter):
516546
return converter.get_schema(self)
517547

518548

519-
def get_schema(converter, *, is_input):
549+
def get_schema(converter, *, is_input, version):
520550
"""Get schema for the given converter"""
521551
converter = get_converter(converter)
522-
context = SchemaContext(is_input=is_input)
552+
context = SchemaContext(is_input=is_input, version=version)
523553
context.definitions.update({
524554
'ref': {
525555
'type': 'object',
@@ -575,7 +605,7 @@ def _get_schema_url(converter, instance):
575605
raise ValueError(f"{converter}.get_schema_url is None")
576606

577607

578-
def dump(instance, converter=None):
608+
def dump(instance, converter=None, *, version):
579609
"""Dump a Python object
580610
581611
If converter is None, the default is used.
@@ -584,23 +614,26 @@ def dump(instance, converter=None):
584614
converter = get_converter(instance)
585615
converter = get_converter(converter)
586616
slug = converter.slug or 'data'
617+
context = DumpContext(version=version)
587618
result = {
588-
'api_version': [0, 0],
589-
slug: converter.dump(instance),
619+
'api_version': context.version,
620+
slug: converter.dump(instance, context),
590621
}
591622
result['$schema'] = _get_schema_url(converter, instance)
592-
schema = get_schema(converter, is_input=False)
623+
schema = get_schema(converter, is_input=False, version=context.version)
593624
jsonschema.validate(result, schema)
594625
return result
595626

596627

597628
def load(converter, data, **kwargs):
598629
"""Load a Python object from the given data"""
630+
version = data['api_version']
599631
converter = get_converter(converter)
600-
schema = get_schema(converter, is_input=True)
632+
context = LoadContext(version=version)
633+
schema = get_schema(converter, is_input=True, version=context.version)
601634
jsonschema.validate(data, schema)
602635
slug = converter.slug or 'data'
603-
return converter.load(data[slug], **kwargs)
636+
return converter.load(data[slug], context, **kwargs)
604637

605638

606639
def register_model(cls, converter=None):

0 commit comments

Comments
 (0)