Skip to content

Commit c5b3740

Browse files
authored
Pruning extended classes (#255)
* Pruning extended classes * Removed marshmallow from children * Removed unneeded parameter * Removed dead code * Updating only non-existing keys
1 parent ecac851 commit c5b3740

File tree

5 files changed

+150
-76
lines changed

5 files changed

+150
-76
lines changed

oarepo_model_builder/entrypoints.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,17 @@ def load_model(
128128
"oarepo_model_builder.loaders.extend", None
129129
),
130130
},
131+
post_reference_processors={
132+
ModelSchema.REF_KEYWORD: load_entry_points_list(
133+
"oarepo_model_builder.loaders.post.ref", None
134+
),
135+
ModelSchema.USE_KEYWORD: load_entry_points_list(
136+
"oarepo_model_builder.loaders.post.use", None
137+
),
138+
ModelSchema.EXTEND_KEYWORD: load_entry_points_list(
139+
"oarepo_model_builder.loaders.post.extend", None
140+
),
141+
},
131142
)
132143
for config in configs:
133144
load_config(schema, config, loaders)
Lines changed: 101 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
from oarepo_model_builder.schema import ModelSchema
2-
from oarepo_model_builder.utils.python_name import package_name
3-
from oarepo_model_builder.validation import InvalidModelException
42

53

64
def extract_extended_record(included_data, *, context, **kwargs):
@@ -32,70 +30,28 @@ def extract_extended_record(included_data, *, context, **kwargs):
3230

3331
def extend_modify_marshmallow(included_data, *, context, **kwargs):
3432
"""
35-
This processor modified marshmallow of the extended object. At first, it puts
36-
marshmallow and ui back to the included data. Then, for the top-level marshmallow & ui.marshmallow
37-
it converts class -> base-classes and adds import for that.
38-
For the properties, it marks them as read=False and write=False and for each object, it marks it as
39-
generate=False - this way, the classes will be reused from the already existing library and not
40-
generated again.
33+
This processor moves the marshmallow section of the base record to base-class-marshmallow
34+
and base-class-ui-marshmallow. It also sets the from-base-class flag to True.
4135
"""
4236

43-
def remove_marshmallow_from_children(node):
37+
def mark_as_from_base_class(node):
4438
ret = {**node}
4539
node_properties = ret.pop("properties", None)
4640
node_items = ret.pop("items", None)
47-
48-
if "marshmallow" in ret:
49-
convert_marshmallow_class_to_base_class(ret["marshmallow"])
50-
elif "properties" in ret:
51-
raise InvalidModelException(
52-
f"marshmallow section not in object {node}. "
53-
f"Please pass generated model (records.json5), not the source model."
54-
)
55-
56-
if "ui" in ret and "marshmallow" in ret["ui"]:
57-
convert_marshmallow_class_to_base_class(ret["ui"]["marshmallow"])
58-
elif "properties" in ret:
59-
raise InvalidModelException(
60-
f"ui.marshmallow section not in object {node}. "
61-
f"Please pass generated model (records.json5), not the source model."
62-
)
41+
ret["from-base-class"] = True
6342

6443
if node_properties:
6544
properties = ret.setdefault("properties", {})
6645
for k, v in node_properties.items():
67-
remove_marshmallow_from_child(v)
68-
v = remove_marshmallow_from_children(v)
46+
v = mark_as_from_base_class(v)
6947
properties[k] = v
7048
if node_items:
71-
remove_marshmallow_from_child(node.item)
72-
ret["items"] = remove_marshmallow_from_children(node.item)
73-
return ret
74-
75-
def remove_marshmallow_from_child(child):
76-
# for object/nested, do not set the read & write to False because
77-
# the extending schema might add more properties.
78-
# This will generate unnecessary classes, but these might be dealt
79-
# on later in marshmallow generator
80-
if "properties" not in child and "items" not in child:
81-
marshmallow = child.setdefault("marshmallow", {})
82-
marshmallow.update({"read": False, "write": False})
83-
84-
ui_marshmallow = child.setdefault("ui", {}).setdefault("marshmallow", {})
85-
ui_marshmallow.update({"read": False, "write": False})
86-
87-
def convert_marshmallow_class_to_base_class(marshmallow):
88-
# pop module & package
89-
marshmallow.pop("module", None)
90-
marshmallow.pop("package", None)
91-
92-
if "class" not in marshmallow:
93-
return
94-
clz = marshmallow.pop("class")
95-
marshmallow.setdefault("base-classes", []).insert(0, clz)
96-
marshmallow.setdefault("imports", []).append(
97-
{"import": package_name(clz), "alias": package_name(clz)}
49+
ret["items"] = mark_as_from_base_class(node.item)
50+
ret["base-class-marshmallow"] = ret.pop("marshmallow", {})
51+
ret["base-class-ui-marshmallow"] = ret.setdefault("ui", {}).pop(
52+
"marshmallow", {}
9853
)
54+
return ret
9955

10056
def as_array(x):
10157
if isinstance(x, list):
@@ -124,7 +80,7 @@ def replace_use_with_extend(data):
12480

12581
included_data["marshmallow"] = context["props"].get("marshmallow", {})
12682
included_data["ui"] = context["props"].get("ui", {})
127-
ret = remove_marshmallow_from_children(included_data)
83+
ret = mark_as_from_base_class(included_data)
12884

12985
for ext in (
13086
ModelSchema.EXTEND_KEYWORD,
@@ -136,3 +92,93 @@ def replace_use_with_extend(data):
13692

13793
replace_use_with_extend(ret)
13894
return ret
95+
96+
97+
def post_extend_modify_marshmallow(*, element, **kwargs):
98+
def convert_schema_classes(node):
99+
node_properties = node.get("properties", None)
100+
node_items = node.get("items", None)
101+
102+
was_inherited = "from-base-class" in node
103+
if not was_inherited:
104+
return False
105+
106+
contains_only_inherited_properties = node.pop("from-base-class", False)
107+
if node_properties:
108+
for k, v in node_properties.items():
109+
prop_contains_only_inherited_properties = convert_schema_classes(v)
110+
if not prop_contains_only_inherited_properties:
111+
contains_only_inherited_properties = False
112+
elif node_items:
113+
contains_only_inherited_properties = (
114+
convert_schema_classes(node_items)
115+
and contains_only_inherited_properties
116+
)
117+
base_class_marshmallow = node.pop("base-class-marshmallow", {})
118+
base_class_ui_marshmallow = node.pop("base-class-ui-marshmallow", {})
119+
120+
def update_marshmallow(new_marshmallow, base_marshmallow):
121+
if new_marshmallow.get("generate", True) is False:
122+
# the class is set to not generate -> if there is a class, do not change it,
123+
# if not, set it to the base class
124+
if not new_marshmallow.get("class") and base_marshmallow.get("class"):
125+
new_marshmallow["class"] = base_marshmallow["class"]
126+
return
127+
128+
if "items" in node:
129+
# array itself does not have a marshmallow, so no need to modify this
130+
_update_non_existing(new_marshmallow, base_marshmallow)
131+
return
132+
133+
if "properties" not in node:
134+
# primitive data type -> set it not to be generated unless the field says otherwise
135+
if "read" not in new_marshmallow:
136+
new_marshmallow["read"] = False
137+
if "write" not in new_marshmallow:
138+
new_marshmallow["write"] = False
139+
for k, v in base_marshmallow.items():
140+
if k not in new_marshmallow:
141+
new_marshmallow[k] = v
142+
return
143+
144+
# now we have an object to modify - convert to base classes only if there are extra properties
145+
convert_to_base_classes = (
146+
node_properties and not contains_only_inherited_properties
147+
)
148+
149+
if "class" in new_marshmallow:
150+
# someone added class to the new_marshmallow, so we do not want to change it
151+
convert_to_base_classes = True
152+
153+
if convert_to_base_classes:
154+
if base_marshmallow.get("class"):
155+
new_marshmallow.setdefault("base-classes", []).insert(
156+
0, base_marshmallow["class"]
157+
)
158+
new_marshmallow["generate"] = True
159+
160+
elif contains_only_inherited_properties:
161+
# keep the base class marshmallow, but do not generate the class as it has been generated
162+
# in the extended library
163+
new_marshmallow.clear()
164+
new_marshmallow.update(base_marshmallow)
165+
new_marshmallow["generate"] = False
166+
167+
else:
168+
_update_non_existing(new_marshmallow, base_marshmallow)
169+
170+
update_marshmallow(node.setdefault("marshmallow", {}), base_class_marshmallow)
171+
update_marshmallow(
172+
node.setdefault("ui", {}).setdefault("marshmallow", {}),
173+
base_class_ui_marshmallow,
174+
)
175+
176+
return contains_only_inherited_properties
177+
178+
convert_schema_classes(element)
179+
180+
181+
def _update_non_existing(target, source):
182+
for k, v in source.items():
183+
if k not in target:
184+
target[k] = v

oarepo_model_builder/schema.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def __init__(
2626
validate=True,
2727
source_locations=None,
2828
reference_processors=None,
29+
post_reference_processors=None,
2930
):
3031
"""
3132
Creates and parses model schema
@@ -48,7 +49,15 @@ def __init__(
4849
self.USE_KEYWORD: [],
4950
self.EXTEND_KEYWORD: [],
5051
},
51-
reference_processors,
52+
reference_processors or {},
53+
)
54+
self._post_reference_processors = deepmerge(
55+
{
56+
self.REF_KEYWORD: [],
57+
self.USE_KEYWORD: [],
58+
self.EXTEND_KEYWORD: [],
59+
},
60+
post_reference_processors or {},
5261
)
5362

5463
if content is not None:
@@ -234,6 +243,13 @@ def _load_and_merge_reference(self, element, key, name, stack):
234243
context=context,
235244
)
236245
deepmerge(element, included_data, [], listmerge="keep")
246+
for rp in self._post_reference_processors[key]:
247+
rp(
248+
element=element,
249+
key=key,
250+
name=name,
251+
context=context,
252+
)
237253

238254
@property
239255
def abs_path(self):

setup.cfg

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = oarepo-model-builder
3-
version = 4.0.76
3+
version = 4.0.77
44
description = A utility library that generates OARepo required data model files from a JSON specification file
55
authors = Miroslav Bauer <[email protected]>, Miroslav Simek <[email protected]>
66
readme = README.md
@@ -173,6 +173,9 @@ oarepo_model_builder.loaders.extend =
173173
0100-extract_record = oarepo_model_builder.loaders.extend:extract_extended_record
174174
1000-modify-marshmallow = oarepo_model_builder.loaders.extend:extend_modify_marshmallow
175175

176+
oarepo_model_builder.loaders.post.extend =
177+
1000-modify-marshmallow = oarepo_model_builder.loaders.extend:post_extend_modify_marshmallow
178+
176179

177180
oarepo_model_builder.settings =
178181
1000-python = oarepo_model_builder.settngs:python.json

tests/test_extend.py

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from oarepo_model_builder.loaders.extend import (
66
extend_modify_marshmallow,
77
extract_extended_record,
8+
post_extend_modify_marshmallow,
89
)
910
from oarepo_model_builder.schema import ModelSchema
1011

@@ -26,24 +27,28 @@ def test_extend_marshmallow():
2627
extend_modify_marshmallow,
2728
]
2829
},
30+
post_reference_processors={
31+
ModelSchema.EXTEND_KEYWORD: [
32+
post_extend_modify_marshmallow,
33+
]
34+
},
2935
)
3036
builder.build(model, "record", ["record"], "")
3137

3238
loaded_model = json5.loads(fs.read("test/models/records.json"))
3339
assert loaded_model["model"]["marshmallow"] == {
34-
"base-classes": ["aaa.BlahSchema"],
35-
"class": "test.services.records.schema.TestSchema",
40+
"base-classes": ["marshmallow.Schema"],
41+
"class": "aaa.BlahSchema",
3642
"extra-code": "",
37-
"generate": True,
38-
"imports": [{"alias": "aaa", "import": "aaa"}],
43+
"generate": False,
3944
"module": "test.services.records.schema",
4045
}
4146
assert loaded_model["model"]["ui"]["marshmallow"] == {
42-
"base-classes": ["aaa.BlahUISchema"],
43-
"class": "test.services.records.ui_schema.TestUISchema",
47+
"base-classes": ["oarepo_runtime.services.schema.ui.InvenioUISchema"],
48+
"class": "aaa.BlahUISchema",
4449
"extra-code": "",
45-
"generate": True,
46-
"imports": [{"alias": "aaa", "import": "aaa"}],
50+
"generate": False,
51+
"imports": [],
4752
"module": "test.services.records.ui_schema",
4853
}
4954
# assert that "a" is read & write false
@@ -53,17 +58,10 @@ def test_extend_marshmallow():
5358
assert property_a["ui"]["marshmallow"]["read"] is False
5459
assert property_a["ui"]["marshmallow"]["write"] is False
5560

56-
schema = fs.read("test/services/records/schema.py")
57-
print(schema)
58-
assert "class TestSchema(BlahSchema)" in schema
59-
assert "class TestMetadataSchema(BlahMetadataSchema)" in schema
60-
assert "metadata = ma_fields.Nested(lambda: TestMetadataSchema())" in schema
61-
62-
schema = fs.read("test/services/records/ui_schema.py")
63-
print(schema)
64-
assert "class TestUISchema(BlahUISchema)" in schema
65-
assert "class TestMetadataUISchema(BlahMetadataUISchema)" in schema
66-
assert "metadata = ma_fields.Nested(lambda: TestMetadataUISchema())" in schema
61+
service_config = fs.read("test/services/records/config.py")
62+
print(service_config)
63+
assert "from aaa import BlahSchema" in service_config
64+
assert "schema = BlahSchema" in service_config
6765

6866

6967
extension_model = {

0 commit comments

Comments
 (0)