linkml 1.8.3__py3-none-any.whl → 1.8.4__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 (40) hide show
  1. linkml/generators/__init__.py +2 -0
  2. linkml/generators/docgen/index.md.jinja2 +6 -6
  3. linkml/generators/docgen.py +64 -14
  4. linkml/generators/golanggen.py +3 -1
  5. linkml/generators/jsonschemagen.py +4 -2
  6. linkml/generators/owlgen.py +36 -17
  7. linkml/generators/projectgen.py +13 -11
  8. linkml/generators/pydanticgen/array.py +340 -56
  9. linkml/generators/pydanticgen/build.py +4 -2
  10. linkml/generators/pydanticgen/pydanticgen.py +35 -16
  11. linkml/generators/pydanticgen/template.py +108 -3
  12. linkml/generators/pydanticgen/templates/imports.py.jinja +11 -3
  13. linkml/generators/pydanticgen/templates/module.py.jinja +1 -3
  14. linkml/generators/pydanticgen/templates/validator.py.jinja +2 -2
  15. linkml/generators/pythongen.py +12 -10
  16. linkml/generators/shaclgen.py +34 -10
  17. linkml/generators/sparqlgen.py +3 -1
  18. linkml/generators/sqlalchemygen.py +5 -3
  19. linkml/generators/sqltablegen.py +4 -2
  20. linkml/generators/typescriptgen.py +13 -6
  21. linkml/linter/linter.py +2 -1
  22. linkml/transformers/logical_model_transformer.py +3 -3
  23. linkml/transformers/relmodel_transformer.py +18 -4
  24. linkml/utils/converter.py +3 -1
  25. linkml/utils/exceptions.py +11 -0
  26. linkml/utils/execute_tutorial.py +22 -20
  27. linkml/utils/generator.py +6 -4
  28. linkml/utils/mergeutils.py +4 -2
  29. linkml/utils/schema_fixer.py +5 -5
  30. linkml/utils/schemaloader.py +5 -3
  31. linkml/utils/sqlutils.py +3 -1
  32. linkml/validator/plugins/pydantic_validation_plugin.py +1 -1
  33. linkml/validators/jsonschemavalidator.py +3 -1
  34. linkml/validators/sparqlvalidator.py +5 -3
  35. linkml/workspaces/example_runner.py +3 -1
  36. {linkml-1.8.3.dist-info → linkml-1.8.4.dist-info}/METADATA +3 -1
  37. {linkml-1.8.3.dist-info → linkml-1.8.4.dist-info}/RECORD +40 -39
  38. {linkml-1.8.3.dist-info → linkml-1.8.4.dist-info}/LICENSE +0 -0
  39. {linkml-1.8.3.dist-info → linkml-1.8.4.dist-info}/WHEEL +0 -0
  40. {linkml-1.8.3.dist-info → linkml-1.8.4.dist-info}/entry_points.txt +0 -0
@@ -45,6 +45,9 @@ from linkml.generators.python.python_ifabsent_processor import PythonIfAbsentPro
45
45
  from linkml.utils import deprecation_warning
46
46
  from linkml.utils.generator import shared_arguments
47
47
 
48
+ logger = logging.getLogger(__name__)
49
+
50
+
48
51
  if int(PYDANTIC_VERSION[0]) == 1:
49
52
  deprecation_warning("pydantic-v1")
50
53
 
@@ -189,7 +192,7 @@ class PydanticGenerator(OOCodeGenerator, LifecycleMixin):
189
192
  template_dir: Optional[Union[str, Path]] = None
190
193
  """
191
194
  Override templates for each PydanticTemplateModel.
192
-
195
+
193
196
  Directory with templates that override the default :attr:`.PydanticTemplateModel.template`
194
197
  for each class. If a matching template is not found in the override directory,
195
198
  the default templates will be used.
@@ -218,7 +221,7 @@ class PydanticGenerator(OOCodeGenerator, LifecycleMixin):
218
221
  )
219
222
 
220
223
  """
221
- imports: Optional[List[Import]] = None
224
+ imports: Optional[Union[List[Import], Imports]] = None
222
225
  """
223
226
  Additional imports to inject into generated module.
224
227
 
@@ -266,6 +269,13 @@ class PydanticGenerator(OOCodeGenerator, LifecycleMixin):
266
269
  else:
267
270
  from typing_extensions import Literal
268
271
 
272
+ """
273
+ sort_imports: bool = True
274
+ """
275
+ Before returning from :meth:`.PydanticGenerator.render`, sort imports with :meth:`.Imports.sort`
276
+
277
+ Default ``True``, but optional in case import order must be explicitly given,
278
+ eg. to avoid circular import errors in complex generator subclasses.
269
279
  """
270
280
  metadata_mode: Union[MetadataMode, str, None] = MetadataMode.AUTO
271
281
  """
@@ -358,8 +368,8 @@ class PydanticGenerator(OOCodeGenerator, LifecycleMixin):
358
368
  try:
359
369
  return compile_python(pycode)
360
370
  except NameError as e:
361
- logging.error(f"Code:\n{pycode}")
362
- logging.error(f"Error compiling generated python code: {e}")
371
+ logger.error(f"Code:\n{pycode}")
372
+ logger.error(f"Error compiling generated python code: {e}")
363
373
  raise e
364
374
 
365
375
  def _get_classes(self, sv: SchemaView) -> Tuple[List[ClassDefinition], Optional[List[ClassDefinition]]]:
@@ -448,11 +458,12 @@ class PydanticGenerator(OOCodeGenerator, LifecycleMixin):
448
458
 
449
459
  def generate_slot(self, slot: SlotDefinition, cls: ClassDefinition) -> SlotResult:
450
460
  slot_args = {
451
- k: slot._as_dict.get(k, None)
461
+ k: getattr(slot, k, None)
452
462
  for k in PydanticAttribute.model_fields.keys()
453
- if slot._as_dict.get(k, None) is not None
463
+ if getattr(slot, k, None) is not None
454
464
  }
455
- slot_args["name"] = underscore(slot.name)
465
+ slot_alias = slot.alias if slot.alias else slot.name
466
+ slot_args["name"] = underscore(slot_alias)
456
467
  slot_args["description"] = slot.description.replace('"', '\\"') if slot.description is not None else None
457
468
  predef = self.predefined_slot_values.get(camelcase(cls.name), {}).get(slot.name, None)
458
469
  if predef is not None:
@@ -665,7 +676,7 @@ class PydanticGenerator(OOCodeGenerator, LifecycleMixin):
665
676
  else:
666
677
  # TODO: default ranges in schemagen
667
678
  # pyrange = 'str'
668
- # logging.error(f'range: {s.range} is unknown')
679
+ # logger.error(f'range: {s.range} is unknown')
669
680
  raise Exception(f"range: {slot_range}")
670
681
  return pyrange
671
682
 
@@ -921,14 +932,20 @@ class PydanticGenerator(OOCodeGenerator, LifecycleMixin):
921
932
  return Import(module=module, objects=[ObjectImport(name=camelcase(class_name))], is_schema=True)
922
933
 
923
934
  def render(self) -> PydanticModule:
935
+ """
936
+ Render the schema to a :class:`PydanticModule` model
937
+ """
924
938
  sv: SchemaView
925
939
  sv = self.schemaview
926
940
 
927
941
  # imports
928
942
  imports = DEFAULT_IMPORTS
929
943
  if self.imports is not None:
930
- for i in self.imports:
931
- imports += i
944
+ if isinstance(self.imports, Imports):
945
+ imports += self.imports
946
+ else:
947
+ for i in self.imports:
948
+ imports += i
932
949
  if self.split_mode == SplitMode.FULL:
933
950
  imports += self._get_imports()
934
951
 
@@ -965,13 +982,14 @@ class PydanticGenerator(OOCodeGenerator, LifecycleMixin):
965
982
  class_results = self.after_generate_classes(class_results, sv)
966
983
 
967
984
  classes = {r.cls.name: r.cls for r in class_results}
968
-
969
985
  injected_classes = self._clean_injected_classes(injected_classes)
970
986
 
987
+ imports.render_sorted = self.sort_imports
988
+
971
989
  module = PydanticModule(
972
990
  metamodel_version=self.schema.metamodel_version,
973
991
  version=self.schema.version,
974
- python_imports=imports.imports,
992
+ python_imports=imports,
975
993
  base_model=base_model,
976
994
  injected_classes=injected_classes,
977
995
  enums=enums,
@@ -987,7 +1005,8 @@ class PydanticGenerator(OOCodeGenerator, LifecycleMixin):
987
1005
 
988
1006
  Args:
989
1007
  rendered_module ( :class:`.PydanticModule` ): Optional, if schema was previously
990
- rendered with :meth:`.render` , use that, otherwise :meth:`.render` fresh.
1008
+ rendered with :meth:`~.PydanticGenerator.render` , use that,
1009
+ otherwise :meth:`~.PydanticGenerator.render` fresh.
991
1010
  """
992
1011
  if rendered_module is not None:
993
1012
  module = rendered_module
@@ -1147,9 +1166,9 @@ def _ensure_inits(paths: List[Path]):
1147
1166
  help="""
1148
1167
  Optional jinja2 template directory to use for class generation.
1149
1168
 
1150
- Pass a directory containing templates with the same name as any of the default
1151
- :class:`.PydanticTemplateModel` templates to override them. The given directory will be
1152
- searched for matching templates, and use the default templates as a fallback
1169
+ Pass a directory containing templates with the same name as any of the default
1170
+ :class:`.PydanticTemplateModel` templates to override them. The given directory will be
1171
+ searched for matching templates, and use the default templates as a fallback
1153
1172
  if an override is not found
1154
1173
 
1155
1174
  Available templates to override:
@@ -1,5 +1,6 @@
1
+ import sys
1
2
  from importlib.util import find_spec
2
- from typing import Any, ClassVar, Dict, Generator, List, Literal, Optional, Union
3
+ from typing import Any, ClassVar, Dict, Generator, List, Literal, Optional, Tuple, Union, get_args
3
4
 
4
5
  from jinja2 import Environment, PackageLoader
5
6
  from pydantic import BaseModel, Field, field_validator
@@ -28,6 +29,14 @@ else:
28
29
  return f
29
30
 
30
31
 
32
+ IMPORT_GROUPS = Literal["future", "stdlib", "thirdparty", "local", "conditional"]
33
+ """
34
+ See :attr:`.Import.group` and :attr:`.Imports.sort`
35
+
36
+ Order of this literal is used in sort and therefore not arbitrary.
37
+ """
38
+
39
+
31
40
  class PydanticTemplateModel(TemplateModel):
32
41
  """
33
42
  Metaclass to render pydantic models with jinja templates.
@@ -292,6 +301,28 @@ class Import(PydanticTemplateModel):
292
301
  Used primarily in split schema generation, see :func:`.pydanticgen.generate_split` for example usage.
293
302
  """
294
303
 
304
+ @computed_field
305
+ def group(self) -> IMPORT_GROUPS:
306
+ """
307
+ Import group used when sorting
308
+
309
+ * ``future`` - from `__future__` import...
310
+ * ``stdlib`` - ... the standard library
311
+ * ``thirdparty`` - other dependencies not in the standard library
312
+ * ``local`` - relative imports (eg. from split generation)
313
+ * ``conditional`` - a :class:`.ConditionalImport`
314
+ """
315
+ if self.module == "__future__":
316
+ return "future"
317
+ elif sys.version_info.minor >= 10 and self.module in sys.stdlib_module_names:
318
+ return "stdlib"
319
+ elif sys.version_info.minor < 10 and self.module in _some_stdlib_module_names:
320
+ return "stdlib"
321
+ elif self.module.startswith("."):
322
+ return "local"
323
+ else:
324
+ return "thirdparty"
325
+
295
326
  def merge(self, other: "Import") -> List["Import"]:
296
327
  """
297
328
  Merge one import with another, see :meth:`.Imports` for an example.
@@ -346,6 +377,16 @@ class Import(PydanticTemplateModel):
346
377
  # one is a module, the other imports objects, keep both
347
378
  return [self, other]
348
379
 
380
+ def sort(self) -> None:
381
+ """
382
+ Sort imported objects
383
+
384
+ * First by whether the first letter is capitalized or not,
385
+ * Then alphabetically (by object name rather than alias)
386
+ """
387
+ if self.objects:
388
+ self.objects = sorted(self.objects, key=lambda obj: (obj.name[0].islower(), obj.name))
389
+
349
390
 
350
391
  class ConditionalImport(Import):
351
392
  """
@@ -391,6 +432,17 @@ class ConditionalImport(Import):
391
432
  condition: str
392
433
  alternative: Import
393
434
 
435
+ @computed_field
436
+ def group(self) -> Literal["conditional"]:
437
+ return "conditional"
438
+
439
+ def sort(self) -> None:
440
+ """
441
+ :meth:`.Import.sort` called for self and :attr:`.alternative`
442
+ """
443
+ super(ConditionalImport, self).sort()
444
+ self.alternative.sort()
445
+
394
446
 
395
447
  class Imports(PydanticTemplateModel):
396
448
  """
@@ -427,6 +479,10 @@ class Imports(PydanticTemplateModel):
427
479
  template: ClassVar[str] = "imports.py.jinja"
428
480
 
429
481
  imports: List[Union[Import, ConditionalImport]] = Field(default_factory=list)
482
+ group_order: Tuple[str, ...] = get_args(IMPORT_GROUPS)
483
+ """Order in which to sort imports by their :attr:`.Import.group`"""
484
+ render_sorted: bool = True
485
+ """When rendering, render in sorted groups"""
430
486
 
431
487
  @classmethod
432
488
  def _merge(
@@ -484,13 +540,17 @@ class Imports(PydanticTemplateModel):
484
540
  break
485
541
 
486
542
  # SPECIAL CASE - __future__ annotations must happen at the top of a file
543
+ # sort here outside of sort method because our imports are invalid without it,
544
+ # where calling ``sort`` should be optional.
487
545
  imports = sorted(imports, key=lambda i: i.module == "__future__", reverse=True)
488
546
  return imports
489
547
 
490
548
  def __add__(self, other: Union[Import, "Imports", List[Import]]) -> "Imports":
491
549
  imports = self.imports.copy()
492
550
  imports = self._merge(imports, other)
493
- return Imports.model_construct(imports=imports)
551
+ return Imports.model_construct(
552
+ imports=imports, **{k: getattr(self, k, None) for k in self.model_fields if k != "imports"}
553
+ )
494
554
 
495
555
  def __len__(self) -> int:
496
556
  return len(self.imports)
@@ -552,6 +612,36 @@ class Imports(PydanticTemplateModel):
552
612
  merged_imports = cls._merge(merged_imports, i)
553
613
  return merged_imports
554
614
 
615
+ @computed_field
616
+ def import_groups(self) -> List[IMPORT_GROUPS]:
617
+ """
618
+ List of what group each import belongs to
619
+ """
620
+ return [i.group for i in self.imports]
621
+
622
+ def sort(self) -> None:
623
+ """
624
+ Sort imports recursively, mimicking isort:
625
+
626
+ * First by :attr:`.Import.group` according to :attr:`.Imports.group_order`
627
+ * Then by whether the :class:`.Import` has any objects
628
+ (``import module`` comes before ``from module import name``)
629
+ * Then alphabetically by module name
630
+ """
631
+
632
+ def _sort_key(i: Import) -> Tuple[int, int, str]:
633
+ return (self.group_order.index(i.group), int(i.objects is not None), i.module)
634
+
635
+ imports = sorted(self.imports, key=_sort_key)
636
+ for i in imports:
637
+ i.sort()
638
+ self.imports = imports
639
+
640
+ def render(self, environment: Optional[Environment] = None, black: bool = False) -> str:
641
+ if self.render_sorted:
642
+ self.sort()
643
+ return super(Imports, self).render(environment=environment, black=black)
644
+
555
645
 
556
646
  class PydanticModule(PydanticTemplateModel):
557
647
  """
@@ -565,7 +655,7 @@ class PydanticModule(PydanticTemplateModel):
565
655
  version: Optional[str] = None
566
656
  base_model: PydanticBaseModel = PydanticBaseModel()
567
657
  injected_classes: Optional[List[str]] = None
568
- python_imports: List[Union[Import, ConditionalImport]] = Field(default_factory=list)
658
+ python_imports: Union[Imports, List[Union[Import, ConditionalImport]]] = Imports()
569
659
  enums: Dict[str, PydanticEnum] = Field(default_factory=dict)
570
660
  classes: Dict[str, PydanticClass] = Field(default_factory=dict)
571
661
  meta: Optional[Dict[str, Any]] = None
@@ -573,6 +663,21 @@ class PydanticModule(PydanticTemplateModel):
573
663
  Metadata for the schema to be included in a linkml_meta module-level instance of LinkMLMeta
574
664
  """
575
665
 
666
+ @field_validator("python_imports", mode="after")
667
+ @classmethod
668
+ def cast_imports(cls, imports: Union[Imports, List[Union[Import, ConditionalImport]]]) -> Imports:
669
+ if isinstance(imports, list):
670
+ imports = Imports(imports=imports)
671
+ return imports
672
+
576
673
  @computed_field
577
674
  def class_names(self) -> List[str]:
578
675
  return [c.name for c in self.classes.values()]
676
+
677
+
678
+ _some_stdlib_module_names = {"copy", "datetime", "decimal", "enum", "inspect", "os", "re", "sys", "typing"}
679
+ """
680
+ sys.stdlib_module_names is only present in 3.10 and later
681
+ so we make a cheap copy of the stdlib modules that we commonly use here,
682
+ but this should be removed whenever support for 3.9 is dropped.
683
+ """
@@ -20,12 +20,20 @@ from {{ module }} import (
20
20
  {%- endif %}
21
21
  {%- endif %}
22
22
  {% endmacro %}
23
+ {#- For when used with Import model -#}
23
24
  {%- if module %}
24
25
  {{ import_(module, alias, objects) }}
25
26
  {% endif -%}
26
27
  {#- For when used with Imports container -#}
27
28
  {%- if imports -%}
28
- {%- for i in imports -%}
29
- {{ i }}
30
- {%- endfor -%}
29
+ {%- if render_sorted -%}
30
+ {% for i in range(imports | length) %}
31
+ {{ imports[i] }}
32
+ {%- if not loop.last and import_groups[i] != import_groups[i+1] %}{{ '\n' }}{% endif -%}
33
+ {% endfor %}
34
+ {%- else -%}
35
+ {%- for import in imports -%}
36
+ {{ import }}
37
+ {%- endfor -%}
38
+ {%- endif -%}
31
39
  {% endif -%}
@@ -1,6 +1,4 @@
1
- {% for import in python_imports %}
2
- {{ import }}
3
- {%- endfor -%}
1
+ {{ python_imports }}
4
2
 
5
3
  metamodel_version = "{{metamodel_version}}"
6
4
  version = "{{version if version else None}}"
@@ -3,9 +3,9 @@
3
3
  pattern=re.compile(r"{{pattern}}")
4
4
  if isinstance(v,list):
5
5
  for element in v:
6
- if not pattern.match(element):
6
+ if isinstance(v, str) and not pattern.match(element):
7
7
  raise ValueError(f"Invalid {{name}} format: {element}")
8
8
  elif isinstance(v,str):
9
9
  if not pattern.match(v):
10
10
  raise ValueError(f"Invalid {{name}} format: {v}")
11
- return v
11
+ return v
@@ -32,6 +32,8 @@ from linkml._version import __version__
32
32
  from linkml.generators.python.python_ifabsent_processor import PythonIfAbsentProcessor
33
33
  from linkml.utils.generator import Generator, shared_arguments
34
34
 
35
+ logger = logging.getLogger(__name__)
36
+
35
37
 
36
38
  @dataclass
37
39
  class PythonGenerator(Generator):
@@ -57,10 +59,10 @@ class PythonGenerator(Generator):
57
59
  dataclass_repr: bool = False
58
60
  """
59
61
  Whether generated dataclasses should also generate a default __repr__ method.
60
-
62
+
61
63
  Default ``False`` so that the parent :class:`linkml_runtime.utils.yamlutils.YAMLRoot` 's
62
64
  ``__repr__`` method is inherited for model pretty printing.
63
-
65
+
64
66
  References:
65
67
  - https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass
66
68
  """
@@ -75,7 +77,7 @@ class PythonGenerator(Generator):
75
77
  if self.format is None:
76
78
  self.format = self.valid_formats[0]
77
79
  if self.schema.default_prefix == "linkml" and not self.genmeta:
78
- logging.error("Generating metamodel without --genmeta is highly inadvisable!")
80
+ logger.error("Generating metamodel without --genmeta is highly inadvisable!")
79
81
  if not self.schema.source_file and isinstance(self.sourcefile, str) and "\n" not in self.sourcefile:
80
82
  self.schema.source_file = os.path.basename(self.sourcefile)
81
83
 
@@ -88,8 +90,8 @@ class PythonGenerator(Generator):
88
90
  try:
89
91
  return compile_python(pycode)
90
92
  except NameError as e:
91
- logging.error(f"Code:\n{pycode}")
92
- logging.error(f"Error compiling generated python code: {e}")
93
+ logger.error(f"Code:\n{pycode}")
94
+ logger.error(f"Error compiling generated python code: {e}")
93
95
  raise e
94
96
 
95
97
  def visit_schema(self, **kwargs) -> None:
@@ -944,11 +946,11 @@ dataclasses._init_fn = dataclasses_init_fn_with_kwargs
944
946
 
945
947
  def forward_reference(self, slot_range: str, owning_class: str) -> bool:
946
948
  """Determine whether slot_range is a forward reference"""
947
- # logging.info(f"CHECKING: {slot_range} {owning_class}")
949
+ # logger.info(f"CHECKING: {slot_range} {owning_class}")
948
950
  if (slot_range in self.schema.classes and self.schema.classes[slot_range].imported_from) or (
949
951
  slot_range in self.schema.enums and self.schema.enums[slot_range].imported_from
950
952
  ):
951
- logging.info(
953
+ logger.info(
952
954
  f"FALSE: FORWARD: {slot_range} {owning_class} // IMP={self.schema.classes[slot_range].imported_from}"
953
955
  )
954
956
  return False
@@ -957,10 +959,10 @@ dataclasses._init_fn = dataclasses_init_fn_with_kwargs
957
959
  clist = [x.name for x in self._sort_classes(self.schema.classes.values())]
958
960
  for cname in clist:
959
961
  if cname == owning_class:
960
- logging.info(f"TRUE: OCCURS SAME: {cname} == {slot_range} owning: {owning_class}")
962
+ logger.info(f"TRUE: OCCURS SAME: {cname} == {slot_range} owning: {owning_class}")
961
963
  return True # Occurs on or after
962
964
  elif cname == slot_range:
963
- logging.info(f"FALSE: OCCURS BEFORE: {cname} == {slot_range} owning: {owning_class}")
965
+ logger.info(f"FALSE: OCCURS BEFORE: {cname} == {slot_range} owning: {owning_class}")
964
966
  return False # Occurs before
965
967
  return True
966
968
 
@@ -1214,7 +1216,7 @@ def cli(
1214
1216
  )
1215
1217
  if validate:
1216
1218
  mod = gen.compile_module()
1217
- logging.info(f"Module {mod} compiled successfully")
1219
+ logger.info(f"Module {mod} compiled successfully")
1218
1220
  print(gen.serialize(emit_metadata=head, **args))
1219
1221
 
1220
1222
 
@@ -5,19 +5,21 @@ from typing import Callable, List
5
5
 
6
6
  import click
7
7
  from jsonasobj2 import JsonObj, as_dict
8
- from linkml_runtime.linkml_model.meta import ElementName
8
+ from linkml_runtime.linkml_model.meta import ClassDefinition, ElementName
9
9
  from linkml_runtime.utils.formatutils import underscore
10
10
  from linkml_runtime.utils.schemaview import SchemaView
11
11
  from linkml_runtime.utils.yamlutils import TypedNode, extended_float, extended_int, extended_str
12
12
  from rdflib import BNode, Graph, Literal, URIRef
13
13
  from rdflib.collection import Collection
14
- from rdflib.namespace import RDF, RDFS, SH, XSD
14
+ from rdflib.namespace import RDF, SH, XSD
15
15
 
16
16
  from linkml._version import __version__
17
17
  from linkml.generators.shacl.shacl_data_type import ShaclDataType
18
18
  from linkml.generators.shacl.shacl_ifabsent_processor import ShaclIfAbsentProcessor
19
19
  from linkml.utils.generator import Generator, shared_arguments
20
20
 
21
+ logger = logging.getLogger(__name__)
22
+
21
23
 
22
24
  @dataclass
23
25
  class ShaclGenerator(Generator):
@@ -74,9 +76,6 @@ class ShaclGenerator(Generator):
74
76
  shape_pv(RDF.type, SH.NodeShape)
75
77
  shape_pv(SH.targetClass, class_uri) # TODO
76
78
 
77
- if c.is_a:
78
- shape_pv(RDFS.subClassOf, URIRef(sv.get_uri(c.is_a, expand=True)))
79
-
80
79
  if self.closed:
81
80
  if c.mixin or c.abstract:
82
81
  shape_pv(SH.closed, Literal(False))
@@ -88,9 +87,9 @@ class ShaclGenerator(Generator):
88
87
  shape_pv(SH.name, Literal(c.title))
89
88
  if c.description is not None:
90
89
  shape_pv(SH.description, Literal(c.description))
91
- list_node = BNode()
92
- Collection(g, list_node, [RDF.type])
93
- shape_pv(SH.ignoredProperties, list_node)
90
+
91
+ shape_pv(SH.ignoredProperties, self._build_ignored_properties(g, c))
92
+
94
93
  if c.annotations and self.include_annotations:
95
94
  self._add_annotations(shape_pv, c)
96
95
  order = 0
@@ -130,7 +129,7 @@ class ShaclGenerator(Generator):
130
129
  # slot definition, as both are mapped to sh:in in SHACL
131
130
  if s.equals_string or s.equals_string_in:
132
131
  error = "'equals_string'/'equals_string_in' and 'any_of' are mutually exclusive"
133
- raise ValueError(f'{TypedNode.yaml_loc(s, suffix="")} {error}')
132
+ raise ValueError(f'{TypedNode.yaml_loc(str(s), suffix="")} {error}')
134
133
 
135
134
  or_node = BNode()
136
135
  prop_pv(SH["or"], or_node)
@@ -244,7 +243,7 @@ class ShaclGenerator(Generator):
244
243
  if rt.annotations and self.include_annotations:
245
244
  self._add_annotations(func, rt)
246
245
  else:
247
- logging.error(f"No URI for type {rt.name}")
246
+ logger.error(f"No URI for type {rt.name}")
248
247
 
249
248
  def _and_equals_string(self, g: Graph, func: Callable, values: List) -> None:
250
249
  pv_node = BNode()
@@ -301,6 +300,31 @@ class ShaclGenerator(Generator):
301
300
  )
302
301
  func(SH["in"], pv_node)
303
302
 
303
+ def _build_ignored_properties(self, g: Graph, c: ClassDefinition) -> BNode:
304
+ def collect_child_properties(class_name: str, output: set) -> None:
305
+ for childName in self.schemaview.class_children(class_name, imports=True, mixins=False, is_a=True):
306
+ output.update(
307
+ {
308
+ URIRef(self.schemaview.get_uri(prop, expand=True))
309
+ for prop in self.schemaview.class_slots(childName)
310
+ }
311
+ )
312
+ collect_child_properties(childName, output)
313
+
314
+ child_properties = set()
315
+ collect_child_properties(c.name, child_properties)
316
+
317
+ class_slot_uris = {
318
+ URIRef(self.schemaview.get_uri(prop, expand=True)) for prop in self.schemaview.class_slots(c.name)
319
+ }
320
+ ignored_properties = child_properties.difference(class_slot_uris)
321
+
322
+ list_node = BNode()
323
+ ignored_properties.add(RDF.type)
324
+ Collection(g, list_node, list(ignored_properties))
325
+
326
+ return list_node
327
+
304
328
 
305
329
  def add_simple_data_type(func: Callable, r: ElementName) -> None:
306
330
  for datatype in list(ShaclDataType):
@@ -14,6 +14,8 @@ from linkml_runtime.utils.schemaview import SchemaView
14
14
  from linkml._version import __version__
15
15
  from linkml.utils.generator import Generator, shared_arguments
16
16
 
17
+ logger = logging.getLogger(__name__)
18
+
17
19
  template = """
18
20
  {% for pfxn, pfx in schema.prefixes.items() -%}
19
21
  PREFIX {{pfxn}}: <{{pfx.prefix_reference}}>
@@ -150,7 +152,7 @@ class SparqlGenerator(Generator):
150
152
  extra = ""
151
153
  if named_graphs is not None:
152
154
  extra += f'FILTER( ?graph in ( {",".join(named_graphs)} ))'
153
- logging.info(f"Named Graphs = {named_graphs} // extra={extra}")
155
+ logger.info(f"Named Graphs = {named_graphs} // extra={extra}")
154
156
  if limit is not None and isinstance(limit, int):
155
157
  limit = f"LIMIT {limit}"
156
158
  else:
@@ -21,6 +21,8 @@ from linkml.generators.sqltablegen import SQLTableGenerator
21
21
  from linkml.transformers.relmodel_transformer import ForeignKeyPolicy, RelationalModelTransformer
22
22
  from linkml.utils.generator import Generator, shared_arguments
23
23
 
24
+ logger = logging.getLogger(__name__)
25
+
24
26
 
25
27
  class TemplateEnum(Enum):
26
28
  DECLARATIVE = "declarative"
@@ -84,7 +86,7 @@ class SQLAlchemyGenerator(Generator):
84
86
  template_obj = Template(template_str)
85
87
  if model_path is None:
86
88
  model_path = self.schema.name
87
- logging.info(f"Package for dataclasses == {model_path}")
89
+ logger.info(f"Package for dataclasses == {model_path}")
88
90
  backrefs = defaultdict(list)
89
91
  for m in tr_result.mappings:
90
92
  backrefs[m.source_class].append(m)
@@ -113,7 +115,7 @@ class SQLAlchemyGenerator(Generator):
113
115
  is_join_table=lambda c: any(tag for tag in c.annotations.keys() if tag == "linkml:derived_from"),
114
116
  classes=rel_schema_classes_ordered,
115
117
  )
116
- logging.debug(f"# Generated code:\n{code}")
118
+ logger.debug(f"# Generated code:\n{code}")
117
119
  return code
118
120
 
119
121
  def serialize(self, **kwargs) -> str:
@@ -173,7 +175,7 @@ class SQLAlchemyGenerator(Generator):
173
175
  def skip(cls: ClassDefinition) -> bool:
174
176
  is_skip = len(cls.attributes) == 0
175
177
  if is_skip:
176
- logging.error(f"SKIPPING: {cls.name}")
178
+ logger.error(f"SKIPPING: {cls.name}")
177
179
  return is_skip
178
180
 
179
181
  # TODO: move this
@@ -16,6 +16,8 @@ from linkml.transformers.relmodel_transformer import ForeignKeyPolicy, Relationa
16
16
  from linkml.utils.generator import Generator, shared_arguments
17
17
  from linkml.utils.schemaloader import SchemaLoader
18
18
 
19
+ logger = logging.getLogger(__name__)
20
+
19
21
 
20
22
  class SqlNamingPolicy(Enum):
21
23
  preserve = "preserve"
@@ -262,12 +264,12 @@ class SQLTableGenerator(Generator):
262
264
  elif range is None:
263
265
  return Text()
264
266
  else:
265
- logging.error(f"Unknown range: {range} for {slot.name} = {slot.range}")
267
+ logger.error(f"Unknown range: {range} for {slot.name} = {slot.range}")
266
268
  return Text()
267
269
  if range_base in RANGEMAP:
268
270
  return RANGEMAP[range_base]
269
271
  else:
270
- logging.error(f"UNKNOWN range base: {range_base} for {slot.name} = {slot.range}")
272
+ logger.error(f"UNKNOWN range base: {range_base} for {slot.name} = {slot.range}")
271
273
  return Text()
272
274
 
273
275
  @staticmethod