linkml 1.7.7__py3-none-any.whl → 1.7.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. linkml/__init__.py +14 -0
  2. linkml/generators/csvgen.py +15 -5
  3. linkml/generators/docgen/class_diagram.md.jinja2 +19 -4
  4. linkml/generators/docgen/subset.md.jinja2 +7 -7
  5. linkml/generators/docgen.py +59 -14
  6. linkml/generators/graphqlgen.py +14 -16
  7. linkml/generators/jsonldcontextgen.py +3 -3
  8. linkml/generators/jsonldgen.py +4 -3
  9. linkml/generators/jsonschemagen.py +9 -0
  10. linkml/generators/markdowngen.py +341 -301
  11. linkml/generators/owlgen.py +87 -20
  12. linkml/generators/plantumlgen.py +9 -8
  13. linkml/generators/prefixmapgen.py +15 -23
  14. linkml/generators/protogen.py +23 -18
  15. linkml/generators/pydanticgen/array.py +15 -3
  16. linkml/generators/pydanticgen/pydanticgen.py +13 -2
  17. linkml/generators/pydanticgen/template.py +6 -4
  18. linkml/generators/pythongen.py +5 -5
  19. linkml/generators/rdfgen.py +14 -5
  20. linkml/generators/shaclgen.py +18 -6
  21. linkml/generators/shexgen.py +9 -7
  22. linkml/generators/sqlalchemygen.py +1 -0
  23. linkml/generators/sqltablegen.py +16 -1
  24. linkml/generators/summarygen.py +8 -2
  25. linkml/generators/terminusdbgen.py +2 -2
  26. linkml/generators/yumlgen.py +2 -2
  27. linkml/transformers/relmodel_transformer.py +21 -1
  28. linkml/utils/__init__.py +3 -0
  29. linkml/utils/deprecation.py +255 -0
  30. linkml/utils/generator.py +87 -61
  31. linkml/utils/rawloader.py +5 -1
  32. linkml/utils/schemaloader.py +2 -1
  33. linkml/utils/sqlutils.py +13 -14
  34. linkml/validator/cli.py +11 -0
  35. linkml/validator/plugins/jsonschema_validation_plugin.py +2 -0
  36. linkml/validator/plugins/shacl_validation_plugin.py +10 -4
  37. linkml/validator/report.py +1 -0
  38. linkml/workspaces/example_runner.py +2 -0
  39. {linkml-1.7.7.dist-info → linkml-1.7.9.dist-info}/METADATA +2 -1
  40. {linkml-1.7.7.dist-info → linkml-1.7.9.dist-info}/RECORD +43 -42
  41. {linkml-1.7.7.dist-info → linkml-1.7.9.dist-info}/LICENSE +0 -0
  42. {linkml-1.7.7.dist-info → linkml-1.7.9.dist-info}/WHEEL +0 -0
  43. {linkml-1.7.7.dist-info → linkml-1.7.9.dist-info}/entry_points.txt +0 -0
@@ -30,7 +30,7 @@ from linkml_runtime.linkml_model.meta import (
30
30
  )
31
31
  from linkml_runtime.utils.formatutils import camelcase, underscore
32
32
  from linkml_runtime.utils.introspection import package_schemaview
33
- from rdflib import OWL, RDF, XSD, BNode, Graph, Literal, URIRef
33
+ from rdflib import DCTERMS, OWL, RDF, XSD, BNode, Graph, Literal, URIRef
34
34
  from rdflib.collection import Collection
35
35
  from rdflib.namespace import RDFS, SKOS
36
36
  from rdflib.plugin import Parser as rdflib_Parser
@@ -147,6 +147,8 @@ class OwlSchemaGenerator(Generator):
147
147
  mixins_as_expressions: bool = None
148
148
  """EXPERIMENTAL: If True, use OWL existential restrictions to represent mixins"""
149
149
 
150
+ default_permissible_value_type: Union[str, URIRef] = field(default_factory=lambda: OWL.Class)
151
+
150
152
  slot_is_literal_map: Mapping[str, Set[bool]] = field(default_factory=lambda: defaultdict(set))
151
153
  """DEPRECATED: use node_owltypes"""
152
154
 
@@ -830,6 +832,28 @@ class OwlSchemaGenerator(Generator):
830
832
  if ixn:
831
833
  self.graph.add((type_uri, OWL.equivalentClass, ixn))
832
834
 
835
+ def _get_metatype(
836
+ self, element: Union[Definition, PermissibleValue], default_value: Optional[Union[str, URIRef]] = None
837
+ ) -> Optional[URIRef]:
838
+ impls = []
839
+ if isinstance(element, Definition):
840
+ impls.extend(element.implements)
841
+ if isinstance(element, PermissibleValue):
842
+ if "implements" in element.annotations:
843
+ ann = element.annotations["implements"]
844
+ v = ann.value
845
+ if not isinstance(v, list):
846
+ v = [v]
847
+ impls.extend(v)
848
+ for impl in impls:
849
+ if impl.startswith("owl:"):
850
+ return OWL[impl.split(":")[1]]
851
+ if impl.startswith("rdfs:"):
852
+ return RDFS[impl.split(":")[1]]
853
+ if isinstance(default_value, str):
854
+ return URIRef(default_value)
855
+ return default_value
856
+
833
857
  def add_enum(self, e: EnumDefinition) -> None:
834
858
  g = self.graph
835
859
  enum_uri = self._enum_uri(e.name)
@@ -856,26 +880,36 @@ class OwlSchemaGenerator(Generator):
856
880
  )
857
881
  )
858
882
  pv_uris = []
883
+ owl_types = []
884
+ enum_owl_type = self._get_metatype(e, self.default_permissible_value_type)
885
+
859
886
  for pv in e.permissible_values.values():
860
- pv_uri = self._permissible_value_uri(pv, enum_uri, e)
861
- pv_uris.append(pv_uri)
862
- if pv_uri:
863
- g.add((pv_uri, RDF.type, OWL.Class))
864
- g.add((pv_uri, RDFS.label, Literal(pv.text)))
865
- # TODO: make this configurable
866
- g.add(
867
- (
868
- enum_uri,
869
- self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME]["permissible_values"],
870
- pv_uri,
871
- )
887
+ pv_owl_type = self._get_metatype(pv, enum_owl_type)
888
+ owl_types.append(pv_owl_type)
889
+ if pv_owl_type == RDFS.Literal:
890
+ pv_node = Literal(pv.text)
891
+ if pv.meaning:
892
+ logging.warning(f"Meaning on literal {pv.text} in {e.name} is ignored")
893
+ else:
894
+ pv_node = self._permissible_value_uri(pv, enum_uri, e)
895
+ pv_uris.append(pv_node)
896
+ g.add(
897
+ (
898
+ enum_uri,
899
+ self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME]["permissible_values"],
900
+ pv_node,
872
901
  )
902
+ )
903
+ if not isinstance(pv_node, Literal):
904
+ g.add((pv_node, RDF.type, pv_owl_type))
905
+ g.add((pv_node, RDFS.label, Literal(pv.text)))
906
+ # TODO: make this configurable
873
907
  # self._add_element_properties(pv_uri, pv)
874
908
  if self.metaclasses:
875
- g.add((pv_uri, RDF.type, enum_uri))
909
+ g.add((pv_node, RDF.type, enum_uri))
876
910
  has_parent = False
877
911
  if pv.is_a:
878
- self.graph.add((pv_uri, RDFS.subClassOf, self._permissible_value_uri(pv.is_a, enum_uri, e)))
912
+ self.graph.add((pv_node, RDFS.subClassOf, self._permissible_value_uri(pv.is_a, enum_uri, e)))
879
913
  has_parent = True
880
914
  for mixin in sorted(pv.mixins):
881
915
  parent = self._permissible_value_uri(mixin, enum_uri, e)
@@ -885,13 +919,33 @@ class OwlSchemaGenerator(Generator):
885
919
  has_parent = True
886
920
  self.graph.add((enum_uri, RDFS.subClassOf, parent))
887
921
  if not has_parent and self.add_root_classes:
888
- self.graph.add((pv_uri, RDFS.subClassOf, URIRef(PermissibleValue.class_class_uri)))
922
+ self.graph.add((pv_node, RDFS.subClassOf, URIRef(PermissibleValue.class_class_uri)))
889
923
  if all([pv is not None for pv in pv_uris]):
890
- self._union_of(pv_uris, node=enum_uri)
891
- for pv_uri in pv_uris:
924
+ all_is_class = all([owl_type == OWL.Class for owl_type in owl_types])
925
+ all_is_individual = all([owl_type == OWL.NamedIndividual for owl_type in owl_types])
926
+ all_is_literal = all([owl_type == RDFS.Literal for owl_type in owl_types])
927
+ sub_pred = DCTERMS.isPartOf
928
+ combo_pred = None
929
+ if all_is_class or all_is_individual or all_is_literal:
930
+ if all_is_class:
931
+ combo_pred = OWL.unionOf
932
+ # self._union_of(pv_uris, node=enum_uri)
933
+ sub_pred = RDFS.subClassOf
934
+ elif all_is_individual:
935
+ combo_pred = OWL.oneOf
936
+ # self._object_one_of(pv_uris, node=enum_uri)
937
+ sub_pred = RDF.type
938
+ elif all_is_literal:
939
+ combo_pred = OWL.oneOf
940
+ # self._object_one_of(pv_uris, node=enum_uri)
941
+ sub_pred = RDF.type
942
+ if combo_pred:
943
+ self._boolean_expression(pv_uris, combo_pred, enum_uri, owl_types=set(owl_types))
944
+ for pv_node in pv_uris:
892
945
  # this would normally be entailed, but we assert here so it is visible
893
946
  # without reasoning
894
- g.add((pv_uri, RDFS.subClassOf, enum_uri))
947
+ if not isinstance(pv_node, Literal):
948
+ g.add((pv_node, sub_pred, enum_uri))
895
949
 
896
950
  def _add_rule(self, subject: Union[URIRef, BNode], rule: ClassRule, cls: ClassDefinition):
897
951
  if not self.use_swrl:
@@ -1030,6 +1084,9 @@ class OwlSchemaGenerator(Generator):
1030
1084
  def _union_of(self, exprs: List[Union[BNode, URIRef]], **kwargs) -> Optional[Union[BNode, URIRef]]:
1031
1085
  return self._boolean_expression(exprs, OWL.unionOf, **kwargs)
1032
1086
 
1087
+ def _object_one_of(self, exprs: List[Union[BNode, URIRef]], **kwargs) -> Optional[Union[BNode, URIRef]]:
1088
+ return self._boolean_expression(exprs, OWL.oneOf, **kwargs)
1089
+
1033
1090
  def _exactly_one_of(self, exprs: List[Union[BNode, URIRef]]) -> Optional[Union[BNode, URIRef]]:
1034
1091
  if not exprs:
1035
1092
  raise ValueError("Must pass at least one")
@@ -1165,7 +1222,7 @@ class OwlSchemaGenerator(Generator):
1165
1222
  return URIRef(expanded)
1166
1223
 
1167
1224
  def _permissible_value_uri(
1168
- self, pv: Union[str, EnumDefinition], enum_uri: str, enum_def: EnumDefinition = None
1225
+ self, pv: Union[str, PermissibleValue], enum_uri: str, enum_def: EnumDefinition = None
1169
1226
  ) -> URIRef:
1170
1227
  if isinstance(pv, str):
1171
1228
  pv_name = pv
@@ -1182,6 +1239,10 @@ class OwlSchemaGenerator(Generator):
1182
1239
 
1183
1240
  def slot_owl_type(self, slot: SlotDefinition) -> URIRef:
1184
1241
  sv = self.schemaview
1242
+ if slot.implements:
1243
+ for t in ["owl:AnnotationProperty", "owl:ObjectProperty", "owl:DatatypeProperty"]:
1244
+ if t in slot.implements:
1245
+ return OWL[t.replace("owl:", "")]
1185
1246
  if slot.range is None:
1186
1247
  range = self.schemaview.schema.default_range
1187
1248
  else:
@@ -1267,6 +1328,12 @@ class OwlSchemaGenerator(Generator):
1267
1328
  show_default=True,
1268
1329
  help="Use model URIs rather than class/slot URIs",
1269
1330
  )
1331
+ @click.option(
1332
+ "--default-permissible-value-type",
1333
+ default=str(OWL.Class),
1334
+ show_default=True,
1335
+ help="Default OWL type for permissible values",
1336
+ )
1270
1337
  @click.version_option(__version__, "-V", "--version")
1271
1338
  def cli(yamlfile, metadata_profile: str, **kwargs):
1272
1339
  """Generate an OWL representation of a LinkML model
@@ -57,7 +57,7 @@ class PlantumlGenerator(Generator):
57
57
  directory: Optional[str] = None,
58
58
  load_image: bool = True,
59
59
  **_,
60
- ) -> None:
60
+ ) -> Optional[str]:
61
61
  if directory:
62
62
  os.makedirs(directory, exist_ok=True)
63
63
  if classes is not None:
@@ -110,10 +110,13 @@ class PlantumlGenerator(Generator):
110
110
  else:
111
111
  self.logger.error(f"{resp.reason} accessing {plantuml_url}")
112
112
  else:
113
- print(
114
- "@startuml\nskinparam nodesep 10\n" + "\n".join(dedup_plantumlclassdef),
115
- end="\n@enduml\n",
113
+ out = (
114
+ "@startuml\n"
115
+ "skinparam nodesep 10\n"
116
+ "hide circle\n"
117
+ "hide empty members\n" + "\n".join(dedup_plantumlclassdef) + "\n@enduml\n"
116
118
  )
119
+ return out
117
120
 
118
121
  def add_class(self, cn: ClassDefinitionName) -> str:
119
122
  """Define the class only if
@@ -130,13 +133,11 @@ class PlantumlGenerator(Generator):
130
133
  if True or cn in slot.domain_of:
131
134
  mod = self.prop_modifier(cls, slot)
132
135
  slot_defs.append(
133
- ' {field} "'
136
+ " {field} "
134
137
  + underscore(self.aliased_slot_name(slot))
135
- + '"'
136
138
  + mod
137
- + ': "'
139
+ + ": "
138
140
  + underscore(slot.range)
139
- + '"'
140
141
  + self.cardinality(slot)
141
142
  )
142
143
  self.class_generated.add(cn)
@@ -3,7 +3,6 @@ Generate JSON-LD contexts
3
3
 
4
4
  """
5
5
 
6
- import csv
7
6
  import os
8
7
  from dataclasses import dataclass, field
9
8
  from typing import Dict, Optional, Set, Union
@@ -59,7 +58,7 @@ class PrefixGenerator(Generator):
59
58
  if self.default_ns:
60
59
  self.emit_prefixes.add(self.default_ns)
61
60
 
62
- def end_schema(self, base: Optional[Union[str, Namespace]] = None, output: Optional[str] = None, **_) -> None:
61
+ def end_schema(self, base: Optional[Union[str, Namespace]] = None, output: Optional[str] = None, **_) -> str:
63
62
  context = JsonObj()
64
63
  if base:
65
64
  base = str(base)
@@ -74,31 +73,24 @@ class PrefixGenerator(Generator):
74
73
  for k, v in self.slot_class_maps.items():
75
74
  context[k] = v
76
75
 
77
- if output:
78
- output_ext = output.split(".")[-1]
76
+ if self.format == "tsv":
77
+ mapping: Dict = {} # prefix to IRI mapping
78
+ for prefix in sorted(self.emit_prefixes):
79
+ mapping[prefix] = self.namespaces[prefix]
79
80
 
80
- if output_ext == "tsv":
81
- mapping: Dict = {}
82
- for prefix in sorted(self.emit_prefixes):
83
- mapping[prefix] = self.namespaces[prefix]
81
+ items = []
82
+ for key, value in mapping.items():
83
+ items.append("\t".join([key, value]))
84
+ out = "\n".join(items)
84
85
 
85
- with open(output, "w", encoding="UTF-8") as outf:
86
- writer = csv.writer(outf, delimiter="\t")
87
- for key, value in mapping.items():
88
- writer.writerow([key, value])
89
- else:
90
- with open(output, "w", encoding="UTF-8") as outf:
91
- outf.write(as_json(context))
92
86
  else:
93
- if self.format == "tsv":
94
- mapping: Dict = {} # prefix to IRI mapping
95
- for prefix in sorted(self.emit_prefixes):
96
- mapping[prefix] = self.namespaces[prefix]
87
+ out = str(as_json(context))
97
88
 
98
- for key, value in mapping.items():
99
- print(key, value, sep="\t")
100
- else:
101
- print(as_json(context))
89
+ if output:
90
+ with open(output, "w", encoding="UTF-8") as outf:
91
+ outf.write(out)
92
+
93
+ return out
102
94
 
103
95
  def visit_class(self, cls: ClassDefinition) -> bool:
104
96
  class_def = {}
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  from dataclasses import dataclass
3
+ from typing import Optional
3
4
 
4
5
  import click
5
6
  from linkml_runtime.linkml_model.meta import ClassDefinition, SlotDefinition
@@ -26,32 +27,36 @@ class ProtoGenerator(Generator):
26
27
  # ObjectVars
27
28
  relative_slot_num: int = 0
28
29
 
29
- def __post_init__(self):
30
- super().__post_init__()
31
- self.generate_header()
30
+ def visit_schema(self, **kwargs) -> Optional[str]:
31
+ return self.generate_header()
32
32
 
33
- def generate_header(self):
34
- print(' syntax="proto3";')
35
- print(" package")
36
- print(f"// metamodel_version: {self.schema.metamodel_version}")
33
+ def generate_header(self) -> str:
34
+ items = []
35
+ items.append(' syntax="proto3";')
36
+ items.append(" package")
37
+ items.append(f"// metamodel_version: {self.schema.metamodel_version}")
37
38
  if self.schema.version:
38
- print(f"// version: {self.schema.version}")
39
+ items.append(f"// version: {self.schema.version}")
40
+ out = "\n".join(items) + "\n"
41
+ return out
39
42
 
40
- def visit_class(self, cls: ClassDefinition) -> bool:
43
+ def visit_class(self, cls: ClassDefinition) -> Optional[str]:
41
44
  if cls.mixin or cls.abstract or not cls.slots:
42
- return False
45
+ return None
46
+ items = []
43
47
  if cls.description:
44
48
  for dline in cls.description.split("\n"):
45
- print(f"// {dline}")
46
- print(f"message {camelcase(cls.name)}")
47
- print(" {")
49
+ items.append(f"// {dline}")
50
+ items.append(f"message {camelcase(cls.name)}")
51
+ items.append(" {")
52
+ out = "\n".join(items)
48
53
  self.relative_slot_num = 0
49
- return True
54
+ return out
50
55
 
51
- def end_class(self, cls: ClassDefinition) -> None:
52
- print(" }")
56
+ def end_class(self, cls: ClassDefinition) -> str:
57
+ return "\n }\n"
53
58
 
54
- def visit_class_slot(self, cls: ClassDefinition, aliased_slot_name: str, slot: SlotDefinition) -> None:
59
+ def visit_class_slot(self, cls: ClassDefinition, aliased_slot_name: str, slot: SlotDefinition) -> str:
55
60
  qual = "repeated " if slot.multivalued else ""
56
61
  slotname = lcamelcase(aliased_slot_name)
57
62
  slot_range = camelcase(slot.range)
@@ -59,7 +64,7 @@ class ProtoGenerator(Generator):
59
64
  # numbering of slots is important in the proto implementation
60
65
  # and should be determined by the rank param.
61
66
  slot.rank = 0
62
- print(f" {qual} {lcamelcase(slot_range)} {(slotname)} = {slot.rank}")
67
+ return f"\n {qual} {lcamelcase(slot_range)} {(slotname)} = {slot.rank}"
63
68
 
64
69
 
65
70
  @shared_arguments(ProtoGenerator)
@@ -30,7 +30,7 @@ else:
30
30
  from typing import Annotated
31
31
 
32
32
  from linkml.generators.pydanticgen.build import SlotResult
33
- from linkml.generators.pydanticgen.template import Import, Imports, ObjectImport
33
+ from linkml.generators.pydanticgen.template import ConditionalImport, Import, Imports, ObjectImport
34
34
 
35
35
 
36
36
  class ArrayRepresentation(Enum):
@@ -54,7 +54,14 @@ if int(PYDANTIC_VERSION[0]) >= 2:
54
54
  item_schema = handler.generate_schema(item_type)
55
55
  if item_schema.get("type", "any") != "any":
56
56
  item_schema["strict"] = True
57
- array_ref = f"any-shape-array-{item_type.__name__}"
57
+
58
+ if item_type is Any:
59
+ # Before python 3.11, `Any` type was a special object without a __name__
60
+ item_name = "Any"
61
+ else:
62
+ item_name = item_type.__name__
63
+
64
+ array_ref = f"any-shape-array-{item_name}"
58
65
 
59
66
  schema = core_schema.definitions_schema(
60
67
  core_schema.list_schema(core_schema.definition_reference_schema(array_ref)),
@@ -78,7 +85,6 @@ if int(PYDANTIC_VERSION[0]) >= 2:
78
85
  + Import(
79
86
  module="typing",
80
87
  objects=[
81
- ObjectImport(name="Annotated"),
82
88
  ObjectImport(name="Generic"),
83
89
  ObjectImport(name="Iterable"),
84
90
  ObjectImport(name="TypeVar"),
@@ -86,6 +92,12 @@ if int(PYDANTIC_VERSION[0]) >= 2:
86
92
  ObjectImport(name="get_args"),
87
93
  ],
88
94
  )
95
+ + ConditionalImport(
96
+ condition="sys.version_info.minor > 8",
97
+ module="typing",
98
+ objects=[ObjectImport(name="Annotated")],
99
+ alternative=Import(module="typing_extensions", objects=[ObjectImport(name="Annotated")]),
100
+ )
89
101
  + Import(module="pydantic", objects=[ObjectImport(name="GetCoreSchemaHandler")])
90
102
  + Import(module="pydantic_core", objects=[ObjectImport(name="CoreSchema"), ObjectImport(name="core_schema")])
91
103
  )
@@ -50,9 +50,13 @@ from linkml.generators.pydanticgen.template import (
50
50
  PydanticModule,
51
51
  TemplateModel,
52
52
  )
53
+ from linkml.utils import deprecation_warning
53
54
  from linkml.utils.generator import shared_arguments
54
55
  from linkml.utils.ifabsent_functions import ifabsent_value_declaration
55
56
 
57
+ if int(PYDANTIC_VERSION[0]) == 1:
58
+ deprecation_warning("pydantic-v1")
59
+
56
60
 
57
61
  def _get_pyrange(t: TypeDefinition, sv: SchemaView) -> str:
58
62
  pyrange = t.repr if t is not None else None
@@ -76,6 +80,7 @@ DEFAULT_IMPORTS = (
76
80
  + Import(module="decimal", objects=[ObjectImport(name="Decimal")])
77
81
  + Import(module="enum", objects=[ObjectImport(name="Enum")])
78
82
  + Import(module="re")
83
+ + Import(module="sys")
79
84
  + Import(
80
85
  module="typing",
81
86
  objects=[
@@ -214,6 +219,11 @@ class PydanticGenerator(OOCodeGenerator):
214
219
  genmeta: bool = False
215
220
  emit_metadata: bool = True
216
221
 
222
+ def __post_init__(self):
223
+ super().__post_init__()
224
+ if int(self.pydantic_version) == 1:
225
+ deprecation_warning("pydanticgen-v1")
226
+
217
227
  def compile_module(self, **kwargs) -> ModuleType:
218
228
  """
219
229
  Compiles generated python code to a module
@@ -644,10 +654,11 @@ class PydanticGenerator(OOCodeGenerator):
644
654
  for k, c in pyschema.classes.items():
645
655
  attrs = {}
646
656
  for attr_name, src_attr in c.attributes.items():
657
+ src_attr = src_attr._as_dict
647
658
  new_fields = {
648
- k: src_attr._as_dict.get(k, None)
659
+ k: src_attr.get(k, None)
649
660
  for k in PydanticAttribute.model_fields.keys()
650
- if src_attr._as_dict.get(k, None) is not None
661
+ if src_attr.get(k, None) is not None
651
662
  }
652
663
  predef_slot = predefined.get(k, {}).get(attr_name, None)
653
664
  if predef_slot is not None:
@@ -1,3 +1,4 @@
1
+ from copy import copy
1
2
  from importlib.util import find_spec
2
3
  from typing import Any, ClassVar, Dict, Generator, List, Literal, Optional, Union, overload
3
4
 
@@ -54,6 +55,10 @@ class TemplateModel(BaseModel):
54
55
  """
55
56
 
56
57
  template: ClassVar[str]
58
+ _environment: ClassVar[Environment] = Environment(
59
+ loader=PackageLoader("linkml.generators.pydanticgen", "templates"), trim_blocks=True, lstrip_blocks=True
60
+ )
61
+
57
62
  pydantic_ver: int = int(PYDANTIC_VERSION[0])
58
63
 
59
64
  def render(self, environment: Optional[Environment] = None, black: bool = False) -> str:
@@ -95,14 +100,11 @@ class TemplateModel(BaseModel):
95
100
  def environment(cls) -> Environment:
96
101
  """
97
102
  Default environment for Template models.
98
-
99
103
  uses a :class:`jinja2.PackageLoader` for the templates directory within this module
100
104
  with the ``trim_blocks`` and ``lstrip_blocks`` parameters set to ``True`` so that the
101
105
  default templates could be written in a more readable way.
102
106
  """
103
- return Environment(
104
- loader=PackageLoader("linkml.generators.pydanticgen", "templates"), trim_blocks=True, lstrip_blocks=True
105
- )
107
+ return copy(cls._environment)
106
108
 
107
109
  if int(PYDANTIC_VERSION[0]) < 2:
108
110
  # simulate pydantic 2's model_fields behavior
@@ -4,6 +4,7 @@ import os
4
4
  import re
5
5
  from copy import copy
6
6
  from dataclasses import dataclass
7
+ from pathlib import Path
7
8
  from types import ModuleType
8
9
  from typing import Callable, Dict, Iterator, List, Optional, Set, Tuple, Union
9
10
 
@@ -62,6 +63,8 @@ class PythonGenerator(Generator):
62
63
  emit_metadata: bool = True
63
64
 
64
65
  def __post_init__(self) -> None:
66
+ if isinstance(self.schema, Path):
67
+ self.schema = str(self.schema)
65
68
  self.sourcefile = self.schema
66
69
  self.schemaview = SchemaView(self.schema, base_dir=self.base_dir)
67
70
  super().__post_init__()
@@ -182,11 +185,8 @@ dataclasses._init_fn = dataclasses_init_fn_with_kwargs
182
185
  # Slots
183
186
  {self.gen_slotdefs()}"""
184
187
 
185
- def end_schema(self, **_):
186
- print(
187
- re.sub(r" +\n", "\n", self.gen_schema().replace("\t", " ")).strip(" "),
188
- end="",
189
- )
188
+ def end_schema(self, **_) -> str:
189
+ return re.sub(r" +\n", "\n", self.gen_schema().replace("\t", " ")).strip(" ")
190
190
 
191
191
  def gen_imports(self) -> str:
192
192
  list_ents = [f"from {k} import {', '.join(v)}" for k, v in self.gen_import_list().items()]
@@ -7,10 +7,12 @@ Generate a JSON LD representation of the model
7
7
 
8
8
  import os
9
9
  import urllib.parse as urlparse
10
+ from copy import deepcopy
10
11
  from dataclasses import dataclass
11
12
  from typing import List, Optional
12
13
 
13
14
  import click
15
+ from linkml_runtime.linkml_model import SchemaDefinition
14
16
  from rdflib import Graph
15
17
  from rdflib.plugin import Parser as rdflib_Parser
16
18
  from rdflib.plugin import plugins as rdflib_plugins
@@ -35,13 +37,19 @@ class RDFGenerator(Generator):
35
37
  # ObjectVars
36
38
  emit_metadata: bool = False
37
39
  context: List[str] = None
40
+ original_schema: SchemaDefinition = None
41
+ """See https://github.com/linkml/linkml/issues/871"""
42
+
43
+ def __post_init__(self):
44
+ self.original_schema = deepcopy(self.schema)
45
+ super().__post_init__()
38
46
 
39
47
  def _data(self, g: Graph) -> str:
40
48
  return g.serialize(format="turtle" if self.format == "ttl" else self.format)
41
49
 
42
- def end_schema(self, output: Optional[str] = None, context: str = None, **_) -> None:
50
+ def end_schema(self, output: Optional[str] = None, context: str = None, **_) -> str:
43
51
  gen = JSONLDGenerator(
44
- self,
52
+ self.original_schema,
45
53
  format=JSONLDGenerator.valid_formats[0],
46
54
  metadata=self.emit_metadata,
47
55
  importmap=self.importmap,
@@ -59,11 +67,12 @@ class RDFGenerator(Generator):
59
67
  base=str(self.namespaces._base),
60
68
  prefix=True,
61
69
  )
70
+ out = self._data(graph)
62
71
  if output:
63
72
  with open(output, "w", encoding="UTF-8") as outf:
64
- outf.write(self._data(graph))
65
- else:
66
- print(self._data(graph))
73
+ outf.write(out)
74
+
75
+ return out
67
76
 
68
77
 
69
78
  @shared_arguments(RDFGenerator)
@@ -22,7 +22,8 @@ class ShaclGenerator(Generator):
22
22
  # ClassVars
23
23
  closed: bool = True
24
24
  """True means add 'sh:closed=true' to all shapes, except of mixin shapes and shapes, that have parents"""
25
-
25
+ suffix: str = None
26
+ """parameterized suffix to be appended. No suffix per default."""
26
27
  generatorname = os.path.basename(__file__)
27
28
  generatorversion = "0.0.1"
28
29
  valid_formats = ["ttl"]
@@ -35,10 +36,11 @@ class ShaclGenerator(Generator):
35
36
  super().__post_init__()
36
37
  self.generate_header()
37
38
 
38
- def generate_header(self):
39
- print(f"# metamodel_version: {self.schema.metamodel_version}")
39
+ def generate_header(self) -> str:
40
+ out = f"\n# metamodel_version: {self.schema.metamodel_version}"
40
41
  if self.schema.version:
41
- print(f"# version: {self.schema.version}")
42
+ out += f"\n# version: {self.schema.version}"
43
+ return out
42
44
 
43
45
  def serialize(self, **args) -> str:
44
46
  g = self.as_graph()
@@ -59,9 +61,12 @@ class ShaclGenerator(Generator):
59
61
 
60
62
  def shape_pv(p, v):
61
63
  if v is not None:
62
- g.add((class_uri, p, v))
64
+ g.add((class_uri_with_suffix, p, v))
63
65
 
64
66
  class_uri = URIRef(sv.get_uri(c, expand=True))
67
+ class_uri_with_suffix = class_uri
68
+ if self.suffix is not None:
69
+ class_uri_with_suffix += self.suffix
65
70
  shape_pv(RDF.type, SH.NodeShape)
66
71
  shape_pv(SH.targetClass, class_uri) # TODO
67
72
  if self.closed:
@@ -211,14 +216,21 @@ def add_simple_data_type(func: Callable, r: ElementName) -> None:
211
216
 
212
217
 
213
218
  @shared_arguments(ShaclGenerator)
219
+ @click.command()
214
220
  @click.option(
215
221
  "--closed/--non-closed",
216
222
  default=True,
217
223
  show_default=True,
218
224
  help="Use '--closed' to generate closed SHACL shapes. Use '--non-closed' to generate open SHACL shapes.",
219
225
  )
226
+ @click.option(
227
+ "-s",
228
+ "--suffix",
229
+ default=None,
230
+ show_default=True,
231
+ help="Use --suffix to append given string to SHACL class name (e. g. --suffix Shape: Person becomes PersonShape).",
232
+ )
220
233
  @click.version_option(__version__, "-V", "--version")
221
- @click.command()
222
234
  def cli(yamlfile, **args):
223
235
  """Generate SHACL turtle from a LinkML model"""
224
236
  gen = ShaclGenerator(yamlfile, **args)
@@ -55,12 +55,12 @@ class ShExGenerator(Generator):
55
55
  self.namespaces.join(self.namespaces[METAMODEL_NAMESPACE_NAME], "")
56
56
  ) # URI for the metamodel
57
57
  self.base = Namespace(self.namespaces.join(self.namespaces._base, "")) # Base URI for what is being modeled
58
- self.generate_header()
59
58
 
60
- def generate_header(self):
61
- print(f"# metamodel_version: {self.schema.metamodel_version}")
59
+ def generate_header(self) -> str:
60
+ out = f"# metamodel_version: {self.schema.metamodel_version}\n"
62
61
  if self.schema.version:
63
- print(f"# version: {self.schema.version}")
62
+ out += f"# version: {self.schema.version}\n"
63
+ return out
64
64
 
65
65
  def visit_schema(self, **_):
66
66
  # Adjust the schema context to include the base model URI
@@ -80,6 +80,8 @@ class ShExGenerator(Generator):
80
80
  else:
81
81
  typeof_uri = self._class_or_type_uri(typ.typeof)
82
82
  self.shapes.append(Shape(id=model_uri, expression=typeof_uri))
83
+ if self.format != "json":
84
+ return self.generate_header()
83
85
 
84
86
  def visit_class(self, cls: ClassDefinition) -> bool:
85
87
  self.shape = Shape()
@@ -160,7 +162,7 @@ class ShExGenerator(Generator):
160
162
  else:
161
163
  constraint.valueExpr = self._class_or_type_uri(slot.range)
162
164
 
163
- def end_schema(self, output: Optional[str] = None, **_) -> None:
165
+ def end_schema(self, output: Optional[str] = None, **_) -> str:
164
166
  self.shex.shapes = self.shapes if self.shapes else [Shape()]
165
167
  shex = as_json_1(self.shex)
166
168
  if self.format == "rdf":
@@ -172,11 +174,11 @@ class ShExGenerator(Generator):
172
174
  g = Graph()
173
175
  self.namespaces.load_graph(g)
174
176
  shex = str(ShExC(self.shex, base=sfx(self.namespaces._base), namespaces=g))
177
+
175
178
  if output:
176
179
  with open(output, "w", encoding="UTF-8") as outf:
177
180
  outf.write(shex)
178
- else:
179
- print(shex)
181
+ return shex
180
182
 
181
183
  def _class_or_type_uri(
182
184
  self,