def gen_references(self) -> str: """ Generate python type declarations for all identifiers (primary keys) """ rval = [] for cls in self._sort_classes(self.schema.classes.values()): if not cls.imported_from: pkeys = self.primary_keys_for(cls) if pkeys: for pk in pkeys: classname = camelcase(cls.name) + camelcase( self.aliased_slot_name(pk)) # If we've got a parent slot and the range of the parent is the range of the child, the # child slot is a subclass of the parent. Otherwise, the child range has been overridden, # so the inheritence chain has been broken parent_pk = self.class_identifier( cls.is_a) if cls.is_a else None parent_pk_slot = self.schema.slots[ parent_pk] if parent_pk else None pk_slot = self.schema.slots[pk] if parent_pk_slot and (parent_pk_slot.name == pk or pk_slot.range == parent_pk_slot.range): parents = self.class_identifier_path( cls.is_a, False) else: parents = self.slot_range_path(pk_slot) parent_cls = 'extended_' + parents[-1] if parents[ -1] in ['str', 'float', 'int'] else parents[-1] rval.append( f'class {classname}({parent_cls}):\n\tpass') break # We only do the first primary key return '\n\n\n'.join(rval)
def _link(self, obj: Optional[Element], *, after_link: str = None, use_desc: bool = False, add_subset: bool = True) -> str: """ Create a link to ref if appropriate. @param ref: the name or value of a class, slot, type or the name of a built in type. @param after_link: Text to put between link and description @param use_desc: True means append a description after the link if available @param add_subset: True means add any subset information that is available @return: """ nl = '\n' if obj is None or not self.is_secondary_ref(obj.name): return self.bbin(obj) if isinstance(obj, SlotDefinition): link_name = ((be(obj.domain) + '➞') if obj.alias else '') + self.aliased_slot_name(obj) link_ref = underscore(obj.name) elif isinstance(obj, TypeDefinition): link_name = camelcase(obj.name) link_ref = f"types/{link_name}" elif isinstance(obj, ClassDefinition): link_name = camelcase(obj.name) link_ref = camelcase(link_name) else: link_name = obj.name link_ref = link_name desc = self.desc_for(obj, use_desc) return f'[{link_name}]' \ f'({link_ref}.{self.format})' + \ (f' {after_link} ' if after_link else '') + (f' - {desc.split(nl)[0]}' if desc else '')
def end_class(self, cls: ClassDefinition) -> None: if cls.is_a: self._add_constraint(self.namespaces.uri_for(camelcase(cls.is_a) + "_t")) for mixin in cls.mixins: if self._class_has_expressions(mixin): self._add_constraint(self.namespaces.uri_for(camelcase(mixin) + "_t")) if cls.name in self.synopsis.applytorefs: for applyto in self.synopsis.applytorefs[cls.name].classrefs: if self._class_has_expressions(applyto): self._add_constraint(self.namespaces.uri_for(camelcase(applyto) + '_t')) self.shape.closed = True self.shape.extra = [RDF.type] if self.shape.expression: # TODO: Figure out how to label a single triple expression if isinstance_(self.shape.expression, tripleExprLabel): self.shape.expression = EachOf(expressions=[self.shape.expression, wildcard(None)]) self.shape.expression.id = self.namespaces.uri_for(camelcase(cls.name) + "_t") else: self.shape.expression = wildcard(self.namespaces.uri_for(camelcase(cls.name) + "_t")) if self.class_identifier(cls): self.shape.extra = [RDF.type] type_constraint = TripleConstraint() type_constraint.predicate = RDF.type type_constraint.valueExpr = NodeConstraint(values=[IRIREF(self.namespaces.uri_for(cls.class_uri))]) if not self.shape.expression: self.shape.expression = type_constraint else: self.shape.expression = EachOf(expressions=[self.shape.expression, type_constraint]) shapeExpr = self.shape shapeExpr.id = self._shape_iri(cls.name) self.shapes.append(shapeExpr)
def _visit(self, node: Any) -> Optional[Any]: if isinstance(node, (YAMLRoot, dict)): if isinstance(node, YAMLRoot): node = self._add_type(node) for k, v in list(node.items()): if v: new_v = self._visit(v) if new_v is not None: node[k] = new_v elif isinstance(node, list): for i in range(0, len(node)): new_v = self._visit(node[i]) if new_v is not None: node[i] = new_v elif isinstance(node, set): for v in list(node): new_v = self._visit(v) if new_v is not None: node.remove(v) node.add(new_v) elif isinstance(node, ClassDefinitionName): return ClassDefinitionName(camelcase(node)) elif isinstance(node, SlotDefinitionName): return SlotDefinitionName(underscore(node)) elif isinstance(node, TypeDefinitionName): return TypeDefinitionName(underscore(node)) elif isinstance(node, SubsetDefinitionName): return SubsetDefinitionName(underscore(node)) elif isinstance(node, ElementName): return ClassDefinitionName(camelcase(node)) if node in self.schema.classes else \ SlotDefinitionName(underscore(node)) if node in self.schema.slots else \ SubsetDefinitionName(camelcase(node)) if node in self.schema.subsets else \ TypeDefinitionName(underscore(node)) if node in self.schema.types else None return None
def visit_class(self, cls: ClassDefinition) -> bool: self.classtab.writerow({ 'Class Name': camelcase(cls.name), 'Parent Class': camelcase(cls.is_a) if cls.is_a else '', 'YAML Class Name': cls.name, 'Description': cls.description }) return True
def test_formats(self): self.assertEqual("ThisIsIt", camelcase("this is it")) self.assertEqual("ThisIsIT", camelcase(" this is iT ")) self.assertEqual("IBeY", camelcase("i be y ")) self.assertEqual("ThisIsIt", camelcase("This__is_it")) self.assertEqual("this_is_it", underscore(" this is it ")) self.assertEqual("this_is_it", underscore("this is it")) self.assertEqual("thisIsIt", lcamelcase(" this is\t it\n")) self.assertEqual('abc', be(' abc\n')) self.assertEqual('', be(None)) self.assertEqual('', be(' '))
def gen_typedefs(self) -> str: """ Generate python type declarations for all defined types """ rval = [] for typ in self.schema.types.values(): if not typ.imported_from: typname = camelcase(typ.name) desc = f'\n\t""" {typ.description} """' if typ.description else '' if typ.typeof: parent_typename = camelcase(typ.typeof) rval.append(f'class {typname}({parent_typename}):{desc}\n\t{self.gen_type_meta(typ)}\n\n') else: base_base = typ.base.rsplit('.')[-1] rval.append(f'class {typname}({base_base}):{desc}\n\t{self.gen_type_meta(typ)}\n\n') return '\n'.join(rval)
def class_or_type_name(self, name: str) -> str: """ Return the camelcase representation of clsname if it is a valid class or type. Prepend "Unknown" if the name isn't valid """ if name in self.schema.classes: return camelcase(name) elif name in self.schema.types: typ = self.schema.types[cast(TypeDefinitionName, name)] if typ.typeof: return camelcase(name) else: return typ.base else: return "Unknown_" + camelcase(name)
def gen_enum(self, enum: EnumDefinition) -> str: enum_name = camelcase(enum.name) return f''' class {enum_name}(EnumDefinitionImpl): {self.gen_enum_comment(enum)} {self.gen_enum_description(enum, enum_name)} '''.strip()
def visit_class_slot(self, cls: ClassDefinition, aliased_slot_name: str, slot: SlotDefinition) -> None: qual = 'repeated ' if slot.multivalued else 'optional ' if not slot.required or slot.key else '' slotname = lcamelcase(aliased_slot_name) slot_range = camelcase(slot.range) self.relative_slot_num += 1 print(f" {qual}{slotname} {slot_range} = {self.relative_slot_num}")
def visit_class_slot(self, cls: ClassDefinition, aliased_slot_name: str, slot: SlotDefinition) -> None: slotrange = camelcase(slot.range) if slot.range in self.schema.classes or slot.range in self.schema.types else "String" if slot.multivalued: slotrange = f"[{slotrange}]" if slot.required: slotrange = slotrange + '!' print(f" {lcamelcase(aliased_slot_name)}: {slotrange}")
def gen_type_meta(self, typ: TypeDefinition) -> str: type_class_uri = self.namespaces.uri_for(typ.uri) if type_class_uri: type_python_uri = self.namespaces.curie_for(type_class_uri, default_ok=False, pythonform=True) type_class_curie = self.namespaces.curie_for(type_class_uri, default_ok=False, pythonform=False) else: type_python_uri = None type_class_curie = None if type_class_curie: type_class_curie = f'"{type_class_curie}"' type_class_uri = type_python_uri if type_python_uri else f'URIRef("{type_class_uri}")' type_model_uri = self.namespaces.uri_or_curie_for( self.schema.default_prefix, camelcase(typ.name)) if ':/' in type_model_uri: type_model_uri = f'URIRef("{type_model_uri}")' else: ns, ln = type_model_uri.split(':', 1) ln_suffix = f".{ln}" if ln.isidentifier() else f'["{ln}"]' type_model_uri = f"{ns.upper()}{ln_suffix}" vars = [ f'type_class_uri = {type_class_uri}', f'type_class_curie = {type_class_curie}', f'type_name = "{typ.name}"', f'type_model_uri = {type_model_uri}' ] return "\n\t".join(vars)
def add_element(self, e: Element) -> None: if e.imported_from: if str(e.imported_from) == biolinkml.METATYPE_URI: # TODO: figure out how to make this sort of stuff part of the model self.v.setdefault(types.__name__, set()).add(camelcase(e.name)) elif str(e.imported_from) == biolinkml.BIOLINK_MODEL_URI: self.v.setdefault(biolinkml.BIOLINK_MODEL_PYTHON_LOC, set()).add(camelcase(e.name)) elif e.imported_from.__contains__('://'): raise ValueError( f"Cannot map {e.imported_from} into a python import statement" ) else: self.v.setdefault(types.__name__, set()).add(camelcase(e.name))
def gen_slot(self, slot: SlotDefinition) -> str: python_slot_name = underscore(slot.name) slot_uri, slot_curie = self.python_uri_for(slot.slot_uri) slot_model_uri, slot_model_curie = \ self.python_uri_for(self.namespaces.uri_or_curie_for(self.schema.default_prefix, python_slot_name)) domain = camelcase( slot.domain) if slot.domain and not self.schema.classes[ slot.domain].mixin else "None" # Going to omit the range on keys where the domain isn't specified (for now) if slot.domain is None and (slot.key or slot.identifier): rnge = "URIRef" else: rnge, _ = self.range_cardinality( slot, self.schema.classes[slot.domain] if slot.domain else None, True) if slot.mappings: map_texts = [ self.namespaces.curie_for(self.namespaces.uri_for(m), default_ok=True, pythonform=True) for m in slot.mappings if m != slot.slot_uri ] else: map_texts = [] if map_texts: mappings = ', mappings = [' + ', '.join(map_texts) + ']' else: mappings = '' pattern = f",\n pattern=re.compile(r'{slot.pattern}')" if slot.pattern else "" return f"""slots.{python_slot_name} = Slot(uri={slot_uri}, name="{slot.name}", curie={slot_curie},
def visit_type(self, typ: TypeDefinition) -> None: with open(self.dir_path(typ), 'w') as typefile: with redirect_stdout(typefile): full_path = sfx(self.namespaces._base) + (sfx( typ.imported_from) if typ.imported_from else '') type_curie = self.namespaces.uri_or_curie_for( full_path, camelcase(typ.name)) type_uri = self.namespaces.uri_for(type_curie) self.frontmatter( **{ 'parent': 'Types', 'title': type_curie, 'grand_parent': self.doc_root_title, 'layout': 'default' }) self.element_header(typ, typ.name, type_curie, type_uri) print("| | | |") print("| --- | --- | --- |") if typ.typeof: print( f"| Parent type | | {self.class_type_link(typ.typeof)} |" ) print(f"| Root (builtin) type | | **{typ.base}** |") if typ.repr: print(f"| Representation | | {typ.repr} |")
def class_identifier_path(self, cls_or_clsname: Union[str, ClassDefinition], force_non_key: bool) -> List[str]: """ Return the path closure to a class identifier if the class has a key and force_non_key is false otherwise return a dictionary closure. :param cls_or_clsname: class definition :param force_non_key: True means inlined even if the class has a key :return: path """ cls = cls_or_clsname if isinstance(cls_or_clsname, ClassDefinition) \ else self.schema.classes[cast(ClassDefinitionName, cls_or_clsname)] # Determine whether the class has a key identifier_slot = None if not force_non_key: identifier_slot = self.class_identifier(cls, keys_count=True) # No key or inlined, its closure is a dictionary if identifier_slot is None: return ['dict', self.class_or_type_name(cls.name)] # We're dealing with a reference pathname = camelcase(cls.name + ' ' + self.aliased_slot_name(identifier_slot)) if cls.is_a: parent_identifier_slot = self.class_identifier(cls.is_a, keys_count=True) if parent_identifier_slot: return self.class_identifier_path(cls.is_a, False) + [pathname] return self.slot_type_path(identifier_slot) + [pathname]
def gen_class_meta(self, cls: ClassDefinition) -> str: class_class_uri = self.namespaces.uri_for(cls.class_uri) if class_class_uri: cls_python_uri = self.namespaces.curie_for(class_class_uri, default_ok=False, pythonform=True) class_class_curie = self.namespaces.curie_for(class_class_uri, default_ok=False, pythonform=False) else: cls_python_uri = None class_class_curie = None if class_class_curie: class_class_curie = f'"{class_class_curie}"' class_class_uri = cls_python_uri if cls_python_uri else f'URIRef("{class_class_uri}")' class_model_uri = self.namespaces.uri_or_curie_for( self.schema.default_prefix or "DEFAULT_", camelcase(cls.name)) if ':/' in class_model_uri: class_model_uri = f'URIRef("{class_model_uri}")' else: ns, ln = class_model_uri.split(':', 1) class_model_uri = f"{ns.upper()}.{ln}" vars = [ f'class_class_uri: ClassVar[URIRef] = {class_class_uri}', f'class_class_curie: ClassVar[str] = {class_class_curie}', f'class_name: ClassVar[str] = "{cls.name}"', f'class_model_uri: ClassVar[URIRef] = {class_model_uri}' ] if cls.string_template: vars.append('') vars.append( f'string_template: ClassVar[str] = "{cls.string_template}"') return "\n\t".join(vars)
def visit_slot(self, slot_name: str, slot: SlotDefinition) -> None: """ Add a slot definition per slot @param slot_name: @param slot: @return: """ # Note: We use the raw name in OWL and add a subProperty arc slot_uri = self._prop_uri(slot.name) self._add_element_properties(slot_uri, slot) self.graph.add( (slot_uri, RDF.type, OWL.ObjectProperty if slot.inherited else OWL.AnnotationProperty)) self.graph.add((slot_uri, RDF.type, self.metamodel.namespaces[METAMODEL_LOCAL_NAME][ camelcase('slot definition')])) self.graph.add((slot_uri, RDFS.range, self._range_uri(slot))) self.graph.add((slot_uri, RDFS.domain, self._class_uri(slot.domain))) # Parent slots if slot.is_a: self.graph.add( (slot_uri, RDFS.subPropertyOf, self._prop_uri(slot.is_a))) for mixin in slot.mixins: self.graph.add( (slot_uri, RDFS.subPropertyOf, self._prop_uri(mixin))) if slot.name in self.synopsis.applytorefs: for appl in self.synopsis.applytorefs[slot.name].slotrefs: self.graph.add( (slot_uri, RDFS.subClassOf, self._prop_uri(appl)))
def visit_class(self, cls: ClassDefinition) -> bool: etype = 'interface' if (cls.abstract or cls.mixin) and not cls.mixins else 'type' mixins = ', '.join([camelcase(mixin) for mixin in cls.mixins]) print(f"{etype} {camelcase(cls.name)}" + (f" implements {mixins}" if mixins else "")) print(" {") return True
def adjust_slot(self, slot: SlotDefinition) -> None: if slot.range in self.schema.classes: slot.range = ClassDefinitionName(camelcase(slot.range)) elif slot.range in self.schema.slots: slot.range = SlotDefinitionName(underscore(slot.range)) elif slot.range in self.schema.types: slot.range = TypeDefinitionName(underscore(slot.range)) slot.slot_uri = self.namespaces.uri_for(slot.slot_uri)
def enum_identifier_path( self, enum_or_enumname: Union[str, EnumDefinition]) -> List[str]: """ Return an enum_identifier path """ return [ 'str', camelcase(enum_or_enumname.name if isinstance( enum_or_enumname, EnumDefinition) else enum_or_enumname) ]
def dir_path( self, obj: Union[ClassDefinition, SlotDefinition, TypeDefinition]) -> str: filename = self.formatted_element_name(obj) if isinstance(obj, ClassDefinition) \ else underscore(obj.name) if isinstance(obj, SlotDefinition) \ else camelcase(obj.name) subdir = '/types' if isinstance(obj, TypeDefinition) else '' return f'{self.directory}{subdir}/{filename}.md'
def visit_class(self, cls: ClassDefinition) -> bool: if cls.abstract: return False self.clsobj = JsonObj(title=camelcase(cls.name), type='object', properties=JsonObj(), description=be(cls.description)) return True
def visit_class_slot(self, cls: ClassDefinition, aliased_slot_name: str, slot: SlotDefinition) -> None: if slot.range in self.schema.classes and slot.inlined: rng = f"#/definitions/{camelcase(slot.range)}" elif slot.range in self.schema.types: rng = self.schema.types[slot.range].base else: # note we assume string for non-lined complex objects rng = "string" # translate to json-schema builtins if rng == 'int': rng = 'integer' elif rng == 'Bool': rng = 'boolean' elif rng == 'str': rng = 'string' elif rng == 'float' or rng == 'double': rng = 'number' elif not rng.startswith('#'): # URIorCURIE, etc rng = 'string' if slot.inlined: # If inline we have to include redefined slots ref = JsonObj() ref['$ref'] = rng if slot.multivalued: prop = JsonObj(type="array", items=ref) else: prop = ref else: if slot.multivalued: prop = JsonObj(type="array", items={'type': rng}) else: prop = JsonObj(type=rng) if slot.description: prop.description = slot.description if slot.required: self.clsobj.required.append(underscore(aliased_slot_name)) self.clsobj.properties[underscore(aliased_slot_name)] = prop if self.topCls is not None and camelcase(self.topCls) == camelcase(cls.name) or \ self.topCls is None and cls.tree_root: self.schemaobj.properties[underscore(aliased_slot_name)] = prop
def _class_or_type_uri(self, item: Union[TypeDefinition, ClassDefinition, ElementName], suffix: Optional[str] = '') -> URIorCURIE: if isinstance(item, (TypeDefinition, ClassDefinition)): cls_or_type = item else: cls_or_type = self.class_or_type_for(item) return self.namespaces.uri_for( self.namespaces.uri_or_curie_for(self.schema_defaults[cls_or_type.from_schema], camelcase(cls_or_type.name) + suffix))
def visit_class(self, cls: ClassDefinition) -> bool: if self.gen_classes and cls.name not in self.gen_classes: return False with open(self.dir_path(cls), 'w') as clsfile: with redirect_stdout(clsfile): self.frontmatter(f"Class: {cls.name}") self.para(be(cls.description)) class_curi = self.namespaces.uri_or_curie_for(self.namespaces._base, camelcase(cls.name)) class_uri = self.namespaces.uri_for(class_curi) print(f'URI: [{class_curi}]({class_uri})') print() if self.image_directory: yg = YumlGenerator(self) yg.serialize(classes=[cls.name], directory=self.image_directory, load_image=not self.noimages) img_url = os.path.join('images', os.path.basename(yg.output_file_name)) else: yg = YumlGenerator(self) img_url = yg.serialize(classes=[cls.name])\ .replace('[', '\\[').replace('?', '%3F').replace(' ', '%20') print(f'![img]({img_url})') self.mappings(cls) self.header(2, 'Inheritance') if cls.is_a is not None: self.bullet(f' is_a: {self.class_link(cls.is_a, use_desc=True)}') for mixin in cls.mixins: self.bullet(f' mixin: {self.class_link(mixin, use_desc=True)}') self.header(2, 'Children') if cls.name in self.synopsis.isarefs: for child in sorted(self.synopsis.isarefs[cls.name].classrefs): self.bullet(f'{self.class_link(child, use_desc=True)}') if cls.name in self.synopsis.mixinrefs: for mixin in sorted(self.synopsis.mixinrefs[cls.name].classrefs): self.bullet(f'{self.class_link(mixin, use_desc=True, after_link="(mixin)")}') if cls.name in self.synopsis.classrefs: self.header(2, 'Used by') for sn in sorted(self.synopsis.classrefs[cls.name].slotrefs): slot = self.schema.slots[sn] if slot.range == cls.name: self.bullet(f' **{self.class_link(slot.domain)}** ' f'*{self.slot_link(slot, add_subset=False)}*{self.predicate_cardinality(slot)} ' f'**{self.class_type_link(slot.range)}**') self.header(2, 'Fields') for sn in sorted(cls.slots): self.slot_field(cls, self.schema.slots[sn]) for slot in sorted(self.all_slots(cls), key=lambda s: s.name): if slot.name not in cls.slots: self.slot_field(cls, slot) return True
def visit_type(self, typ: TypeDefinition) -> None: """ Visit a given type definition and write the following properties in Markdown, - Frontmatter - Description - Domain and Range constraints - Parents - Children - Used by Parameters ---------- typ: biolinkml.meta.TypeDefinition A TypeDefinition """ with open(self.dir_path(typ), 'w') as typefile: with redirect_stdout(typefile): full_path = sfx(self.namespaces._base) + (sfx( typ.imported_from) if typ.imported_from else '') type_curie = self.namespaces.uri_or_curie_for( full_path, camelcase(typ.name)) type_uri = self.namespaces.uri_for(type_curie) if type_curie.startswith( 'https://w3id.org/biolink/vocab/biolinkml:types/'): ref = type_curie.split('/')[-1] type_uri = f"https://biolink.github.io/biolinkml/docs/types/{ref}" type_curie = f"metatype:{ref}" elif type_uri.startswith('https://w3id.org/biolink/vocab/'): ref = type_curie.split('/')[-1] type_uri = f"https://w3id.org/biolink/vocab/types/{ref}" if typ.imported_from and 'biolinkml:types' in typ.imported_from: parent = 'Built-in Types' else: parent = 'Defined Types' self.frontmatter( **{ 'parent': parent, 'grand_parent': 'Types', 'title': type_curie, 'layout': 'default' }) self.element_header(typ, typ.name, type_curie, type_uri) print("| | | |") print("| --- | --- | --- |") if typ.typeof: print( f"| Parent type | | {self.class_type_link(typ.typeof)} |" ) print(f"| Root (builtin) type | | **{typ.base}** |") if typ.repr: print(f"| Representation | | {typ.repr} |")
def gen_references(self) -> str: """ Generate python type declarations for all identifiers (primary keys) """ rval = [] for cls in self.schema.classes.values(): if not cls.imported_from: pkeys = self.primary_keys_for(cls) if pkeys: for pk in pkeys: classname = camelcase(cls.name) + camelcase( self.aliased_slot_name(pk)) if cls.is_a and self.class_identifier(cls.is_a): parents = self.class_identifier_path( cls.is_a, False) else: parents = self.slot_type_path( self.schema.slots[pk]) rval.append( f'class {classname}({parents[-1]}):\n\tpass') break # We only do the first primary key return '\n\n\n'.join(rval)
def add_element(self, e: Element) -> None: if e.imported_from: if str(e.imported_from) == biolinkml.METATYPE_URI: # TODO: figure out how to make this sort of stuff part of the model self.v.setdefault(types.__name__, set()).add(camelcase(e.name)) elif e.imported_from.__contains__('://'): raise ( NotImplementedError, f"Cannot map {e.imported_from} into a python import statement" ) else: anchor_path = os.path.dirname(self.schema_location) abs_import_path = os.path.join(anchor_path, e.imported_from) \ if not os.path.isabs(e.imported_from) else e.imported_from python_base_dir = os.path.dirname( os.path.dirname(inspect.getfile(biolinkml))) python_import_dir = os.path.relpath( abs_import_path, python_base_dir) self.v.setdefault(python_import_dir.replace('/', '.'), set()).add(camelcase(e.name))
def set_from_schema(schema: SchemaDefinition) -> None: for t in [schema.subsets, schema.classes, schema.slots, schema.types]: for k in t.keys(): t[k].from_schema = schema.id if isinstance(t[k], SlotDefinition): fragment = underscore(t[k].name) else: fragment = camelcase(t[k].name) if schema.default_prefix in schema.prefixes: ns = schema.prefixes[schema.default_prefix].prefix_reference else: ns = str(URIRef(schema.id) + "/") t[k].definition_uri = f'{ns}{fragment}'