diff --git a/schemasheets/schemamaker.py b/schemasheets/schemamaker.py index 6ceacdd..1d4ce1c 100644 --- a/schemasheets/schemamaker.py +++ b/schemasheets/schemamaker.py @@ -22,7 +22,7 @@ from schemasheets.schemasheet_datamodel import ColumnConfig, TableConfig, get_configmodel, get_metamodel, COL_NAME, \ DESCRIPTOR, \ - tmap, T_CLASS, T_PV, T_SLOT, T_SUBSET, T_SCHEMA, T_ENUM, T_PREFIX, T_TYPE, SchemaSheet, T_SETTING + tmap, T_CLASS, T_PV, T_SLOT, T_ATTRIBUTE, T_SUBSET, T_SCHEMA, T_ENUM, T_PREFIX, T_TYPE, SchemaSheet, T_SETTING from schemasheets.conf.configschema import Cardinality from schemasheets.utils.google_sheets import gsheets_download_url from schemasheets.utils.prefixtool import guess_prefix_expansion @@ -209,7 +209,7 @@ def add_row(self, row: Dict[str, Any], table_config: TableConfig): else: raise ValueError(f'No mapping for {k}; cc={cc}') - def get_current_element(self, elt: Element) -> Union[Element, PermissibleValue]: + def get_current_element(self, elt: Element, is_attr=False) -> Union[Element, PermissibleValue]: """ Look up an element in the current schema using a stub element as key @@ -232,6 +232,7 @@ def get_current_element(self, elt: Element) -> Union[Element, PermissibleValue]: This time the existing "foo" class from the schema, with its adornments, will be returned :param elt: proxy for element to look up + :param is_attr: if True, then the element is an attribute, not a slot :return: """ sc = self.schema @@ -244,7 +245,7 @@ def get_current_element(self, elt: Element) -> Union[Element, PermissibleValue]: if isinstance(elt, ClassDefinition): ix = sc.classes elif isinstance(elt, SlotDefinition): - if self.use_attributes: + if self.use_attributes or is_attr: ix = copy(sc.slots) else: ix = sc.slots @@ -321,6 +322,8 @@ def row_focal_element(self, row: Dict[str, Any], table_config: TableConfig, raise ValueError(f'Cardinality of schema col must be 1; got: {vs}') self.schema.name = vs[0] vmap[k] = [self.schema] + elif k == T_ATTRIBUTE: + vmap[k] = [self.get_current_element(elt_cls(v), is_attr=True) for v in vs] else: vmap[k] = [self.get_current_element(elt_cls(v)) for v in vs] @@ -337,11 +340,14 @@ def check_excess(descriptors): else: cls = self.get_current_element(ClassDefinition(cc.settings.applies_to_class)) vmap[T_CLASS] = [cls] - if T_SLOT in vmap: - check_excess([T_SLOT, T_CLASS]) - if len(vmap[T_SLOT]) != 1: - raise ValueError(f'Cardinality of slot field must be 1; got {vmap[T_SLOT]}') - main_elt = vmap[T_SLOT][0] + if T_SLOT in vmap or T_ATTRIBUTE in vmap: + if T_SLOT in vmap and T_ATTRIBUTE in vmap: + raise ValueError(f'Cannot have both slot and attribute in same row') + T_SLOT_ATTR = T_SLOT if T_SLOT in vmap else T_ATTRIBUTE + check_excess([T_SLOT_ATTR, T_CLASS]) + if len(vmap[T_SLOT_ATTR]) != 1: + raise ValueError(f'Cardinality of slot field must be 1; got {vmap[T_SLOT_ATTR]}') + main_elt = vmap[T_SLOT_ATTR][0] if T_CLASS in vmap: # The sheet does double duty representing a class and a slot; # Here *both* the "class" and "slot" columns are populated, so @@ -349,7 +355,7 @@ def check_excess(descriptors): # TODO: add option to allow to instead represent these as attributes c: ClassDefinition for c in vmap[T_CLASS]: - if self.use_attributes: + if self.use_attributes or T_SLOT_ATTR == T_ATTRIBUTE: # slots always belong to a class; # no separate top level slots a = SlotDefinition(main_elt.name) diff --git a/schemasheets/schemasheet_datamodel.py b/schemasheets/schemasheet_datamodel.py index 2610da7..b3bca45 100644 --- a/schemasheets/schemasheet_datamodel.py +++ b/schemasheets/schemasheet_datamodel.py @@ -23,6 +23,7 @@ T_SCHEMA = 'schema' T_CLASS = 'class' T_SLOT = 'slot' +T_ATTRIBUTE = 'attribute' T_ENUM = 'enum' T_PV = 'permissible_value' T_TYPE = 'type' @@ -34,6 +35,7 @@ T_SCHEMA: SchemaDefinition, T_CLASS: ClassDefinition, T_SLOT: SlotDefinition, + T_ATTRIBUTE: SlotDefinition, T_ENUM: EnumDefinition, T_PV: PermissibleValue, T_TYPE: TypeDefinition,