3232 set_resolver ,
3333)
3434from oold .generator import Generator
35+ from oold .model .v1 import _types as oold_type_registry
3536from oold .utils .codegen import OOLDJsonSchemaParser
3637from opensemantic .v1 import OswBaseModel
3738from pydantic import PydanticDeprecatedSince20
@@ -1390,6 +1391,10 @@ def load_entity(
13901391 entity = None
13911392 schemas = []
13921393 schemas_fetched = True
1394+ # maps a category (page title, e.g. "Category:OSW...") to the
1395+ # class to use for it, either an already registered class (e.g. a
1396+ # packaged model class) or a freshly compiled one
1397+ category_to_cls : Dict [str , Type [model .Entity ]] = {}
13931398 jsondata = page .get_slot_content ("jsondata" )
13941399 if param .remove_empty :
13951400 remove_empty (jsondata )
@@ -1411,23 +1416,99 @@ def load_entity(
14111416 # If a schema_to_use is provided, we do not need to check if
14121417 # the model exists
14131418 if not param .model_to_use :
1414- if not hasattr (model , cls_name ):
1415- if param .autofetch_schema :
1416- self .fetch_schema (
1417- OSW .FetchSchemaParam (
1418- schema_title = category ,
1419- mode = "append" ,
1420- offline_pages = param .offline_pages ,
1419+ # Prefer a class already registered for this category
1420+ # IRI (e.g. a packaged model class) over compiling a
1421+ # new one. Compiling one anyway would take over the
1422+ # oold type registry entry for this category and hide
1423+ # the packaged class' typed fields/helpers.
1424+ registered_cls = oold_type_registry .get (category )
1425+ # Controllers and result wrappers (e.g.
1426+ # WikiFileController, UploadFileResult) inherit
1427+ # their category IRI from the model class they
1428+ # extend, and the registry keeps whichever class
1429+ # was defined last. Such a specialization asks for
1430+ # fields a plain page's jsondata does not carry, so
1431+ # prefer the canonical class from osw.model.entity
1432+ # over a subclass of it.
1433+ canonical_cls = getattr (model , cls_name , None )
1434+ if (
1435+ registered_cls is not None
1436+ # the schema title can also name a module
1437+ # attribute that is not a class
1438+ and isinstance (canonical_cls , type )
1439+ and registered_cls is not canonical_cls
1440+ and issubclass (registered_cls , canonical_cls )
1441+ ):
1442+ _logger .debug (
1443+ f"Ignoring '{ registered_cls } ' registered for "
1444+ f"category '{ category } ': it specializes "
1445+ f"'{ canonical_cls } ', which is used instead."
1446+ )
1447+ registered_cls = None
1448+ if registered_cls is not None :
1449+ category_to_cls [category ] = registered_cls
1450+ else :
1451+ if not hasattr (model , cls_name ):
1452+ if param .autofetch_schema :
1453+ self .fetch_schema (
1454+ OSW .FetchSchemaParam (
1455+ schema_title = category ,
1456+ mode = "append" ,
1457+ offline_pages = param .offline_pages ,
1458+ )
14211459 )
1460+ if not hasattr (model , cls_name ):
1461+ schemas_fetched = False
1462+ _logger .error (
1463+ f"Model { cls_name } not found. Schema "
1464+ f"{ category } needs to be fetched first."
14221465 )
1423- if not hasattr (model , cls_name ):
1424- schemas_fetched = False
1425- _logger .error (
1426- f"Model { cls_name } not found. Schema { category } "
1427- f"needs to be fetched first."
1428- )
1466+ else :
1467+ generated_cls = getattr (model , cls_name )
1468+ # The class we are about to use may have just
1469+ # claimed (or may already hold) the registry
1470+ # slot for this category. If a different class
1471+ # is registered for it, someone's registration
1472+ # was silently overwritten - do not raise, but
1473+ # make sure this does not pass silently.
1474+ conflicting_cls = oold_type_registry .get (category )
1475+ if (
1476+ conflicting_cls is not None
1477+ and conflicting_cls is not generated_cls
1478+ # the slot holder specializes
1479+ # generated_cls, e.g. a controller
1480+ # that inherits the category IRI:
1481+ # the case the guard above expects,
1482+ # not a conflict. A fetch can leave
1483+ # a non-class under the schema title.
1484+ and not (
1485+ isinstance (generated_cls , type )
1486+ and issubclass (
1487+ conflicting_cls , generated_cls
1488+ )
1489+ )
1490+ ):
1491+ _logger .warning (
1492+ f"Class '{ generated_cls } ' generated for "
1493+ f"category '{ category } ' claims the oold "
1494+ f"type registry slot already held by a "
1495+ f"different class '{ conflicting_cls } '."
1496+ )
1497+ category_to_cls [category ] = generated_cls
14291498 if not schemas_fetched :
14301499 continue
1500+ # fetch_schema() reloads osw.model.entity, which replaces every
1501+ # class defined there, so a class picked before a later fetch in
1502+ # the loop above may be out of date: take the current object
1503+ for category , chosen_cls in category_to_cls .items ():
1504+ # a non-class is left for the construction below to reject
1505+ if (
1506+ isinstance (chosen_cls , type )
1507+ and chosen_cls .__module__ == model .__name__
1508+ ):
1509+ category_to_cls [category ] = getattr (
1510+ model , chosen_cls .__name__ , chosen_cls
1511+ )
14311512
14321513 try :
14331514 if param .model_to_use :
@@ -1437,13 +1518,15 @@ def load_entity(
14371518 _logger .error ("no schema defined" )
14381519
14391520 elif len (schemas ) == 1 :
1440- cls : Type [model .Entity ] = getattr (model , schemas [0 ]["title" ])
1521+ # category_to_cls is fully populated for every category
1522+ # that reached this point (see the loop above)
1523+ cls : Type [model .Entity ] = category_to_cls [jsondata ["type" ][0 ]]
14411524 entity : model .Entity = cls (** jsondata )
14421525
14431526 else :
14441527 bases = []
1445- for schema in schemas :
1446- bases .append (getattr ( model , schema [ "title" ]) )
1528+ for category in jsondata [ "type" ] :
1529+ bases .append (category_to_cls [ category ] )
14471530 cls = create_model ("Test" , __base__ = tuple (bases ))
14481531 entity : model .Entity = cls (** jsondata )
14491532 except Exception as e :
0 commit comments