linkml 1.5.5__py3-none-any.whl → 1.5.7__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 (81) hide show
  1. linkml/__init__.py +2 -6
  2. linkml/_version.py +1 -1
  3. linkml/generators/PythonGenNotes.md +4 -4
  4. linkml/generators/__init__.py +26 -5
  5. linkml/generators/common/type_designators.py +27 -22
  6. linkml/generators/csvgen.py +4 -10
  7. linkml/generators/docgen/class.md.jinja2 +7 -0
  8. linkml/generators/docgen/class_diagram.md.jinja2 +0 -6
  9. linkml/generators/docgen/subset.md.jinja2 +54 -13
  10. linkml/generators/docgen.py +94 -92
  11. linkml/generators/dotgen.py +5 -9
  12. linkml/generators/erdiagramgen.py +58 -53
  13. linkml/generators/excelgen.py +10 -16
  14. linkml/generators/golanggen.py +11 -21
  15. linkml/generators/golrgen.py +4 -13
  16. linkml/generators/graphqlgen.py +3 -11
  17. linkml/generators/javagen.py +8 -15
  18. linkml/generators/jsonldcontextgen.py +7 -36
  19. linkml/generators/jsonldgen.py +14 -12
  20. linkml/generators/jsonschemagen.py +183 -136
  21. linkml/generators/linkmlgen.py +1 -1
  22. linkml/generators/markdowngen.py +40 -89
  23. linkml/generators/namespacegen.py +1 -2
  24. linkml/generators/oocodegen.py +22 -25
  25. linkml/generators/owlgen.py +48 -49
  26. linkml/generators/prefixmapgen.py +6 -14
  27. linkml/generators/projectgen.py +7 -14
  28. linkml/generators/protogen.py +3 -5
  29. linkml/generators/pydanticgen.py +85 -73
  30. linkml/generators/pythongen.py +89 -157
  31. linkml/generators/rdfgen.py +5 -11
  32. linkml/generators/shaclgen.py +32 -18
  33. linkml/generators/shexgen.py +19 -24
  34. linkml/generators/sparqlgen.py +5 -13
  35. linkml/generators/sqlalchemy/__init__.py +3 -2
  36. linkml/generators/sqlalchemy/sqlalchemy_declarative_template.py +7 -7
  37. linkml/generators/sqlalchemy/sqlalchemy_imperative_template.py +3 -3
  38. linkml/generators/sqlalchemygen.py +29 -27
  39. linkml/generators/sqlddlgen.py +34 -26
  40. linkml/generators/sqltablegen.py +21 -21
  41. linkml/generators/sssomgen.py +11 -13
  42. linkml/generators/summarygen.py +2 -4
  43. linkml/generators/terminusdbgen.py +7 -19
  44. linkml/generators/typescriptgen.py +10 -18
  45. linkml/generators/yamlgen.py +0 -2
  46. linkml/generators/yumlgen.py +23 -71
  47. linkml/linter/cli.py +4 -11
  48. linkml/linter/config/datamodel/config.py +17 -47
  49. linkml/linter/linter.py +2 -4
  50. linkml/linter/rules.py +34 -48
  51. linkml/reporting/__init__.py +2 -0
  52. linkml/reporting/model.py +9 -24
  53. linkml/transformers/relmodel_transformer.py +20 -33
  54. linkml/transformers/schema_renamer.py +14 -10
  55. linkml/utils/converter.py +15 -15
  56. linkml/utils/datautils.py +9 -24
  57. linkml/utils/datavalidator.py +2 -2
  58. linkml/utils/execute_tutorial.py +10 -12
  59. linkml/utils/generator.py +74 -92
  60. linkml/utils/helpers.py +4 -2
  61. linkml/utils/ifabsent_functions.py +23 -15
  62. linkml/utils/mergeutils.py +19 -35
  63. linkml/utils/rawloader.py +2 -6
  64. linkml/utils/schema_builder.py +31 -19
  65. linkml/utils/schema_fixer.py +28 -18
  66. linkml/utils/schemaloader.py +44 -89
  67. linkml/utils/schemasynopsis.py +50 -73
  68. linkml/utils/sqlutils.py +40 -30
  69. linkml/utils/typereferences.py +9 -6
  70. linkml/utils/validation.py +4 -5
  71. linkml/validators/__init__.py +2 -0
  72. linkml/validators/jsonschemavalidator.py +104 -53
  73. linkml/validators/sparqlvalidator.py +5 -15
  74. linkml/workspaces/datamodel/workspaces.py +13 -30
  75. linkml/workspaces/example_runner.py +75 -68
  76. {linkml-1.5.5.dist-info → linkml-1.5.7.dist-info}/METADATA +2 -2
  77. linkml-1.5.7.dist-info/RECORD +109 -0
  78. linkml-1.5.5.dist-info/RECORD +0 -109
  79. {linkml-1.5.5.dist-info → linkml-1.5.7.dist-info}/LICENSE +0 -0
  80. {linkml-1.5.5.dist-info → linkml-1.5.7.dist-info}/WHEEL +0 -0
  81. {linkml-1.5.5.dist-info → linkml-1.5.7.dist-info}/entry_points.txt +0 -0
@@ -2,15 +2,19 @@ import os
2
2
  from contextlib import redirect_stdout
3
3
  from dataclasses import dataclass, field
4
4
  from io import StringIO
5
- from typing import Any, Callable, Dict, List, Optional, Set, TextIO, Union
5
+ from typing import Any, Callable, Dict, List, Optional, Set, Union
6
6
 
7
7
  import click
8
8
  from jsonasobj2 import JsonObj, values
9
- from linkml_runtime.linkml_model.meta import (ClassDefinition,
10
- ClassDefinitionName, Element,
11
- EnumDefinition, SchemaDefinition,
12
- SlotDefinition, SubsetDefinition,
13
- TypeDefinition)
9
+ from linkml_runtime.linkml_model.meta import (
10
+ ClassDefinition,
11
+ ClassDefinitionName,
12
+ Element,
13
+ EnumDefinition,
14
+ SlotDefinition,
15
+ SubsetDefinition,
16
+ TypeDefinition,
17
+ )
14
18
  from linkml_runtime.utils.formatutils import be, camelcase, underscore
15
19
 
16
20
  from linkml._version import __version__
@@ -30,7 +34,7 @@ class MarkdownGenerator(Generator):
30
34
  The markdown is suitable for deployment as a MkDocs or Sphinx site
31
35
  """
32
36
 
33
- #ClassVars
37
+ # ClassVars
34
38
  generatorname = os.path.basename(__file__)
35
39
  generatorversion = "0.2.1"
36
40
  directory_output = True
@@ -71,7 +75,7 @@ class MarkdownGenerator(Generator):
71
75
  if directory:
72
76
  os.makedirs(directory, exist_ok=True)
73
77
  elif image_dir:
74
- raise ValueError(f"Image directory can only be used with '-d' option")
78
+ raise ValueError("Image directory can only be used with '-d' option")
75
79
  if image_dir:
76
80
  self.image_directory = os.path.join(directory, "images")
77
81
  if not noimages:
@@ -80,9 +84,7 @@ class MarkdownGenerator(Generator):
80
84
  if not self.no_types_dir:
81
85
  os.makedirs(os.path.join(directory, "types"), exist_ok=True)
82
86
 
83
- with open(
84
- self.exist_warning(directory, index_file), "w", encoding="UTF-8"
85
- ) as ixfile:
87
+ with open(self.exist_warning(directory, index_file), "w", encoding="UTF-8") as ixfile:
86
88
  with redirect_stdout(ixfile):
87
89
  self.frontmatter(f"{self.schema.name}")
88
90
  self.para(
@@ -92,11 +94,7 @@ class MarkdownGenerator(Generator):
92
94
 
93
95
  self.header(3, "Classes")
94
96
  for cls in sorted(self.schema.classes.values(), key=lambda c: c.name):
95
- if (
96
- not cls.is_a
97
- and not cls.mixin
98
- and self.is_secondary_ref(cls.name)
99
- ):
97
+ if not cls.is_a and not cls.mixin and self.is_secondary_ref(cls.name):
100
98
  self.class_hier(cls)
101
99
 
102
100
  self.header(3, "Mixins")
@@ -114,9 +112,7 @@ class MarkdownGenerator(Generator):
114
112
  self.enum_hier(enu)
115
113
 
116
114
  self.header(3, "Subsets")
117
- for subset in sorted(
118
- self.schema.subsets.values(), key=lambda s: s.name
119
- ):
115
+ for subset in sorted(self.schema.subsets.values(), key=lambda s: s.name):
120
116
  self.bullet(self.subset_link(subset, use_desc=True), 0)
121
117
 
122
118
  self.header(3, "Types")
@@ -131,14 +127,9 @@ class MarkdownGenerator(Generator):
131
127
  else:
132
128
  typ_typ = f"**{typ.base}**"
133
129
 
134
- self.bullet(
135
- self.type_link(
136
- typ, after_link=f" ({typ_typ})", use_desc=True
137
- )
138
- )
130
+ self.bullet(self.type_link(typ, after_link=f" ({typ_typ})", use_desc=True))
139
131
 
140
132
  def visit_class(self, cls: ClassDefinition) -> bool:
141
-
142
133
  # allow client to relabel metamodel
143
134
  mixin_local_name = self.get_metamodel_slot_name("Mixin")
144
135
  class_local_name = self.get_metamodel_slot_name("Class")
@@ -146,9 +137,7 @@ class MarkdownGenerator(Generator):
146
137
  if self.gen_classes and cls.name not in self.gen_classes:
147
138
  return False
148
139
 
149
- with open(
150
- self.exist_warning(self.dir_path(cls)), "w", encoding="UTF-8"
151
- ) as clsfile:
140
+ with open(self.exist_warning(self.dir_path(cls)), "w", encoding="UTF-8") as clsfile:
152
141
  with redirect_stdout(clsfile):
153
142
  class_curi = self.namespaces.uri_or_curie_for(
154
143
  str(self.namespaces._base), camelcase(cls.name)
@@ -164,9 +153,7 @@ class MarkdownGenerator(Generator):
164
153
  directory=self.image_directory,
165
154
  load_image=not self.noimages,
166
155
  )
167
- img_url = os.path.join(
168
- "images", os.path.basename(yg.output_file_name)
169
- )
156
+ img_url = os.path.join("images", os.path.basename(yg.output_file_name))
170
157
  else:
171
158
  yg = YumlGenerator(self)
172
159
  img_url = (
@@ -219,9 +206,7 @@ class MarkdownGenerator(Generator):
219
206
  self.header(2, "Attributes")
220
207
 
221
208
  # List all of the slots that directly belong to the class
222
- slot_list = [
223
- slot for slot in [self.schema.slots[sn] for sn in cls.slots]
224
- ]
209
+ slot_list = [slot for slot in [self.schema.slots[sn] for sn in cls.slots]]
225
210
  own_slots = [slot for slot in slot_list if cls.name in slot.domain_of]
226
211
  if own_slots:
227
212
  self.header(3, "Own")
@@ -232,9 +217,7 @@ class MarkdownGenerator(Generator):
232
217
  # List all of the inherited slots
233
218
  ancestors = set(self.ancestors(cls))
234
219
  inherited_slots = [
235
- slot
236
- for slot in slot_list
237
- if set(slot.domain_of).intersection(ancestors)
220
+ slot for slot in slot_list if set(slot.domain_of).intersection(ancestors)
238
221
  ]
239
222
  if inherited_slots:
240
223
  self.header(3, "Inherited from " + cls.is_a + ":")
@@ -246,9 +229,7 @@ class MarkdownGenerator(Generator):
246
229
  mixed_in_classes = set()
247
230
  for mixin in cls.mixins:
248
231
  mixed_in_classes.add(mixin)
249
- mixed_in_classes.update(
250
- set(self.ancestors(self.schema.classes[mixin]))
251
- )
232
+ mixed_in_classes.update(set(self.ancestors(self.schema.classes[mixin])))
252
233
  for slot in slot_list:
253
234
  mixers = set(slot.domain_of).intersection(mixed_in_classes)
254
235
  for mixer in mixers:
@@ -260,9 +241,7 @@ class MarkdownGenerator(Generator):
260
241
  return False
261
242
 
262
243
  def visit_type(self, typ: TypeDefinition) -> None:
263
- with open(
264
- self.exist_warning(self.dir_path(typ)), "w", encoding="UTF-8"
265
- ) as typefile:
244
+ with open(self.exist_warning(self.dir_path(typ)), "w", encoding="UTF-8") as typefile:
266
245
  with redirect_stdout(typefile):
267
246
  type_uri = typ.definition_uri
268
247
  type_curie = self.namespaces.curie_for(type_uri)
@@ -278,12 +257,8 @@ class MarkdownGenerator(Generator):
278
257
  self.element_properties(typ)
279
258
 
280
259
  def visit_slot(self, aliased_slot_name: str, slot: SlotDefinition) -> None:
281
- with open(
282
- self.exist_warning(self.dir_path(slot)), "w", encoding="UTF-8"
283
- ) as slotfile:
260
+ with open(self.exist_warning(self.dir_path(slot)), "w", encoding="UTF-8") as slotfile:
284
261
  with redirect_stdout(slotfile):
285
- import logging
286
-
287
262
  slot_curie = self.namespaces.uri_or_curie_for(
288
263
  str(self.namespaces._base), underscore(slot.name)
289
264
  )
@@ -312,29 +287,26 @@ class MarkdownGenerator(Generator):
312
287
  self.bullet(f"{self.class_link(rc)}")
313
288
  if aliased_slot_name == "relation":
314
289
  if slot.subproperty_of:
315
- self.bullet(
316
- f" reifies: {self.slot_link(slot.subproperty_of) if slot.subproperty_of in self.schema.slots else slot.subproperty_of}"
290
+ reifies = (
291
+ self.slot_link(slot.subproperty_of)
292
+ if slot.subproperty_of in self.schema.slots
293
+ else slot.subproperty_of
317
294
  )
295
+ self.bullet(f" reifies: {reifies}")
318
296
  self.element_properties(slot)
319
297
 
320
298
  def visit_enum(self, enum: EnumDefinition) -> None:
321
- with open(
322
- self.exist_warning(self.dir_path(enum)), "w", encoding="UTF-8"
323
- ) as enumfile:
299
+ with open(self.exist_warning(self.dir_path(enum)), "w", encoding="UTF-8") as enumfile:
324
300
  with redirect_stdout(enumfile):
325
301
  enum_curie = self.namespaces.uri_or_curie_for(
326
302
  str(self.namespaces._base), underscore(enum.name)
327
303
  )
328
304
  enum_uri = self.namespaces.uri_for(enum_curie)
329
- self.element_header(
330
- obj=enum, name=enum.name, curie=enum_curie, uri=enum_uri
331
- )
305
+ self.element_header(obj=enum, name=enum.name, curie=enum_curie, uri=enum_uri)
332
306
  self.element_properties(enum)
333
307
 
334
308
  def visit_subset(self, subset: SubsetDefinition) -> None:
335
- with open(
336
- self.exist_warning(self.dir_path(subset)), "w", encoding="UTF-8"
337
- ) as subsetfile:
309
+ with open(self.exist_warning(self.dir_path(subset)), "w", encoding="UTF-8") as subsetfile:
338
310
  with redirect_stdout(subsetfile):
339
311
  curie = self.namespaces.uri_or_curie_for(
340
312
  str(self.namespaces._base), underscore(subset.name)
@@ -367,7 +339,6 @@ class MarkdownGenerator(Generator):
367
339
  self.element_properties(subset)
368
340
 
369
341
  def element_header(self, obj: Element, name: str, curie: str, uri: str) -> None:
370
- simple_name = curie.split(":", 1)[1]
371
342
  if isinstance(obj, TypeDefinition):
372
343
  obj_type = "Type"
373
344
  elif isinstance(obj, ClassDefinition):
@@ -382,9 +353,7 @@ class MarkdownGenerator(Generator):
382
353
  obj_type = "Class"
383
354
 
384
355
  header_label = (
385
- f"{obj_type}: ~~{name}~~ _(deprecated)_"
386
- if obj.deprecated
387
- else f"{obj_type}: {name}"
356
+ f"{obj_type}: ~~{name}~~ _(deprecated)_" if obj.deprecated else f"{obj_type}: {name}"
388
357
  )
389
358
  self.header(1, header_label)
390
359
 
@@ -413,7 +382,7 @@ class MarkdownGenerator(Generator):
413
382
  def enum_list(title: str, obj: EnumDefinition) -> None:
414
383
  # This data is from the enum provided in the YAML
415
384
  self.header(2, title)
416
- print(f"| Text | Description | Meaning | Other Information |")
385
+ print("| Text | Description | Meaning | Other Information |")
417
386
  print("| :--- | :---: | :---: | ---: |")
418
387
 
419
388
  for item, item_info in obj.permissible_values.items():
@@ -507,11 +476,7 @@ class MarkdownGenerator(Generator):
507
476
  if isinstance(obj, EnumDefinition)
508
477
  else camelcase(obj.name)
509
478
  )
510
- subdir = (
511
- "/types"
512
- if isinstance(obj, TypeDefinition) and not self.no_types_dir
513
- else ""
514
- )
479
+ subdir = "/types" if isinstance(obj, TypeDefinition) and not self.no_types_dir else ""
515
480
  return f"{self.directory}{subdir}/{filename}.md"
516
481
 
517
482
  def mappings(self, obj: Union[SlotDefinition, ClassDefinition]) -> None:
@@ -641,11 +606,7 @@ class MarkdownGenerator(Generator):
641
606
  parent = self.schema.classes[obj.is_a]
642
607
  else:
643
608
  parent = None
644
- return (
645
- ""
646
- if parent and obj.description == parent.description
647
- else obj.description
648
- )
609
+ return "" if parent and obj.description == parent.description else obj.description
649
610
  return ""
650
611
 
651
612
  def _link(
@@ -668,9 +629,7 @@ class MarkdownGenerator(Generator):
668
629
  if obj is None or not self.is_secondary_ref(obj.name):
669
630
  return self.bbin(obj)
670
631
  if isinstance(obj, SlotDefinition):
671
- link_name = (
672
- (be(obj.domain) + "➞") if obj.alias else ""
673
- ) + self.aliased_slot_name(obj)
632
+ link_name = ((be(obj.domain) + "➞") if obj.alias else "") + self.aliased_slot_name(obj)
674
633
  link_ref = underscore(obj.name)
675
634
  elif isinstance(obj, TypeDefinition):
676
635
  link_name = camelcase(obj.name)
@@ -825,25 +784,17 @@ class MarkdownGenerator(Generator):
825
784
  @click.command()
826
785
  @click.option("--dir", "-d", required=True, help="Output directory")
827
786
  @click.option("--classes", "-c", multiple=True, help="Class(es) to emit")
828
- @click.option(
829
- "--map-fields", "-M", multiple=True, help="Map metamodel fields, e.g. slot=field"
830
- )
831
- @click.option(
832
- "--img", "-i", is_flag=True, help="Download YUML images to 'image' directory"
833
- )
787
+ @click.option("--map-fields", "-M", multiple=True, help="Map metamodel fields, e.g. slot=field")
788
+ @click.option("--img", "-i", is_flag=True, help="Download YUML images to 'image' directory")
834
789
  @click.option("--index-file", "-I", help="Name of markdown file that holds index")
835
790
  @click.option("--noimages", is_flag=True, help="Do not (re-)generate images")
836
791
  @click.option("--noyuml", is_flag=True, help="Do not add yUML figures to pages")
837
- @click.option(
838
- "--notypesdir", is_flag=True, help="Do not create a separate types directory"
839
- )
792
+ @click.option("--notypesdir", is_flag=True, help="Do not create a separate types directory")
840
793
  @click.option("--warnonexist", is_flag=True, help="Warn if output file already exists")
841
794
  @click.version_option(__version__, "-V", "--version")
842
795
  def cli(yamlfile, map_fields, dir, img, index_file, notypesdir, warnonexist, **kwargs):
843
796
  """Generate markdown documentation of a LinkML model"""
844
- gen = MarkdownGenerator(
845
- yamlfile, no_types_dir=notypesdir, warn_on_exist=warnonexist, **kwargs
846
- )
797
+ gen = MarkdownGenerator(yamlfile, no_types_dir=notypesdir, warn_on_exist=warnonexist, **kwargs)
847
798
  if map_fields is not None:
848
799
  gen.metamodel_name_map = {}
849
800
  for mf in map_fields:
@@ -5,7 +5,6 @@ import click
5
5
  from linkml_runtime.utils.formatutils import be, split_line
6
6
 
7
7
  from linkml._version import __version__
8
- from linkml.generators import PYTHON_GEN_VERSION
9
8
  from linkml.generators.pythongen import PythonGenerator
10
9
  from linkml.utils.generator import shared_arguments
11
10
 
@@ -13,7 +12,7 @@ from linkml.utils.generator import shared_arguments
13
12
  @dataclass
14
13
  class NamespaceGenerator(PythonGenerator):
15
14
  generatorname = os.path.basename(__file__)
16
- generatorversion = PYTHON_GEN_VERSION
15
+ generatorversion = "0.0.1"
17
16
  valid_formats = ["py"]
18
17
  visit_all_class_slots = False
19
18
 
@@ -2,12 +2,16 @@ import abc
2
2
  import re
3
3
  import unicodedata
4
4
  from dataclasses import dataclass, field
5
- from typing import List, Optional, Dict
6
-
7
- from linkml_runtime.linkml_model.meta import (ClassDefinition,
8
- EnumDefinition, EnumDefinitionName,
9
- SchemaDefinition, SlotDefinition,
10
- TypeDefinition)
5
+ from typing import Dict, List, Optional
6
+
7
+ from linkml_runtime.linkml_model.meta import (
8
+ ClassDefinition,
9
+ EnumDefinition,
10
+ EnumDefinitionName,
11
+ SchemaDefinition,
12
+ SlotDefinition,
13
+ TypeDefinition,
14
+ )
11
15
  from linkml_runtime.utils.formatutils import camelcase, lcamelcase, underscore
12
16
  from linkml_runtime.utils.schemaview import SchemaView
13
17
 
@@ -67,7 +71,6 @@ class OOClass:
67
71
 
68
72
  @dataclass
69
73
  class OOCodeGenerator(Generator):
70
-
71
74
  # ClassVars
72
75
  java_style = True
73
76
  visit_all_class_slots = False
@@ -120,7 +123,7 @@ class OOCodeGenerator(Generator):
120
123
  return label
121
124
  else:
122
125
  # add an underscore if the value starts with a digit
123
- label = re.sub("(?=^\d)", "number_", label)
126
+ label = re.sub(r"(?=^\d)", "number_", label)
124
127
 
125
128
  safe_label = ""
126
129
  for character in label:
@@ -128,33 +131,25 @@ class OOCodeGenerator(Generator):
128
131
 
129
132
  return safe_label
130
133
 
131
- def generate_enums(
132
- self, all_enums: Dict[EnumDefinitionName, EnumDefinition]
133
- ) -> Dict:
134
+ def generate_enums(self, all_enums: Dict[EnumDefinitionName, EnumDefinition]) -> Dict:
134
135
  # TODO: make an explicit class to represent how an enum is passed to the template
135
136
  enums = {}
136
137
  for enum_name, enum_original in all_enums.items():
137
- enum = {
138
- "name": camelcase(enum_name),
139
- "values": {}
140
- }
138
+ enum = {"name": camelcase(enum_name), "values": {}}
141
139
 
142
140
  if hasattr(enum_original, "description"):
143
141
  enum["description"] = enum_original.description
144
142
 
145
143
  for pv in enum_original.permissible_values.values():
146
144
  label = self.generate_enum_label(pv.text)
147
- val = {
148
- "label": label,
149
- "value": pv.text.replace('"', '\\"')
150
- }
145
+ val = {"label": label, "value": pv.text.replace('"', '\\"')}
151
146
  if hasattr(pv, "description"):
152
147
  val["description"] = pv.description
153
- else :
148
+ else:
154
149
  val["description"] = None
155
150
 
156
151
  enum["values"][label] = val
157
-
152
+
158
153
  enums[enum_name] = enum
159
154
 
160
155
  return enums
@@ -170,12 +165,14 @@ class OOCodeGenerator(Generator):
170
165
  for cn in sv.all_classes(imports=False):
171
166
  c = sv.get_class(cn)
172
167
  safe_cn = camelcase(cn)
173
- oodoc = OODocument(
174
- name=safe_cn, package=self.package, source_schema=sv.schema
175
- )
168
+ oodoc = OODocument(name=safe_cn, package=self.package, source_schema=sv.schema)
176
169
  docs.append(oodoc)
177
170
  ooclass = OOClass(
178
- name=safe_cn, description=c.description, package=self.package, fields=[], source_class=c
171
+ name=safe_cn,
172
+ description=c.description,
173
+ package=self.package,
174
+ fields=[],
175
+ source_class=c,
179
176
  )
180
177
  # currently hardcoded for java style, one class per doc
181
178
  oodoc.classes = [ooclass]
@@ -6,18 +6,23 @@ import logging
6
6
  import os
7
7
  from dataclasses import dataclass, field
8
8
  from enum import Enum, unique
9
- from typing import List, Optional, Set, TextIO, Union
9
+ from typing import Optional, TextIO, Union
10
10
 
11
11
  import click
12
- from linkml_runtime.linkml_model.meta import (ClassDefinition,
13
- ClassDefinitionName, Definition,
14
- Element, ElementName,
15
- EnumDefinition,
16
- EnumDefinitionName,
17
- SchemaDefinition, SlotDefinition,
18
- SlotDefinitionName,
19
- TypeDefinition,
20
- TypeDefinitionName)
12
+ from linkml_runtime.linkml_model.meta import (
13
+ ClassDefinition,
14
+ ClassDefinitionName,
15
+ Definition,
16
+ Element,
17
+ ElementName,
18
+ EnumDefinition,
19
+ EnumDefinitionName,
20
+ SchemaDefinition,
21
+ SlotDefinition,
22
+ SlotDefinitionName,
23
+ TypeDefinition,
24
+ TypeDefinitionName,
25
+ )
21
26
  from linkml_runtime.utils.formatutils import camelcase, underscore
22
27
  from rdflib import OWL, RDF, BNode, Graph, Literal, URIRef
23
28
  from rdflib.collection import Collection
@@ -28,7 +33,6 @@ from rdflib.plugin import plugins as rdflib_plugins
28
33
  from linkml import METAMODEL_NAMESPACE, METAMODEL_NAMESPACE_NAME
29
34
  from linkml._version import __version__
30
35
  from linkml.utils.generator import Generator, shared_arguments
31
- from linkml.utils.schemaloader import SchemaLoader
32
36
 
33
37
 
34
38
  @unique
@@ -70,6 +74,7 @@ class OwlSchemaGenerator(Generator):
70
74
  valid_formats = ["owl", "ttl"] + [
71
75
  x.name for x in rdflib_plugins(None, rdflib_Parser) if "/" not in str(x.name)
72
76
  ]
77
+ file_extension = "owl"
73
78
  visits_are_sorted = True
74
79
  uses_schemaloader = True
75
80
  requires_metamodel = True
@@ -82,6 +87,7 @@ class OwlSchemaGenerator(Generator):
82
87
  graph: Optional[Graph] = None
83
88
  top_value_uri: Optional[URIRef] = None
84
89
  type_objects: bool = field(default_factory=lambda: True)
90
+ assert_equivalent_classes: bool = False
85
91
 
86
92
  def visit_schema(self, output: Optional[str] = None, **_):
87
93
  owl_id = self.schema.id
@@ -92,9 +98,7 @@ class OwlSchemaGenerator(Generator):
92
98
  for prefix in self.metamodel.schema.emit_prefixes:
93
99
  self.graph.bind(prefix, self.metamodel.namespaces[prefix])
94
100
  for pfx in self.schema.prefixes.values():
95
- self.graph.namespace_manager.bind(
96
- pfx.prefix_prefix, URIRef(pfx.prefix_reference)
97
- )
101
+ self.graph.namespace_manager.bind(pfx.prefix_prefix, URIRef(pfx.prefix_reference))
98
102
 
99
103
  self.graph.add((base, RDF.type, OWL.Ontology))
100
104
  self._add_element_properties(base, self.schema)
@@ -112,9 +116,7 @@ class OwlSchemaGenerator(Generator):
112
116
  # add value placeholder
113
117
  if self.type_objects:
114
118
  # TODO: additional axioms, e.g. String subClassOf hasValue some string
115
- self.top_value_uri = self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME][
116
- "topValue"
117
- ]
119
+ self.top_value_uri = self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME]["topValue"]
118
120
  self.graph.add((self.top_value_uri, RDF.type, OWL.DatatypeProperty))
119
121
  self.graph.add((self.top_value_uri, RDFS.label, Literal("value")))
120
122
 
@@ -198,7 +200,7 @@ class OwlSchemaGenerator(Generator):
198
200
  k_curie = k
199
201
  try:
200
202
  k_uri = self.namespaces.uri_for(k_curie)
201
- except ValueError as e:
203
+ except ValueError:
202
204
  try:
203
205
  k_uri = self.metamodel.namespaces.uri_for(k_curie)
204
206
  except ValueError as me:
@@ -266,6 +268,13 @@ class OwlSchemaGenerator(Generator):
266
268
  cls_uri,
267
269
  )
268
270
  )
271
+
272
+ if cls.class_uri and self.assert_equivalent_classes:
273
+ eq_class_uri = self.namespaces.uri_for(cls.class_uri)
274
+ if str(eq_class_uri) != cls.definition_uri:
275
+ self.graph.add((cls_uri, OWL.equivalentClass, eq_class_uri))
276
+ self.graph.remove((cls_uri, SKOS.exactMatch, eq_class_uri))
277
+
269
278
  # If defining slots, we generate an equivalentClass entry
270
279
  # equ_node = BNode()
271
280
  # self.graph.add((cls_uri, OWL.equivalentClass, equ_node))
@@ -317,7 +326,7 @@ class OwlSchemaGenerator(Generator):
317
326
  slot_uri = self.namespaces.uri_for(slot.slot_uri)
318
327
  if slot_uri == "rdf:type":
319
328
  logging.warning(
320
- f"rdflib may have issues serializing rdf:type with turtle serializer"
329
+ "rdflib may have issues serializing rdf:type with turtle serializer"
321
330
  )
322
331
  if slot.required:
323
332
  if slot.multivalued:
@@ -346,9 +355,7 @@ class OwlSchemaGenerator(Generator):
346
355
  if slot.multivalued:
347
356
  # restriction(slot only type)
348
357
  self.graph.add((slot_node, RDF.type, OWL.Restriction))
349
- self.graph.add(
350
- (slot_node, OWL.allValuesFrom, self._range_uri(slot))
351
- )
358
+ self.graph.add((slot_node, OWL.allValuesFrom, self._range_uri(slot)))
352
359
  self.graph.add((slot_node, OWL.onProperty, slot_uri))
353
360
  else:
354
361
  # intersectionOf(restriction(slot only type) restriction(slot max 1 type))
@@ -370,13 +377,10 @@ class OwlSchemaGenerator(Generator):
370
377
  @param slot:
371
378
  @return:
372
379
  """
373
- # determine if this is a slot that has been induced by slot_usage; if so the meaning of the slot is context-specific
374
- # and should not be used for global properties
375
- if (
376
- slot.alias is not None
377
- and slot.alias != slot.name
378
- and slot.alias in self.schema.slots
379
- ):
380
+ # determine if this is a slot that has been induced by slot_usage; if so
381
+ # the meaning of the slot is context-specific and should not be used for
382
+ # global properties
383
+ if slot.alias is not None and slot.alias != slot.name and slot.alias in self.schema.slots:
380
384
  logging.debug(
381
385
  f"SKIPPING slot induced by slot_usage: {slot.alias} // {slot.name} // {slot}"
382
386
  )
@@ -492,9 +496,7 @@ class OwlSchemaGenerator(Generator):
492
496
  g.add(
493
497
  (
494
498
  enum_uri,
495
- self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME][
496
- "permissible_values"
497
- ],
499
+ self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME]["permissible_values"],
498
500
  pv_uri,
499
501
  )
500
502
  )
@@ -512,8 +514,7 @@ class OwlSchemaGenerator(Generator):
512
514
  if k in metamodel.schema.slots:
513
515
  defining_slot = self.metamodel.schema.slots[k]
514
516
  if v is not None and (
515
- "owl" in defining_slot.in_subset
516
- or "OwlProfile" in defining_slot.in_subset
517
+ "owl" in defining_slot.in_subset or "OwlProfile" in defining_slot.in_subset
517
518
  ):
518
519
  if isinstance(v, list):
519
520
  ve = v
@@ -532,18 +533,12 @@ class OwlSchemaGenerator(Generator):
532
533
  ),
533
534
  ):
534
535
  return
535
- if (
536
- k == "name"
537
- and isinstance(el, SlotDefinition)
538
- and el.alias is not None
539
- ):
536
+ if k == "name" and isinstance(el, SlotDefinition) and el.alias is not None:
540
537
  prop_uri = RDFS.label
541
538
  e = el.alias
542
539
  else:
543
540
  prop_uri = URIRef(
544
- self.metamodel.namespaces.uri_for(
545
- defining_slot.slot_uri
546
- )
541
+ self.metamodel.namespaces.uri_for(defining_slot.slot_uri)
547
542
  )
548
543
  object = self._as_rdf_element(e, defining_slot)
549
544
  if object is not None:
@@ -568,7 +563,7 @@ class OwlSchemaGenerator(Generator):
568
563
  elif element_name in self.metamodel.schema.enums:
569
564
  return self._enum_uri(element_name)
570
565
  else:
571
- logging.warning(f"Unknown range {defining_slot.range}")
566
+ logging.warning(f"Unknown range {parent_slot.range}")
572
567
  return None
573
568
 
574
569
  def _range_is_datatype(self, slot: SlotDefinition) -> bool:
@@ -613,9 +608,7 @@ class OwlSchemaGenerator(Generator):
613
608
 
614
609
  def _add_metamodel_class(self, cname: str) -> None:
615
610
  metac = self.metamodel.schema.classes[cname]
616
- metac_uri = self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME][
617
- camelcase(metac.name)
618
- ]
611
+ metac_uri = self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME][camelcase(metac.name)]
619
612
  self.graph.add((metac_uri, RDF.type, OWL.Class))
620
613
  # self._add_element_properties(metac_uri, metac)
621
614
 
@@ -679,6 +672,12 @@ class OwlSchemaGenerator(Generator):
679
672
  show_default=True,
680
673
  help="Suffix to append to schema id to generate OWL Ontology IRI",
681
674
  )
675
+ @click.option(
676
+ "--assert-equivalent-classes/--no-assert-equivalent-classes",
677
+ default=False,
678
+ show_default=True,
679
+ help="If true, add owl:equivalentClass between a class and a class_uri",
680
+ )
682
681
  @click.version_option(__version__, "-V", "--version")
683
682
  def cli(yamlfile, metadata_profile: str, **kwargs):
684
683
  """Generate an OWL representation of a LinkML model
@@ -700,9 +699,9 @@ def cli(yamlfile, metadata_profile: str, **kwargs):
700
699
  else:
701
700
  metadata_profile = MetadataProfile.linkml
702
701
  print(
703
- OwlSchemaGenerator(
704
- yamlfile, metadata_profile=metadata_profile, **kwargs
705
- ).serialize(**kwargs)
702
+ OwlSchemaGenerator(yamlfile, metadata_profile=metadata_profile, **kwargs).serialize(
703
+ **kwargs
704
+ )
706
705
  )
707
706
 
708
707