Skip to content

Commit f24cfe2

Browse files
Preserve original type in extend_and_specialize (#1063)
Force the `ref_resolver` logic to retain the original type when applying the `extend` logic to Schema SALAD objects, in order to allow for inheritance-based typing in record fields. This update also allows for parent-based loader naming, allowing to generalize the `_loaders` logic into a single type-based dictionary.
1 parent 4d95aa7 commit f24cfe2

9 files changed

Lines changed: 306 additions & 399 deletions

File tree

src/schema_salad/metaschema.py

Lines changed: 147 additions & 308 deletions
Large diffs are not rendered by default.

src/schema_salad/python_codegen.py

Lines changed: 99 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from importlib.resources import files
66
from io import StringIO
77
from types import ModuleType
8-
from typing import Final, Any, IO
8+
from typing import Final, Any, IO, cast
99

1010
try:
1111
black: ModuleType | None
@@ -95,15 +95,14 @@ def __init__(
9595
super().__init__()
9696
self.out: Final = out
9797
self.current_class_is_abstract = False
98-
self.current_class_is_inherited = False
99-
self.current_field_loaders: dict[str, str] = {}
10098
self.serializer = StringIO()
10199
self.idfield = ""
102100
self.copyright: Final = copyright
103101
self.parents_map: Final = parents_map or {}
104102
self.parser_info: Final = parser_info
105103
self.salad_version: Final = salad_version
106104
self.inherited_classes: dict[str, str] = {}
105+
self.dynamic_loaders: set[str] = set()
107106

108107
@staticmethod
109108
def safe_name(name: str) -> str:
@@ -189,12 +188,9 @@ def begin_class(
189188
idfield: str,
190189
optional_fields: set[str],
191190
) -> None:
192-
self.current_field_loaders = {}
193191
if (classname := self.safe_name(classname)) in self.inherited_classes:
194-
self.current_class_is_inherited = True
192+
self.current_class_is_abstract = True
195193
return
196-
else:
197-
self.current_class_is_inherited = False
198194
self.current_class_is_abstract = abstract
199195

200196
if extends:
@@ -304,8 +300,7 @@ def fromDoc(
304300
doc: Any,
305301
baseuri: str,
306302
loadingOptions: LoadingOptions,
307-
loaders: Mapping[str, Loader],
308-
docRoot: str | None = None
303+
docRoot: str | None = None,
309304
) -> Self:
310305
_doc = copy.copy(doc)
311306
@@ -336,19 +331,6 @@ def end_class(self, classname: str, field_names: list[str]) -> None:
336331
if self.current_class_is_abstract:
337332
return
338333

339-
self.add_lazy_init(
340-
LazyInitDef(
341-
self.safe_name(classname) + "FieldLoaders",
342-
"{}.update({{{}}})".format(
343-
self.safe_name(classname) + "FieldLoaders",
344-
", ".join(f'"{k}": {v}' for k, v in self.current_field_loaders.items()),
345-
),
346-
)
347-
)
348-
349-
if self.current_class_is_inherited:
350-
return
351-
352334
self.out.write(
353335
fmt(
354336
"""
@@ -444,28 +426,77 @@ def type_loader(
444426
"_UnionLoader(({},))".format(", ".join(sub_names1)),
445427
)
446428
)
447-
case {"type": "array" | "https://w3id.org/cwl/salad#array", "items": items}:
429+
case {"type": "array" | "https://w3id.org/cwl/salad#array", "items": items, **rest}:
448430
i1: Final = self.type_loader(items)
449-
return self.declare_type(
450-
TypeDef(
451-
f"array_of_{i1.name}",
452-
f"_ArrayLoader({i1.name})",
431+
if "original_items" in rest:
432+
original_type = self.safe_name(shortname(cast(str, rest["original_items"])))
433+
self.declare_type(
434+
TypeDef(
435+
f"{original_type}Loader",
436+
i1.name,
437+
abstract=True,
438+
)
439+
)
440+
self.declare_type(
441+
TypeDef(
442+
f"{original_type}ProxyLoader",
443+
'_ProxyLoader("{}")'.format(original_type + "Loader"),
444+
)
445+
)
446+
return self.declare_type(
447+
TypeDef(
448+
f"array_of_{original_type}",
449+
f"_ArrayLoader({original_type}ProxyLoader)",
450+
)
451+
)
452+
else:
453+
return self.declare_type(
454+
TypeDef(
455+
f"array_of_{i1.name}",
456+
f"_ArrayLoader({i1.name})",
457+
)
453458
)
454-
)
455459
case {"type": "map" | "https://w3id.org/cwl/salad#map", "values": values, **rest}:
456460
i2: Final = self.type_loader(values)
457461
name = self.safe_name(str(rest["name"])) if "name" in rest else None
458-
anon_type = self.declare_type(
459-
TypeDef(
460-
f"map_of_{i2.name}",
461-
"_MapLoader({}, {}, {}, {})".format(
462+
if "original_values" in rest:
463+
original_type = self.safe_name(shortname(cast(str, rest["original_values"])))
464+
self.declare_type(
465+
TypeDef(
466+
original_type + "Loader",
462467
i2.name,
463-
f"'{name}'", # noqa: B907
464-
f"'{container}'" if container is not None else None, # noqa: B907
465-
no_link_check,
466-
),
468+
abstract=True,
469+
)
470+
)
471+
self.declare_type(
472+
TypeDef(
473+
f"{original_type}ProxyLoader",
474+
'_ProxyLoader("{}")'.format(original_type + "Loader"),
475+
)
476+
)
477+
anon_type = self.declare_type(
478+
TypeDef(
479+
f"map_of_{original_type}",
480+
"_MapLoader({}, {}, {}, {})".format(
481+
f"{original_type}ProxyLoader",
482+
f"'{name}'", # noqa: B907
483+
f"'{container}'" if container is not None else None, # noqa: B907
484+
no_link_check,
485+
),
486+
)
487+
)
488+
else:
489+
anon_type = self.declare_type(
490+
TypeDef(
491+
f"map_of_{i2.name}",
492+
"_MapLoader({}, {}, {}, {})".format(
493+
i2.name,
494+
f"'{name}'", # noqa: B907
495+
f"'{container}'" if container is not None else None, # noqa: B907
496+
no_link_check,
497+
),
498+
)
467499
)
468-
)
469500
if "name" in rest:
470501
return self.declare_type(
471502
TypeDef(self.safe_name(str(rest["name"])) + "Loader", anon_type.name)
@@ -504,25 +535,31 @@ def type_loader(
504535
classname = self.safe_name(name)
505536
if (prefix := name.split("#")[0]) in self.parents_map:
506537
self.inherited_classes[classname] = f"{self.parents_map[prefix]}.{classname}"
507-
self.declare_type(
508-
TypeDef(
509-
classname + "FieldLoaders",
510-
"{}",
511-
instance_type="MutableMapping[str, Loader]",
538+
if rest.get("abstract", False):
539+
self.declare_type(
540+
TypeDef(
541+
classname + "Loader",
542+
"None",
543+
abstract=True,
544+
)
512545
)
513-
)
514-
return self.declare_type(
515-
TypeDef(
516-
classname + "Loader",
517-
"_RecordLoader({}, {}, {}, {})".format(
518-
self.inherited_classes.get(classname, classname),
519-
classname + "FieldLoaders",
520-
f"'{container}'" if container is not None else None, # noqa: B907
521-
no_link_check,
522-
),
523-
abstract=bool(rest.get("abstract", False)),
546+
return self.declare_type(
547+
TypeDef(
548+
classname + "ProxyLoader",
549+
'_ProxyLoader("{}")'.format(classname + "Loader"),
550+
)
551+
)
552+
else:
553+
return self.declare_type(
554+
TypeDef(
555+
classname + "Loader",
556+
"_RecordLoader({}, {}, {})".format(
557+
self.inherited_classes.get(classname, classname),
558+
f"'{container}'" if container is not None else None, # noqa: B907
559+
no_link_check,
560+
),
561+
)
524562
)
525-
)
526563

527564
case {
528565
"type": "union" | "https://w3id.org/cwl/salad#union",
@@ -573,9 +610,6 @@ def declare_id_field(
573610

574611
self.declare_field(name, fieldtype, doc, True, "")
575612

576-
if self.current_class_is_inherited:
577-
return
578-
579613
if optional:
580614
opt = """{safename} = "_:" + str(_uuid__.uuid4())""".format(
581615
safename=self.safe_name(name)
@@ -607,11 +641,6 @@ def declare_field(
607641
if self.current_class_is_abstract:
608642
return
609643

610-
self.current_field_loaders[shortname(name)] = fieldtype.name
611-
612-
if self.current_class_is_inherited:
613-
return
614-
615644
if optional:
616645
self.out.write(f""" {self.safe_name(name)} = None\n""")
617646
self.out.write(f""" if "{shortname(name)}" in _doc:\n""") # noqa: B907
@@ -644,14 +673,15 @@ def declare_field(
644673
self.out.write(
645674
"""{spc} {safename} = _load_field(
646675
{spc} _doc.get("{fieldname}"),
647-
{spc} loaders["{fieldname}"],
676+
{spc} {fieldtype},
648677
{spc} {baseurivar},
649678
{spc} loadingOptions,
650679
{spc} lc=_doc.get("{fieldname}")
651680
{spc} )
652681
""".format(
653682
safename=self.safe_name(name),
654683
fieldname=shortname(name),
684+
fieldtype=fieldtype.name,
655685
baseurivar=baseurivar,
656686
spc=spc,
657687
)
@@ -851,6 +881,12 @@ def epilogue(self, root_loader: TypeDef) -> None:
851881
self.out.write(fmt(f"{collected_type.name}: {type_} = {collected_type.init}\n", 0))
852882
self.out.write("\n")
853883

884+
self.out.write("_loaders.update({\n")
885+
for _, collected_type in self.collected_types.items():
886+
if collected_type.abstract:
887+
self.out.write(f' "{collected_type.name}": {collected_type.init},\n')
888+
self.out.write("})\n\n")
889+
854890
if self.lazy_inits:
855891
for lazy_init in self.lazy_inits.values():
856892
self.out.write(fmt(f"{lazy_init.init}\n", 0))

src/schema_salad/python_codegen_support.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
import copy
6-
from collections.abc import MutableSequence, Sequence, MutableMapping, Mapping
6+
from collections.abc import MutableSequence, Sequence, MutableMapping
77
from io import StringIO
88
from itertools import chain
99
from typing import Any, Final, cast, Generic
@@ -22,6 +22,7 @@
2222
from schema_salad.sourceline import SourceLine, add_lc_filename
2323
from schema_salad.utils import yaml_no_ts # requires schema-salad v8.2+
2424

25+
_loaders: Final[dict[str, Loader | None]] = {}
2526
_vocab: Final[dict[str, str]] = {}
2627
_rvocab: Final[dict[str, str]] = {}
2728

@@ -258,12 +259,10 @@ class _RecordLoader(Loader, Generic[SaveableType]):
258259
def __init__(
259260
self,
260261
classtype: type[SaveableType],
261-
loaders: Mapping[str, Loader],
262262
container: str | None = None,
263263
no_link_check: bool | None = None,
264264
) -> None:
265265
self.classtype: Final = classtype
266-
self.loaders: Final = loaders
267266
self.container: Final = container
268267
self.no_link_check: Final = no_link_check
269268

@@ -284,7 +283,7 @@ def load(
284283
loadingOptions = LoadingOptions(
285284
copyfrom=loadingOptions, container=self.container, no_link_check=self.no_link_check
286285
)
287-
return self.classtype.fromDoc(doc, baseuri, loadingOptions, self.loaders, docRoot=docRoot)
286+
return self.classtype.fromDoc(doc, baseuri, loadingOptions, docRoot=docRoot)
288287

289288
def __repr__(self) -> str:
290289
return str(self.classtype.__name__)
@@ -605,6 +604,29 @@ def load(
605604
return self.inner.load(doc, baseuri, loadingOptions, lc=lc)
606605

607606

607+
class _ProxyLoader(Loader):
608+
def __init__(self, name: str) -> None:
609+
self.name: Final = name
610+
611+
def load(
612+
self,
613+
doc: Any,
614+
baseuri: str,
615+
loadingOptions: LoadingOptions,
616+
docRoot: str | None = None,
617+
lc: Any | None = None,
618+
) -> Any | None:
619+
if (
620+
self.name in loadingOptions.loaders
621+
and (loader := loadingOptions.loaders.get(self.name)) is not None
622+
):
623+
return loader.load(doc, baseuri, loadingOptions, lc=lc)
624+
elif self.name in _loaders and (loader := _loaders.get(self.name)) is not None:
625+
return loader.load(doc, baseuri, loadingOptions, lc=lc)
626+
else:
627+
raise ValidationException(f"No Loader instance available for {self.name}")
628+
629+
608630
def _document_load(
609631
loader: Loader,
610632
doc: str | MutableMapping[str, Any] | MutableSequence[Any],
@@ -637,6 +659,7 @@ def _document_load(
637659
schemas=doc.get("$schemas", None),
638660
baseuri=doc.get("$base", None),
639661
addl_metadata=addl_metadata,
662+
loaders=_loaders | loadingOptions.loaders,
640663
)
641664

642665
doc2: Final = copy.copy(doc)

src/schema_salad/runtime.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import tempfile
1414
import xml.sax # nosec
1515
from abc import ABCMeta, abstractmethod
16-
from collections.abc import MutableMapping, MutableSequence, Mapping
16+
from collections.abc import MutableMapping, MutableSequence
1717
from typing import Any, Final, TypeAlias, cast, TypeVar
1818
from urllib.parse import quote, urlparse, urlsplit
1919
from urllib.request import pathname2url
@@ -67,6 +67,7 @@ class LoadingOptions:
6767
includes: Final[list[str]]
6868
no_link_check: Final[bool | None]
6969
container: Final[str | None]
70+
loaders: Final[dict[str, Loader | None]]
7071

7172
def __init__(
7273
self,
@@ -83,6 +84,7 @@ def __init__(
8384
includes: list[str] | None = None,
8485
no_link_check: bool | None = None,
8586
container: str | None = None,
87+
loaders: dict[str, Loader | None] | None = None,
8688
) -> None:
8789
"""Create a LoadingOptions object."""
8890
self.original_doc = original_doc
@@ -147,6 +149,12 @@ def __init__(
147149
temp_container = copyfrom.container if copyfrom is not None else None
148150
self.container = temp_container
149151

152+
if loaders is not None:
153+
loaders = loaders
154+
else:
155+
loaders = copyfrom.loaders if copyfrom is not None else {}
156+
self.loaders = loaders
157+
150158
if fetcher is not None:
151159
temp_fetcher = fetcher
152160
elif copyfrom is not None:
@@ -216,7 +224,6 @@ def fromDoc(
216224
_doc: Any,
217225
baseuri: str,
218226
loadingOptions: LoadingOptions,
219-
loaders: Mapping[str, Loader],
220227
docRoot: str | None = None,
221228
) -> Self:
222229
"""Construct this object from the result of yaml.load()."""

0 commit comments

Comments
 (0)