Skip to content

Commit 2ed0a1f

Browse files
committed
fix(core): honour per-property settings in the overwrite policy
- rebuild the effective settings when per_property/overwrite is reassigned - reject per_property combined with 'replace remote' / 'keep existing' - normalise overwrite=None and the 'none' sentinel to the field default - report a missing 'model' as a validation error, not an AttributeError - stop rewriting the schema of an existing page under 'keep existing'
1 parent 3a45142 commit 2ed0a1f

2 files changed

Lines changed: 586 additions & 20 deletions

File tree

‎src/osw/core.py‎

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1337,7 +1337,13 @@ class OverwriteClassParam(OswBaseModel):
13371337

13381338
@validator("per_property")
13391339
def validate_per_property(cls, per_property, values):
1340+
if per_property is None: # nothing to check, the fallback applies
1341+
return per_property
13401342
model_ = values.get("model")
1343+
if model_ is None:
1344+
# 'model' itself did not validate; without it the property names
1345+
# below cannot be checked at all
1346+
raise ValueError("'model' is required to validate 'per_property'")
13411347
field_names = list(model_.__fields__.keys())
13421348
keys = per_property.keys()
13431349
if not all(key in field_names for key in keys):
@@ -1348,36 +1354,68 @@ def validate_per_property(cls, per_property, values):
13481354

13491355
return per_property
13501356

1357+
@classmethod
1358+
def _normalize_overwrite(cls, value):
1359+
"""Replace the two non-policy values by the default setting.
1360+
1361+
Neither ``None`` nor the ``none`` sentinel is a policy:
1362+
``get_overwrite_setting()`` would hand them to the merge, where they
1363+
match no branch and silently behave like 'false'.
1364+
"""
1365+
if value is None or value is AddOverwriteClassOptions.none:
1366+
return cls.__fields__["overwrite"].get_default()
1367+
return value
1368+
13511369
def __setattr__(self, key, value):
13521370
"""Called when setting an attribute"""
1371+
if key == "overwrite":
1372+
value = self._normalize_overwrite(value)
1373+
# the effective settings are derived from these three, so any of them
1374+
# changing has to rebuild them
1375+
if key not in ("model", "overwrite", "per_property"):
1376+
super().__setattr__(key, value)
1377+
return
1378+
previous = getattr(self, key)
13531379
super().__setattr__(key, value)
1354-
if key == "per_property":
1355-
# compare value and self.per_property
1356-
if value != self.per_property and value is not None:
1357-
self._per_property = {
1358-
field_name: value.get(field_name, self.overwrite)
1359-
for field_name in self.model.__fields__.keys()
1360-
}
1361-
elif key == "overwrite" or key == "model":
1362-
if self.per_property is not None:
1363-
self._per_property = {
1364-
field_name: self.per_property.get(field_name, self.overwrite)
1365-
for field_name in self.model.__fields__.keys()
1366-
}
1380+
try:
1381+
self._sync_per_property()
1382+
except ValueError:
1383+
# _sync_per_property() rejects before it touches _per_property,
1384+
# so restoring the field is enough to undo the assignment. Leaving
1385+
# a rejected value in place would let it take effect later, on the
1386+
# next assignment that happens to be accepted.
1387+
super().__setattr__(key, previous)
1388+
raise
13671389

13681390
def __init__(self, **data):
13691391
"""Called after validation. Sets the fallback for every property that
13701392
has not been specified in per_property."""
13711393
super().__init__(**data)
1372-
per_property_ = {}
1373-
if self.per_property is not None:
1374-
per_property_ = self.per_property
1394+
# routed through __setattr__, which normalizes and rebuilds
1395+
self.overwrite = self.overwrite
1396+
# todo: from class definition get properties with hidden /
1397+
# read_only option # those can be safely overwritten - set the to True
1398+
1399+
def _sync_per_property(self) -> None:
1400+
"""Rebuild the effective overwrite setting of every model field."""
1401+
if self.per_property and isinstance(
1402+
self.overwrite, AddOverwriteClassOptions
1403+
):
1404+
# _apply_overwrite_policy() short-circuits on 'replace remote'
1405+
# and 'keep existing' before it looks at a single property, so
1406+
# this combination would discard 'per_property' silently. Check
1407+
# it here rather than in a validator so that it also holds when
1408+
# either field is reassigned after construction.
1409+
raise ValueError(
1410+
f"'per_property' cannot be combined with overwrite="
1411+
f"'{self.overwrite.value}', which acts on the entity as a "
1412+
f"whole. Use an OverwriteOptions value for 'overwrite'."
1413+
)
1414+
per_property_ = self.per_property or {}
13751415
self._per_property = {
13761416
field_name: per_property_.get(field_name, self.overwrite)
13771417
for field_name in self.model.__fields__.keys()
13781418
}
1379-
# todo: from class definition get properties with hidden /
1380-
# read_only option # those can be safely overwritten - set the to True
13811419

13821420
def get_overwrite_setting(self, property_name: str) -> OverwriteOptions:
13831421
"""Returns the fallback overwrite option for the given field name"""
@@ -1773,7 +1811,18 @@ def store_entity_(
17731811
offline=param.offline,
17741812
)
17751813
)
1776-
if len(meta_category_templates.keys()) > 0:
1814+
# _apply_overwrite_policy() returned the remote page untouched. The
1815+
# schema regeneration below writes the jsonschema slot regardless of
1816+
# the policy, which would edit a page the caller asked to keep.
1817+
kept_existing = (
1818+
page.exists
1819+
# mirrors the branch order of _apply_overwrite_policy(), which
1820+
# tests 'offline is True' before it tests 'keep existing'
1821+
and param.offline is not True
1822+
and overwrite_class_param.overwrite
1823+
== AddOverwriteClassOptions.keep_existing
1824+
)
1825+
if not kept_existing and len(meta_category_templates.keys()) > 0:
17771826
generated_schemas = {}
17781827
try:
17791828
jsondata = page.get_slot_content("jsondata")
@@ -1810,7 +1859,7 @@ def store_entity_(
18101859
)
18111860
).aggregated_schema
18121861
page.set_slot_content("jsonschema", new_schema)
1813-
if param.offline is False:
1862+
if param.offline is False and not kept_existing:
18141863
page.edit(
18151864
param.edit_comment, bot_edit=param.bot_edit
18161865
) # will set page.changed if the content of the page has changed

0 commit comments

Comments
 (0)