@@ -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