From 274a9baa6f2cbcc5a7ef8a78924b0fad78add43a Mon Sep 17 00:00:00 2001 From: Cody Mitchell Date: Sat, 21 Mar 2026 22:00:42 -0500 Subject: [PATCH 01/19] test: warn when owl coherency checks are skipped --- tests/linkml/test_compliance/README.md | 4 ++++ tests/linkml/test_compliance/helper.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/tests/linkml/test_compliance/README.md b/tests/linkml/test_compliance/README.md index eb669d6602..0989572aa0 100644 --- a/tests/linkml/test_compliance/README.md +++ b/tests/linkml/test_compliance/README.md @@ -104,3 +104,7 @@ Currently data validation using OWL is off by default, and data tests pass. In order to run the OWL tests, you should have ROBOT in your path. The unit test suite will then generate turtle files containing both ontology and data, and use HermiT to validate. Note this is currently slow. + +If ROBOT is not available, OWL coherency checks are marked `UNTESTED` and the +test run now emits a pytest warning so the reduced validation coverage is +visible in local runs and CI logs. diff --git a/tests/linkml/test_compliance/helper.py b/tests/linkml/test_compliance/helper.py index 1b73099aec..5f156ee5bb 100644 --- a/tests/linkml/test_compliance/helper.py +++ b/tests/linkml/test_compliance/helper.py @@ -8,6 +8,7 @@ import shutil import subprocess import tempfile +import warnings from collections import defaultdict from collections.abc import Callable, Iterator from copy import copy, deepcopy @@ -1013,6 +1014,23 @@ def robot_is_on_path(): return shutil.which("robot") is not None +@lru_cache +def _warn_robot_missing_for_owl_compliance() -> None: + """ + Emit one visible warning when OWL coherency checks are skipped. + + The compliance suite currently treats missing ROBOT as an untested path. + Make that explicit in pytest output so CI and local runs do not silently + look stronger than they are. + """ + warnings.warn( + "ROBOT is not on PATH; OWL coherency checks are being marked UNTESTED. " + "Install ROBOT to exercise OWL reasoner-backed compliance validation.", + pytest.PytestWarning, + stacklevel=2, + ) + + def robot_check_coherency( data_path: str | Path, ontology_path: str | Path, output_path: str | Path = None ) -> bool | None: @@ -1028,6 +1046,7 @@ def robot_check_coherency( :return: """ if not robot_is_on_path(): + _warn_robot_missing_for_owl_compliance() return None if not ontology_path: return None From ea5e52a6be2d95c10eb188a7928a1dea2c465e58 Mon Sep 17 00:00:00 2001 From: Nico Matentzoglu Date: Wed, 1 Apr 2026 00:43:03 +0300 Subject: [PATCH 02/19] Update CITATION.cff with new ORCID entries --- CITATION.cff | 71 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 9e50768a7c..ff100b7fbe 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -6,72 +6,101 @@ authors: orcid: https://orcid.org/0000-0002-8719-7760 - family-names: Solbrig given-names: Harold + orcid: https://orcid.org/0000-0002-5928-3071 - family-names: Harris given-names: Nomi L. orcid: https://orcid.org/0000-0001-6315-3707 - family-names: Kalita given-names: Patrick + orcid: https://orcid.org/0000-0002-6150-307X - family-names: Miller given-names: Mark A. - family-names: Patil given-names: Sujay + orcid: https://orcid.org/0000-0001-6142-1106 - family-names: Schaper given-names: Kevin + orcid: https://orcid.org/0000-0003-3311-7320 - family-names: Bizon given-names: Chris + orcid: https://orcid.org/0000-0002-9491-7674 - family-names: Caufield given-names: J. Harry + orcid: https://orcid.org/0000-0001-5705-7831 - family-names: Cirujano Cuesta given-names: Silvano - family-names: Cox given-names: Corey + orcid: https://orcid.org/0000-0001-9042-5982 - family-names: Dekervel given-names: Frank + orcid: https://orcid.org/0009-0008-4913-7127 - family-names: Dooley given-names: Damion M. + orcid: https://orcid.org/0000-0002-8844-9165 - family-names: Duncan given-names: William D. + orcid: https://orcid.org/0000-0001-9625-1899 - family-names: Fliss given-names: Tim + orcid: https://orcid.org/0000-0003-3303-658X - family-names: Gehrke given-names: Sarah + orcid: https://orcid.org/0000-0003-3245-2880 - family-names: Graefe given-names: Adam S.L. + orcid: https://orcid.org/0009-0004-8124-8864 - family-names: Hegde given-names: Harshad + orcid: https://orcid.org/0000-0002-2411-565X - family-names: Ireland given-names: AJ + orcid: https://orcid.org/0000-0003-1982-9065 - family-names: Jacobsen given-names: Julius O.B. + orcid: https://orcid.org/0000-0002-3265-1591 - family-names: Krishnamurthy given-names: Madan + orcid: https://orcid.org/0000-0002-9767-3636 - family-names: Kroll given-names: Carlo + orcid: https://orcid.org/0009-0008-4562-7399 - family-names: Linke given-names: David + orcid: https://orcid.org/0000-0002-5898-1820 - family-names: Ly given-names: Ryan + orcid: https://orcid.org/0000-0001-9238-0642 - family-names: Matentzoglu given-names: Nicolas + orcid: https://orcid.org/0000-0002-7356-1779 - family-names: Overton given-names: James A. + orcid: https://orcid.org/0000-0001-5139-5557 - family-names: Saunders given-names: Jonny L. + orcid: https://orcid.org/0000-0003-0545-5066 - family-names: Unni given-names: Deepak R. + orcid: https://orcid.org/0000-0002-3583-7340 - family-names: Vaidya given-names: Gaurav + orcid: https://orcid.org/0000-0003-0587-0454 - family-names: Vierdag given-names: Wouter-Michiel A.M. + orcid: https://orcid.org/0000-0003-1666-5421 - family-names: Putman given-names: Tim - family-names: LinkML Community Contributors - family-names: Ruebel given-names: Oliver + orcid: https://orcid.org/0000-0001-9902-1984 - family-names: Chute given-names: Christopher G. + orcid: https://orcid.org/0000-0001-5437-2545 - family-names: Brush given-names: Matthew H. + orcid: https://orcid.org/0000-0002-1048-5019 - family-names: Haendel given-names: Melissa A. orcid: https://orcid.org/0000-0001-9114-8737 @@ -90,80 +119,108 @@ preferred-citation: orcid: https://orcid.org/0000-0002-8719-7760 - family-names: Solbrig given-names: Harold + orcid: https://orcid.org/0000-0002-5928-3071 - family-names: Harris given-names: Nomi L. orcid: https://orcid.org/0000-0001-6315-3707 - family-names: Kalita given-names: Patrick + orcid: https://orcid.org/0000-0002-6150-307X - family-names: Miller given-names: Mark A. - family-names: Patil given-names: Sujay + orcid: https://orcid.org/0000-0001-6142-1106 - family-names: Schaper given-names: Kevin + orcid: https://orcid.org/0000-0003-3311-7320 - family-names: Bizon given-names: Chris + orcid: https://orcid.org/0000-0002-9491-7674 - family-names: Caufield given-names: J. Harry + orcid: https://orcid.org/0000-0001-5705-7831 - family-names: Cirujano Cuesta given-names: Silvano - family-names: Cox given-names: Corey + orcid: https://orcid.org/0000-0001-9042-5982 - family-names: Dekervel given-names: Frank + orcid: https://orcid.org/0009-0008-4913-7127 - family-names: Dooley given-names: Damion M. + orcid: https://orcid.org/0000-0002-8844-9165 - family-names: Duncan given-names: William D. + orcid: https://orcid.org/0000-0001-9625-1899 - family-names: Fliss given-names: Tim + orcid: https://orcid.org/0000-0003-3303-658X - family-names: Gehrke given-names: Sarah + orcid: https://orcid.org/0000-0003-3245-2880 - family-names: Graefe given-names: Adam S.L. + orcid: https://orcid.org/0009-0004-8124-8864 - family-names: Hegde given-names: Harshad + orcid: https://orcid.org/0000-0002-2411-565X - family-names: Ireland given-names: AJ + orcid: https://orcid.org/0000-0003-1982-9065 - family-names: Jacobsen given-names: Julius O.B. + orcid: https://orcid.org/0000-0002-3265-1591 - family-names: Krishnamurthy given-names: Madan + orcid: https://orcid.org/0000-0002-9767-3636 - family-names: Kroll given-names: Carlo + orcid: https://orcid.org/0009-0008-4562-7399 - family-names: Linke given-names: David + orcid: https://orcid.org/0000-0002-5898-1820 - family-names: Ly given-names: Ryan + orcid: https://orcid.org/0000-0001-9238-0642 - family-names: Matentzoglu given-names: Nicolas + orcid: https://orcid.org/0000-0002-7356-1779 - family-names: Overton given-names: James A. + orcid: https://orcid.org/0000-0001-5139-5557 - family-names: Saunders given-names: Jonny L. + orcid: https://orcid.org/0000-0003-0545-5066 - family-names: Unni given-names: Deepak R. + orcid: https://orcid.org/0000-0002-3583-7340 - family-names: Vaidya given-names: Gaurav + orcid: https://orcid.org/0000-0003-0587-0454 - family-names: Vierdag given-names: Wouter-Michiel A.M. - - family-names: Putman - given-names: Tim + orcid: https://orcid.org/0000-0003-1666-5421 - family-names: LinkML Community Contributors - family-names: Ruebel given-names: Oliver + orcid: https://orcid.org/0000-0001-9902-1984 - family-names: Chute given-names: Christopher G. + orcid: https://orcid.org/0000-0001-5437-2545 - family-names: Brush given-names: Matthew H. + orcid: https://orcid.org/0000-0002-1048-5019 - family-names: Haendel given-names: Melissa A. orcid: https://orcid.org/0000-0001-9114-8737 - family-names: Mungall given-names: Christopher J. orcid: https://orcid.org/0000-0002-6601-2165 - title: "LinkML: A Linked Open Data Modeling Language" - journal: arXiv - year: 2025 - url: https://arxiv.org/abs/2511.16935 - doi: 10.48550/arXiv.2511.16935 + title: "LinkML: an open data modeling framework" + journal: GigaScience + volume: 15 + year: 2026 + url: https://doi.org/10.1093/gigascience/giaf152 + doi: 10.1093/gigascience/giaf152 From fc8924afd4ad2ba5e884b75530d443a327128b7b Mon Sep 17 00:00:00 2001 From: Nico Matentzoglu Date: Wed, 1 Apr 2026 11:21:49 +0300 Subject: [PATCH 03/19] Update CITATION.cff --- CITATION.cff | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CITATION.cff b/CITATION.cff index 3b479b36ac..d4609f45b6 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -201,7 +201,6 @@ preferred-citation: - family-names: Vierdag given-names: Wouter-Michiel A.M. orcid: https://orcid.org/0000-0003-1666-5421 - - family-names: LinkML Community Contributors - family-names: Ruebel given-names: Oliver orcid: https://orcid.org/0000-0001-9902-1984 @@ -217,6 +216,7 @@ preferred-citation: - family-names: Mungall given-names: Christopher J. orcid: https://orcid.org/0000-0002-6601-2165 + - family-names: LinkML Community Contributors title: "LinkML: an open data modeling framework" journal: GigaScience volume: 15 From d1adeeb55fe81888efacd8cf241ef1a1d2ac74e7 Mon Sep 17 00:00:00 2001 From: jdsika Date: Thu, 2 Apr 2026 17:20:24 +0200 Subject: [PATCH 04/19] feat(generators): add --exclude-external-imports flag Add a dedicated --exclude-external-imports / --no-exclude-external-imports CLI flag to control whether external vocabulary terms are included in generated artifacts when --no-mergeimports is set. Previously external terms leaked into JSON-LD contexts even with --no-mergeimports. The new flag explicitly suppresses terms whose class_uri or slot_uri belong to an imported (external) schema. Tests cover linkml:types built-in import preservation, local file import preservation, and interaction with mergeimports=False. Signed-off-by: jdsika --- .../src/linkml/generators/jsonldcontextgen.py | 54 +++- .../test_generators/test_jsonldcontextgen.py | 291 ++++++++++++++++++ 2 files changed, 342 insertions(+), 3 deletions(-) diff --git a/packages/linkml/src/linkml/generators/jsonldcontextgen.py b/packages/linkml/src/linkml/generators/jsonldcontextgen.py index 60eaa9ffda..306783dd86 100644 --- a/packages/linkml/src/linkml/generators/jsonldcontextgen.py +++ b/packages/linkml/src/linkml/generators/jsonldcontextgen.py @@ -56,8 +56,22 @@ class ContextGenerator(Generator): fix_multivalue_containers: bool | None = False exclude_imports: bool = False """If True, elements from imported schemas won't be included in the generated context""" + exclude_external_imports: bool = False + """If True, elements from URL-based external vocabulary imports are excluded. + + Local file imports and linkml standard imports are kept. This is useful + when extending an external ontology (e.g. W3C Verifiable Credentials) + whose terms are ``@protected`` in their own JSON-LD context — redefining + them locally would violate JSON-LD 1.1 §4.1.11. + + Note: this flag has no effect when ``mergeimports=False`` because + non-local elements are already absent from the visitor iteration + in that mode. + """ _local_classes: set | None = field(default=None, repr=False) _local_slots: set | None = field(default=None, repr=False) + _external_classes: set | None = field(default=None, repr=False) + _external_slots: set | None = field(default=None, repr=False) # Framing (opt-in via CLI flag) emit_frame: bool = False @@ -69,7 +83,7 @@ def __post_init__(self) -> None: super().__post_init__() if self.namespaces is None: raise TypeError("Schema text must be supplied to context generator. Preparsed schema will not work") - if self.exclude_imports: + if self.exclude_imports or self.exclude_external_imports: if self.schemaview: sv = self.schemaview else: @@ -77,8 +91,31 @@ def __post_init__(self) -> None: if isinstance(source, str) and self.base_dir and not Path(source).is_absolute(): source = str(Path(self.base_dir) / source) sv = SchemaView(source, importmap=self.importmap, base_dir=self.base_dir) - self._local_classes = set(sv.all_classes(imports=False).keys()) - self._local_slots = set(sv.all_slots(imports=False).keys()) + if self.exclude_imports: + self._local_classes = set(sv.all_classes(imports=False).keys()) + self._local_slots = set(sv.all_slots(imports=False).keys()) + if self.exclude_external_imports: + self._external_classes, self._external_slots = self._collect_external_elements(sv) + + @staticmethod + def _collect_external_elements(sv: SchemaView) -> tuple[set[str], set[str]]: + """Identify classes and slots from URL-based external vocabulary imports. + + Walks the SchemaView ``schema_map`` (populated by ``imports_closure``) + and collects element names from schemas whose import key starts with + ``http://`` or ``https://``. Local file imports and ``linkml:`` + standard imports are left untouched. + """ + sv.imports_closure() + external_classes: set[str] = set() + external_slots: set[str] = set() + for schema_key, schema_def in sv.schema_map.items(): + if schema_key == sv.schema.name: + continue + if schema_key.startswith("http://") or schema_key.startswith("https://"): + external_classes.update(schema_def.classes.keys()) + external_slots.update(schema_def.slots.keys()) + return external_classes, external_slots def visit_schema(self, base: str | Namespace | None = None, output: str | None = None, **_): # Add any explicitly declared prefixes @@ -194,6 +231,8 @@ def end_schema( def visit_class(self, cls: ClassDefinition) -> bool: if self.exclude_imports and cls.name not in self._local_classes: return False + if self.exclude_external_imports and cls.name in self._external_classes: + return False class_def = {} cn = camelcase(cls.name) @@ -246,6 +285,8 @@ def _literal_coercion_for_ranges(self, ranges: list[str]) -> tuple[bool, str | N def visit_slot(self, aliased_slot_name: str, slot: SlotDefinition) -> None: if self.exclude_imports and slot.name not in self._local_slots: return + if self.exclude_external_imports and slot.name in self._external_slots: + return if slot.identifier: slot_def = "@id" @@ -390,6 +431,13 @@ def serialize( help="Use --exclude-imports to exclude imported elements from the generated JSON-LD context. This is useful when " "extending an ontology whose terms already have context definitions in their own JSON-LD context file.", ) +@click.option( + "--exclude-external-imports/--no-exclude-external-imports", + default=False, + show_default=True, + help="Exclude elements from URL-based external vocabulary imports while keeping local file imports. " + "Useful when extending ontologies (e.g. W3C VC v2) whose terms are @protected in their own JSON-LD context.", +) @click.version_option(__version__, "-V", "--version") def cli(yamlfile, emit_frame, embed_context_in_frame, output, **args): """Generate jsonld @context definition from LinkML model""" diff --git a/tests/linkml/test_generators/test_jsonldcontextgen.py b/tests/linkml/test_generators/test_jsonldcontextgen.py index 6de23347aa..ff5b75e662 100644 --- a/tests/linkml/test_generators/test_jsonldcontextgen.py +++ b/tests/linkml/test_generators/test_jsonldcontextgen.py @@ -1,4 +1,5 @@ import json +import textwrap import pytest from click.testing import CliRunner @@ -571,3 +572,293 @@ def test_exclude_imports(input_path): # Imported class and slot must NOT be present assert "BaseClass" not in ctx, "Imported class 'BaseClass' must not appear in exclude-imports context" assert "baseProperty" not in ctx, "Imported slot 'baseProperty' must not appear in exclude-imports context" + + +@pytest.mark.parametrize("mergeimports", [True, False], ids=["merge", "no-merge"]) +def test_exclude_external_imports(tmp_path, mergeimports): + """With --exclude-external-imports, elements from URL-based external + vocabulary imports must not appear in the generated JSON-LD context, + while local file imports and linkml standard imports are kept. + + When a schema imports terms from an external vocabulary (e.g. W3C VC + v2), those terms already have context definitions in their own JSON-LD + context file. Re-defining them in the local context can conflict with + @protected term definitions from the external context (JSON-LD 1.1 + section 4.1.11). + """ + ext_dir = tmp_path / "ext" + ext_dir.mkdir() + (ext_dir / "external_vocab.yaml").write_text( + textwrap.dedent("""\ + id: https://example.org/external-vocab + name: external_vocab + default_prefix: ext + prefixes: + linkml: https://w3id.org/linkml/ + ext: https://example.org/external-vocab/ + imports: + - linkml:types + slots: + issuer: + slot_uri: ext:issuer + range: string + validFrom: + slot_uri: ext:validFrom + range: date + classes: + ExternalCredential: + class_uri: ext:ExternalCredential + slots: + - issuer + - validFrom + """), + encoding="utf-8", + ) + + (tmp_path / "main.yaml").write_text( + textwrap.dedent("""\ + id: https://example.org/main + name: main + default_prefix: main + prefixes: + linkml: https://w3id.org/linkml/ + main: https://example.org/main/ + ext: https://example.org/external-vocab/ + imports: + - linkml:types + - https://example.org/external-vocab + slots: + localName: + slot_uri: main:localName + range: string + classes: + LocalThing: + class_uri: main:LocalThing + slots: + - localName + """), + encoding="utf-8", + ) + + importmap = {"https://example.org/external-vocab": str(ext_dir / "external_vocab")} + + context_text = ContextGenerator( + str(tmp_path / "main.yaml"), + exclude_external_imports=True, + mergeimports=mergeimports, + importmap=importmap, + base_dir=str(tmp_path), + ).serialize() + context = json.loads(context_text) + ctx = context["@context"] + + # Local terms must be present + assert "localName" in ctx or "local_name" in ctx, ( + f"Local slot missing with mergeimports={mergeimports}, got: {list(ctx.keys())}" + ) + assert "LocalThing" in ctx, f"Local class missing with mergeimports={mergeimports}, got: {list(ctx.keys())}" + + # External vocabulary terms must NOT be present + assert "issuer" not in ctx, f"External slot 'issuer' present with mergeimports={mergeimports}" + assert "validFrom" not in ctx and "valid_from" not in ctx, ( + f"External slot 'validFrom' present with mergeimports={mergeimports}" + ) + assert "ExternalCredential" not in ctx, ( + f"External class 'ExternalCredential' present with mergeimports={mergeimports}" + ) + + +def test_exclude_external_imports_preserves_linkml_types(tmp_path): + """linkml:types (standard library import) must NOT be treated as external. + + The ``linkml:types`` import resolves to a URL internally + (``https://w3id.org/linkml/types``), but it is a standard LinkML import, + not a user-declared external vocabulary. The ``_collect_external_elements`` + method filters by ``schema_key.startswith("http")`` — this test verifies + that linkml built-in types (string, integer, date, etc.) survive the filter. + """ + (tmp_path / "schema.yaml").write_text( + textwrap.dedent("""\ + id: https://example.org/test + name: test_linkml_types + default_prefix: ex + prefixes: + linkml: https://w3id.org/linkml/ + ex: https://example.org/ + imports: + - linkml:types + slots: + name: + slot_uri: ex:name + range: string + age: + slot_uri: ex:age + range: integer + classes: + Person: + class_uri: ex:Person + slots: + - name + - age + """), + encoding="utf-8", + ) + + context_text = ContextGenerator( + str(tmp_path / "schema.yaml"), + exclude_external_imports=True, + ).serialize() + ctx = json.loads(context_text)["@context"] + + # Local classes and slots must be present + assert "Person" in ctx, f"Local class 'Person' missing, got: {list(ctx.keys())}" + assert "name" in ctx, f"Local slot 'name' missing, got: {list(ctx.keys())}" + assert "age" in ctx, f"Local slot 'age' missing, got: {list(ctx.keys())}" + + +def test_exclude_external_imports_preserves_local_file_imports(tmp_path): + """Local file imports (non-URL) must be preserved when exclude_external_imports is set. + + Only URL-based imports (http:// or https://) are considered external. + File-path imports between local schemas must remain in the context. + """ + local_dir = tmp_path / "local" + local_dir.mkdir() + (local_dir / "base.yaml").write_text( + textwrap.dedent("""\ + id: https://example.org/base + name: base + default_prefix: base + prefixes: + linkml: https://w3id.org/linkml/ + base: https://example.org/base/ + imports: + - linkml:types + slots: + baseField: + slot_uri: base:baseField + range: string + classes: + BaseRecord: + class_uri: base:BaseRecord + slots: + - baseField + """), + encoding="utf-8", + ) + + (tmp_path / "main.yaml").write_text( + textwrap.dedent("""\ + id: https://example.org/main + name: main + default_prefix: main + prefixes: + linkml: https://w3id.org/linkml/ + main: https://example.org/main/ + base: https://example.org/base/ + imports: + - linkml:types + - local/base + slots: + localField: + slot_uri: main:localField + range: string + classes: + MainRecord: + class_uri: main:MainRecord + slots: + - localField + """), + encoding="utf-8", + ) + + context_text = ContextGenerator( + str(tmp_path / "main.yaml"), + exclude_external_imports=True, + mergeimports=True, + base_dir=str(tmp_path), + ).serialize() + ctx = json.loads(context_text)["@context"] + + # Local file import terms must be present + assert "MainRecord" in ctx, f"Local class 'MainRecord' missing, got: {list(ctx.keys())}" + assert "BaseRecord" in ctx, f"Local-file-imported class 'BaseRecord' missing, got: {list(ctx.keys())}" + assert "baseField" in ctx or "base_field" in ctx, ( + f"Local-file-imported slot 'baseField' missing, got: {list(ctx.keys())}" + ) + + +def test_exclude_external_imports_works_with_mergeimports_false(tmp_path): + """exclude_external_imports is effective even when mergeimports=False. + + Although mergeimports=False prevents most imported elements from appearing, + external vocabulary elements can still leak into the context via the + schema_map. The exclude_external_imports flag catches these. + """ + ext_dir = tmp_path / "ext" + ext_dir.mkdir() + (ext_dir / "external_vocab.yaml").write_text( + textwrap.dedent("""\ + id: https://example.org/external-vocab + name: external_vocab + default_prefix: ext + prefixes: + linkml: https://w3id.org/linkml/ + ext: https://example.org/external-vocab/ + imports: + - linkml:types + slots: + issuer: + slot_uri: ext:issuer + range: string + classes: + ExternalCredential: + class_uri: ext:ExternalCredential + slots: + - issuer + """), + encoding="utf-8", + ) + + (tmp_path / "main.yaml").write_text( + textwrap.dedent("""\ + id: https://example.org/main + name: main + default_prefix: main + prefixes: + linkml: https://w3id.org/linkml/ + main: https://example.org/main/ + ext: https://example.org/external-vocab/ + imports: + - linkml:types + - https://example.org/external-vocab + slots: + localName: + slot_uri: main:localName + range: string + classes: + LocalThing: + class_uri: main:LocalThing + slots: + - localName + """), + encoding="utf-8", + ) + + importmap = {"https://example.org/external-vocab": str(ext_dir / "external_vocab")} + + ctx_text = ContextGenerator( + str(tmp_path / "main.yaml"), + exclude_external_imports=True, + mergeimports=False, + importmap=importmap, + base_dir=str(tmp_path), + ).serialize() + ctx = json.loads(ctx_text)["@context"] + + # Local terms must still be present + assert "LocalThing" in ctx, f"Local class missing, got: {list(ctx.keys())}" + + # External vocabulary terms must be excluded + assert "issuer" not in ctx, "External slot 'issuer' should be excluded with mergeimports=False" + assert "ExternalCredential" not in ctx, "External class should be excluded with mergeimports=False" From 978be6f45f6a09a53e19d496f43dbfc1276dba6d Mon Sep 17 00:00:00 2001 From: jdsika Date: Thu, 2 Apr 2026 22:46:10 +0200 Subject: [PATCH 05/19] docs: correct stale docstring for exclude_external_imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The docstring incorrectly stated that the flag has no effect when mergeimports=False. In reality, external vocabulary elements can still leak into the context via the schema_map even in that mode, and the flag actively catches them — as verified by the existing test test_exclude_external_imports_works_with_mergeimports_false. Replace the inaccurate note with a correct statement. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/linkml/src/linkml/generators/jsonldcontextgen.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/linkml/src/linkml/generators/jsonldcontextgen.py b/packages/linkml/src/linkml/generators/jsonldcontextgen.py index 306783dd86..5298a602f9 100644 --- a/packages/linkml/src/linkml/generators/jsonldcontextgen.py +++ b/packages/linkml/src/linkml/generators/jsonldcontextgen.py @@ -64,9 +64,9 @@ class ContextGenerator(Generator): whose terms are ``@protected`` in their own JSON-LD context — redefining them locally would violate JSON-LD 1.1 §4.1.11. - Note: this flag has no effect when ``mergeimports=False`` because - non-local elements are already absent from the visitor iteration - in that mode. + This flag is effective regardless of the ``mergeimports`` setting: + even with ``mergeimports=False``, external vocabulary elements can + leak into the context via the schema map. """ _local_classes: set | None = field(default=None, repr=False) _local_slots: set | None = field(default=None, repr=False) From 15d2146cb27ceef6f7a6d318a75c01160e71bb39 Mon Sep 17 00:00:00 2001 From: Silvano Cirujano Cuesta Date: Thu, 2 Apr 2026 22:46:13 +0200 Subject: [PATCH 06/19] Identifiers cannot be null --- .../test_generators/input/identifier.yaml | 17 +++++++++++++++ .../test_generators/test_jsonschemagen.py | 21 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 tests/linkml/test_generators/input/identifier.yaml diff --git a/tests/linkml/test_generators/input/identifier.yaml b/tests/linkml/test_generators/input/identifier.yaml new file mode 100644 index 0000000000..0648770875 --- /dev/null +++ b/tests/linkml/test_generators/input/identifier.yaml @@ -0,0 +1,17 @@ +id: https://example.org/nullable-id +name: nullable-id +prefixes: + linkml: https://w3id.org/linkml/ + ni: https://example.org/nullable-id/ +imports: + - linkml:types +default_range: string + +classes: + MyClass: + attributes: + id: + identifier: true + needed: + required: true + name: diff --git a/tests/linkml/test_generators/test_jsonschemagen.py b/tests/linkml/test_generators/test_jsonschemagen.py index 4688804235..ca1654771b 100644 --- a/tests/linkml/test_generators/test_jsonschemagen.py +++ b/tests/linkml/test_generators/test_jsonschemagen.py @@ -316,6 +316,27 @@ def test_slot_title_from_title_slot(subtests, input_path): external_file_test(subtests, input_path("jsonschema_slot_title_from_title.yaml"), {"title_from": "title"}) +@pytest.mark.xfail(reason="identifier slots incorrectly allow null (#2448)", strict=True) +@pytest.mark.parametrize("not_closed", [True, False]) +def test_slot_identifier_non_nullability(input_path, not_closed): + """ + Identifier slots are not allowed to be "null" + + References: + - https://github.com/linkml/linkml/issues/2448 + """ + schema = input_path("identifier.yaml") + generator = JsonSchemaGenerator(schema, mergeimports=True, not_closed=not_closed) + generated = json.loads(generator.serialize()) + key = "id" + for cls in ["MyClass"]: + id = generated["$defs"][cls]["properties"][key] + if "type" in id: + assert "null" not in id["type"], f"{key} does not allow null" + elif "anyOf" in id: + assert {"type": "null"} not in id["anyOf"], f"{key} does not allow null" + + @pytest.mark.parametrize("not_closed", [True, False]) def test_slot_not_required_nullability(input_path, not_closed): """ From 4d45213909faea3162bb47139b9ab283aa030af9 Mon Sep 17 00:00:00 2001 From: Kevin Schaper Date: Thu, 2 Apr 2026 14:39:22 -0700 Subject: [PATCH 07/19] Handle invalid Python class names in pydanticgen using class alias (#2534) --- .../generators/pydanticgen/pydanticgen.py | 71 ++++++++---- .../test_pydanticgen_special_chars.py | 106 +++++++++++++++++- 2 files changed, 156 insertions(+), 21 deletions(-) diff --git a/packages/linkml/src/linkml/generators/pydanticgen/pydanticgen.py b/packages/linkml/src/linkml/generators/pydanticgen/pydanticgen.py index bfc00ea99d..0bc98458bc 100644 --- a/packages/linkml/src/linkml/generators/pydanticgen/pydanticgen.py +++ b/packages/linkml/src/linkml/generators/pydanticgen/pydanticgen.py @@ -177,6 +177,11 @@ def make_valid_python_identifier(name: str) -> str: return identifier +def _is_valid_python_name(name: str) -> bool: + """Check if a string is a valid Python identifier and not a keyword.""" + return name.isidentifier() and not keyword.iskeyword(name) + + @dataclass class PydanticGenerator(OOCodeGenerator, LifecycleMixin): """ @@ -475,9 +480,10 @@ def generate_class(self, cls: ClassDefinition) -> ClassResult: if cls.union_of: return self._generate_union_class(cls) + class_python_name = self._get_class_python_name(cls.name) pyclass = PydanticClass( - name=camelcase(cls.name), - bases=self.class_bases.get(camelcase(cls.name), PydanticBaseModel.default_name), + name=class_python_name, + bases=self.class_bases.get(class_python_name, PydanticBaseModel.default_name), description=cls.description.replace('"', '\\"') if cls.description is not None else None, ) @@ -537,14 +543,14 @@ def _generate_union_class(self, cls: ClassDefinition) -> ClassResult: ) # Get the union types with string quotes to handle forward references - union_types = [f'"{camelcase(union_cls)}"' for union_cls in cls.union_of] + union_types = [f'"{self._get_class_python_name(union_cls)}"' for union_cls in cls.union_of] union_type_str = f"Union[{', '.join(union_types)}]" # Create a type alias instead of a class # Sanitize description for single-line comment (replace newlines with spaces) description = cls.description.replace("\n", " ").strip() if cls.description else None pyclass = PydanticClass( - name=camelcase(cls.name), + name=self._get_class_python_name(cls.name), bases=[], # Empty list for type aliases description=description, is_type_alias=True, @@ -581,7 +587,7 @@ def generate_slot(self, slot: SlotDefinition, cls: ClassDefinition) -> SlotResul del slot_args["alias"] slot_args["description"] = slot.description.replace('"', '\\"') if slot.description is not None else None - predef = self.predefined_slot_values.get(camelcase(cls.name), {}).get(slot.name, None) + predef = self.predefined_slot_values.get(self._get_class_python_name(cls.name), {}).get(slot.name, None) if predef is not None: slot_args["predefined"] = str(predef) @@ -658,21 +664,19 @@ def predefined_slot_values(self) -> dict[str, dict[str, str]]: ifabsent_processor = PydanticIfAbsentProcessor(sv) slot_values = defaultdict(dict) for class_def in sv.all_classes().values(): + class_python_name = self._get_class_python_name(class_def.name) for slot_name in sv.class_slots(class_def.name): slot = sv.induced_slot(slot_name, class_def.name) if slot.designates_type: target_value = get_type_designator_value(sv, slot, class_def) - slot_values[camelcase(class_def.name)][slot.name] = f'"{target_value}"' + slot_values[class_python_name][slot.name] = f'"{target_value}"' if slot.multivalued: - slot_values[camelcase(class_def.name)][slot.name] = ( - "[" + slot_values[camelcase(class_def.name)][slot.name] + "]" + slot_values[class_python_name][slot.name] = ( + "[" + slot_values[class_python_name][slot.name] + "]" ) - slot_values[camelcase(class_def.name)][slot.name] = slot_values[camelcase(class_def.name)][ - slot.name - ] elif slot.ifabsent is not None: value = ifabsent_processor.process_slot(slot, class_def) - slot_values[camelcase(class_def.name)][slot.name] = value + slot_values[class_python_name][slot.name] = value self._predefined_slot_values = slot_values @@ -690,19 +694,46 @@ def class_bases(self) -> dict[str, list[str]]: for class_def in sv.all_classes().values(): class_parents = [] if class_def.is_a: - class_parents.append(camelcase(class_def.is_a)) + class_parents.append(self._get_class_python_name(class_def.is_a)) if self.gen_mixin_inheritance and class_def.mixins: - class_parents.extend([camelcase(mixin) for mixin in class_def.mixins]) + class_parents.extend([self._get_class_python_name(mixin) for mixin in class_def.mixins]) if len(class_parents) > 0: # Use the sorted list of classes to order the parent classes, but reversed to match MRO needs class_parents.sort( key=lambda x: self.sorted_class_names.index(x) if x in self.sorted_class_names else -1 ) class_parents.reverse() - parents[camelcase(class_def.name)] = class_parents + parents[self._get_class_python_name(class_def.name)] = class_parents self._class_bases = parents return self._class_bases + def _get_class_python_name(self, class_name: str) -> str: + """ + Get a valid Python class name for a LinkML class. + + Tries ``camelcase(name)`` first. If that is not a valid Python identifier, + falls back to ``camelcase(alias)`` when the class defines one. Raises + :class:`ValueError` if neither yields a valid identifier. + """ + python_name = camelcase(class_name) + if _is_valid_python_name(python_name): + return python_name + + class_def = self.schemaview.get_class(class_name) + if class_def and class_def.alias: + alias_name = camelcase(class_def.alias) + if _is_valid_python_name(alias_name): + return alias_name + raise ValueError( + f"Class '{class_name}' has alias '{class_def.alias}' but " + f"'{alias_name}' is not a valid Python identifier" + ) + + raise ValueError( + f"Class name '{class_name}' (Python: '{python_name}') is not a valid Python identifier. " + "Consider providing a class alias that is a valid Python identifier." + ) + def get_mixin_identifier_range(self, mixin) -> str: sv = self.schemaview id_ranges = list( @@ -738,9 +769,10 @@ def get_class_slot_range(self, slot_range: str, inlined: bool, inlined_as_list: len([x for x in sv.class_induced_slots(slot_range) if x.designates_type]) > 0 and len(sv.class_descendants(slot_range)) > 1 ): - return "Union[" + ",".join([camelcase(c) for c in sv.class_descendants(slot_range)]) + "]" + descendants = [self._get_class_python_name(c) for c in sv.class_descendants(slot_range)] + return "Union[" + ",".join(descendants) + "]" else: - return f"{camelcase(slot_range)}" + return f"{self._get_class_python_name(slot_range)}" # For the more difficult cases, set string as the default and attempt to improve it range_cls_identifier_slot_range = "str" @@ -1064,7 +1096,8 @@ def _get_element_import(self, class_name: ElementName) -> Import: schema_name = self.schemaview.element_by_schema_map()[class_name] schema = [s for s in self.schemaview.schema_map.values() if s.name == schema_name][0] module = self.generate_module_import(schema, self.split_context) - return Import(module=module, objects=[ObjectImport(name=camelcase(class_name))], is_schema=True) + python_name = self._get_class_python_name(class_name) + return Import(module=module, objects=[ObjectImport(name=python_name)], is_schema=True) def render(self) -> PydanticModule: """ @@ -1107,7 +1140,7 @@ def render(self) -> PydanticModule: # just swap in typing.Any instead down below source_classes = [c for c in source_classes if c.class_uri != "linkml:Any"] source_classes = self.before_generate_classes(source_classes, sv) - self.sorted_class_names = [camelcase(c.name) for c in source_classes] + self.sorted_class_names = [self._get_class_python_name(c.name) for c in source_classes] for cls in source_classes: cls = self.before_generate_class(cls, sv) result = self.generate_class(cls) diff --git a/tests/linkml/test_generators/test_pydanticgen_special_chars.py b/tests/linkml/test_generators/test_pydanticgen_special_chars.py index a9dbd3bc8c..9f82d109c7 100644 --- a/tests/linkml/test_generators/test_pydanticgen_special_chars.py +++ b/tests/linkml/test_generators/test_pydanticgen_special_chars.py @@ -1,11 +1,11 @@ """ -Tests for Pydantic generator handling of special characters in field names +Tests for Pydantic generator handling of special characters in field and class names """ import pytest from linkml.generators.pydanticgen import PydanticGenerator -from linkml.generators.pydanticgen.pydanticgen import make_valid_python_identifier +from linkml.generators.pydanticgen.pydanticgen import _is_valid_python_name, make_valid_python_identifier from linkml.validator import Validator from linkml.validator.plugins import PydanticValidationPlugin @@ -131,3 +131,105 @@ def test_validation_with_python_field_names(): validator = Validator(schema=schema_text, validation_plugins=[PydanticValidationPlugin()]) report = validator.validate(test_data) assert len(report.results) == 0, f"Validation failed: {report.results}" + + +# --- Tests for _is_valid_python_name --- + + +@pytest.mark.parametrize( + "name, expected", + [ + ("person", True), + ("MyClass", True), + ("_private", True), + ("3DModel", False), + ("Per-son", False), + ("Per!son", False), + ("def", False), + ("class", False), + ("in", False), + ], +) +def test_is_valid_python_name(name: str, expected: bool): + assert _is_valid_python_name(name) is expected + + +# --- Tests for invalid class names --- + + +@pytest.mark.parametrize("class_name", ["3DModel", "Per-son", "Per!son"]) +def test_invalid_class_name_without_alias_raises(class_name): + """Classes with invalid Python names and no alias should raise ValueError.""" + schema_text = f""" + id: test_schema + name: test_schema + imports: + - linkml:types + default_range: string + + classes: + {class_name}: + attributes: + name: + range: string + """ + with pytest.raises(ValueError, match="not a valid Python identifier"): + PydanticGenerator(schema_text).serialize() + + +@pytest.mark.parametrize( + "class_name, class_alias", + [ + ("3DModel", "ThreeDModel"), + ("Per-son", "Person"), + ("my-class", "MyClass"), + ], +) +def test_class_name_with_alias(class_name, class_alias): + """Classes with invalid Python names but valid aliases should use the alias.""" + schema_text = f""" + id: test_schema + name: test_schema + imports: + - linkml:types + default_range: string + + classes: + {class_name}: + alias: {class_alias} + attributes: + name: + range: string + """ + generator = PydanticGenerator(schema_text) + code = generator.serialize() + # The generated code should compile and use the alias as the class name + compile(code, "test", "exec") + assert f"class {class_alias}" in code + + +@pytest.mark.parametrize( + "class_name, bad_alias", + [ + ("3DModel", "3DAlias"), + ("Per-son", "Per-son-alias"), + ], +) +def test_class_name_with_invalid_alias_raises(class_name, bad_alias): + """Classes with invalid names AND invalid aliases should raise ValueError.""" + schema_text = f""" + id: test_schema + name: test_schema + imports: + - linkml:types + default_range: string + + classes: + {class_name}: + alias: {bad_alias} + attributes: + name: + range: string + """ + with pytest.raises(ValueError, match="not a valid Python identifier"): + PydanticGenerator(schema_text).serialize() From 820b2473d94d43646fc96f4ad5dd42eb86be3bfa Mon Sep 17 00:00:00 2001 From: Kevin Schaper Date: Thu, 2 Apr 2026 15:00:39 -0700 Subject: [PATCH 08/19] feat(sqla): add SQLAlchemy 2.x declarative code generation --- .../sqlalchemy_declarative_2x_template.py | 94 +++ .../src/linkml/generators/sqlalchemygen.py | 33 +- .../src/linkml/generators/sqltablegen.py | 11 + pyproject.toml | 8 + .../golden/personinfo_sqla_2x.py | 722 ++++++++++++++++++ .../test_generators/test_sqlalchemygen.py | 160 +++- 6 files changed, 1019 insertions(+), 9 deletions(-) create mode 100644 packages/linkml/src/linkml/generators/sqlalchemy/sqlalchemy_declarative_2x_template.py create mode 100644 tests/linkml/test_generators/golden/personinfo_sqla_2x.py diff --git a/packages/linkml/src/linkml/generators/sqlalchemy/sqlalchemy_declarative_2x_template.py b/packages/linkml/src/linkml/generators/sqlalchemy/sqlalchemy_declarative_2x_template.py new file mode 100644 index 0000000000..21738d360d --- /dev/null +++ b/packages/linkml/src/linkml/generators/sqlalchemy/sqlalchemy_declarative_2x_template.py @@ -0,0 +1,94 @@ +sqlalchemy_declarative_2x_template_str = """\ +from __future__ import annotations + +from datetime import date, datetime, time +from decimal import Decimal + +from sqlalchemy import ( + Boolean, + Date, + DateTime, + Enum, + Float, + ForeignKey, + Integer, + Numeric, + Text, + Time, +) +from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship + + +class Base(DeclarativeBase): + pass + + +metadata = Base.metadata +{% for c in classes %} + + +class {{ classname(c.name) }}({% if c.is_a %}{{ classname(c.is_a) }}{% else %}Base{% endif %}): + \"\"\" +{% if c.description %} + {{ c.description }} +{% else %} + {{ c.alias }} +{% endif %} + \"\"\" + + __tablename__ = "{{ c.name }}" + +{% for s in c.attributes.values() %} +{% set pytype = python_type(s.annotations['sql_type'].value) %} +{% if 'primary_key' in s.annotations %} + {{ s.alias }}: Mapped[{{ pytype }}] = mapped_column({{ s.annotations['sql_type'].value }} + {%- if 'foreign_key' in s.annotations %}, ForeignKey("{{ s.annotations['foreign_key'].value }}"){% endif -%} + , primary_key=True + {%- if 'autoincrement' in s.annotations %}, autoincrement=True{% endif -%} + ) +{% elif 'required' in s.annotations %} + {{ s.alias }}: Mapped[{{ pytype }}] = mapped_column({{ s.annotations['sql_type'].value }} + {%- if 'foreign_key' in s.annotations %}, ForeignKey("{{ s.annotations['foreign_key'].value }}"){% endif -%} + ) +{% else %} + {{ s.alias }}: Mapped[{{ pytype }} | None] = mapped_column({{ s.annotations['sql_type'].value }} + {%- if 'foreign_key' in s.annotations %}, ForeignKey("{{ s.annotations['foreign_key'].value }}"){% endif -%} + ) +{% endif %} +{% if 'foreign_key' in s.annotations and 'original_slot' in s.annotations %} + {{ s.annotations['original_slot'].value }}: Mapped[{{ classname(s.range) }} | None] = relationship(foreign_keys=[{{ s.alias }}]) +{% endif %} +{% endfor %} +{% for mapping in backrefs[c.name] %} +{% if mapping.mapping_type == "ManyToMany" %} + + # ManyToMany + {{ mapping.source_slot }}: Mapped[list[{{ classname(mapping.target_class) }}]] = relationship(secondary="{{ mapping.join_class }}") +{% elif mapping.mapping_type == "MultivaluedScalar" %} + + {{ mapping.source_slot }}_rel: Mapped[list[{{ classname(mapping.join_class) }}]] = relationship() + {{ mapping.source_slot }}: AssociationProxy[list[str]] = association_proxy( + "{{ mapping.source_slot }}_rel", + "{{ mapping.target_slot }}", + creator=lambda x_: {{ classname(mapping.join_class) }}({{ mapping.target_slot }}=x_), + ) +{% else %} + + # One-To-Many: {{ mapping }} + {{ mapping.source_slot }}: Mapped[list[{{ classname(mapping.target_class) }}]] = relationship(foreign_keys="[{{ mapping.target_class }}.{{ mapping.target_slot }}]") +{% endif %} +{% endfor %} + + def __repr__(self): + return f"{{ c.name }}( + {%- for s in c.attributes.values() -%} + {{ s.alias }}={self.{{ s.alias }}}, + {%- endfor -%} + )" +{% if c.is_a or c.mixins %} + + __mapper_args__ = {"concrete": True} +{% endif %} +{% endfor %} +""" diff --git a/packages/linkml/src/linkml/generators/sqlalchemygen.py b/packages/linkml/src/linkml/generators/sqlalchemygen.py index 8efab64bae..dc77e3c3f7 100644 --- a/packages/linkml/src/linkml/generators/sqlalchemygen.py +++ b/packages/linkml/src/linkml/generators/sqlalchemygen.py @@ -7,15 +7,16 @@ from types import ModuleType import click -from jinja2 import Template +from jinja2 import Environment, Template from sqlalchemy import Enum from linkml._version import __version__ from linkml.generators.pydanticgen import PydanticGenerator from linkml.generators.pythongen import PythonGenerator +from linkml.generators.sqlalchemy.sqlalchemy_declarative_2x_template import sqlalchemy_declarative_2x_template_str from linkml.generators.sqlalchemy.sqlalchemy_declarative_template import sqlalchemy_declarative_template_str from linkml.generators.sqlalchemy.sqlalchemy_imperative_template import sqlalchemy_imperative_template_str -from linkml.generators.sqltablegen import SQLTableGenerator +from linkml.generators.sqltablegen import SQL_TYPE_TO_PYTHON_TYPE, SQLTableGenerator from linkml.transformers.relmodel_transformer import ForeignKeyPolicy, RelationalModelTransformer from linkml.utils.generator import Generator, shared_arguments from linkml_runtime.linkml_model import Annotation, ClassDefinition, ClassDefinitionName, SchemaDefinition @@ -29,6 +30,7 @@ class TemplateEnum(Enum): DECLARATIVE = "declarative" IMPERATIVE = "imperative" + DECLARATIVE_2X = "declarative_2x" @dataclass @@ -80,9 +82,14 @@ def generate_sqla( template_str = sqlalchemy_imperative_template_str elif template == TemplateEnum.DECLARATIVE: template_str = sqlalchemy_declarative_template_str + elif template == TemplateEnum.DECLARATIVE_2X: + template_str = sqlalchemy_declarative_2x_template_str else: raise Exception(f"Unknown template type: {template}") - template_obj = Template(template_str) + if template == TemplateEnum.DECLARATIVE_2X: + template_obj = Environment(trim_blocks=True, lstrip_blocks=True).from_string(template_str) + else: + template_obj = Template(template_str) if model_path is None: model_path = self.schema.name logger.info(f"Package for dataclasses == {model_path}") @@ -109,6 +116,7 @@ def generate_sqla( no_model_import=no_model_import, is_join_table=lambda c: any(tag for tag in c.annotations.keys() if tag == "linkml:derived_from"), classes=rel_schema_classes_ordered, + python_type=lambda sql_repr: SQL_TYPE_TO_PYTHON_TYPE.get(sql_repr, "str"), ) logger.debug(f"# Generated code:\n{code}") return code @@ -127,7 +135,7 @@ def compile_sqla( """ Generates and compiles SQL Alchemy bindings - - If template is DECLARATIVE, then a single python module with classes is generated + - If template is DECLARATIVE or DECLARATIVE_2X, then a single python module with classes is generated - If template is IMPERATIVE, only mappings are generated - if compile_python_dataclasses is True then a standard datamodel is generated @@ -142,7 +150,7 @@ def compile_sqla( if model_path is None: model_path = self.schema.name - if template == TemplateEnum.DECLARATIVE: + if template in (TemplateEnum.DECLARATIVE, TemplateEnum.DECLARATIVE_2X): sqla_code = self.generate_sqla(model_path=None, no_model_import=True, template=template, **kwargs) return compile_python(sqla_code, package_path=model_path) elif compile_python_dataclasses: @@ -211,16 +219,27 @@ def order_classes_by_hierarchy(sv: SchemaView) -> list[ClassDefinitionName]: show_default=True, help="Emit FK declarations", ) +@click.option( + "--sqla-style", + type=click.Choice(["1", "2"]), + default=None, + help="SQLAlchemy style to generate (1 or 2). Defaults to 1. Only applies in declarative mode.", +) @click.version_option(__version__, "-V", "--version") @click.command(name="sqla") -def cli(yamlfile, declarative, generate_classes, pydantic, use_foreign_keys=True, **args): +def cli(yamlfile, declarative, generate_classes, pydantic, use_foreign_keys=True, sqla_style=None, **args): """Generate SQL DDL representation""" + if sqla_style and not declarative: + raise click.UsageError("--sqla-style only applies in declarative mode (remove --no-declarative)") if pydantic: pygen = PydanticGenerator(yamlfile) print(pygen.serialize()) gen = SQLAlchemyGenerator(yamlfile, **args) if declarative: - t = TemplateEnum.DECLARATIVE + if sqla_style == "2": + t = TemplateEnum.DECLARATIVE_2X + else: + t = TemplateEnum.DECLARATIVE else: t = TemplateEnum.IMPERATIVE if use_foreign_keys: diff --git a/packages/linkml/src/linkml/generators/sqltablegen.py b/packages/linkml/src/linkml/generators/sqltablegen.py index e6196b909b..cad0ce01d1 100644 --- a/packages/linkml/src/linkml/generators/sqltablegen.py +++ b/packages/linkml/src/linkml/generators/sqltablegen.py @@ -63,6 +63,17 @@ class SqlNamingPolicy(Enum): "XSDDate": Date(), } +SQL_TYPE_TO_PYTHON_TYPE: dict[str, str] = { + "Text()": "str", + "Integer()": "int", + "Float()": "float", + "Numeric()": "Decimal", + "Boolean()": "bool", + "Time()": "time", + "DateTime()": "datetime", + "Date()": "date", +} + VARCHAR_REGEX = re.compile(r"VARCHAR2?(\((\d+)\))?") ORACLE_MAX_VARCHAR_LENGTH = 4096 diff --git a/pyproject.toml b/pyproject.toml index 8e9211a9a9..fda49b272c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -132,13 +132,21 @@ select = [ "UP", # pyupgrade ] +[tool.ruff.format] +# Golden files are generated output whose formatting is determined by the code generator +exclude = ["tests/**/golden/*.py"] + [tool.ruff.lint.isort] known-first-party = ["linkml", "linkml_runtime"] [tool.ruff.lint.per-file-ignores] # These templates can have long lines "packages/linkml/src/linkml/generators/sqlalchemy/sqlalchemy_declarative_template.py" = ["E501"] +"packages/linkml/src/linkml/generators/sqlalchemy/sqlalchemy_declarative_2x_template.py" = ["E501"] "packages/linkml/src/linkml/generators/sqlalchemy/sqlalchemy_imperative_template.py" = ["E501"] + +# Golden files are generated output — may have unused imports, long lines, unsorted imports +"tests/**/golden/*.py" = ["E501", "F401", "I001"] "packages/linkml/src/linkml/linter/config/datamodel/config.py" = ["E501", "F401", "I001", "UP007", "UP035", "UP045"] # Auto-generated model files use Optional/Union with string forward references diff --git a/tests/linkml/test_generators/golden/personinfo_sqla_2x.py b/tests/linkml/test_generators/golden/personinfo_sqla_2x.py new file mode 100644 index 0000000000..8bfd13f18f --- /dev/null +++ b/tests/linkml/test_generators/golden/personinfo_sqla_2x.py @@ -0,0 +1,722 @@ +from __future__ import annotations + +from datetime import date, datetime, time +from decimal import Decimal + +from sqlalchemy import ( + Boolean, + Date, + DateTime, + Enum, + Float, + ForeignKey, + Integer, + Numeric, + Text, + Time, +) +from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship + + +class Base(DeclarativeBase): + pass + + +metadata = Base.metadata + + +class NamedThing(Base): + """ + A generic grouping for any identifiable entity + """ + + __tablename__ = "NamedThing" + + id: Mapped[str] = mapped_column(Text(), primary_key=True) + name: Mapped[str] = mapped_column(Text()) + description: Mapped[str | None] = mapped_column(Text()) + depicted_by: Mapped[str | None] = mapped_column(Text()) + + def __repr__(self): + return f"NamedThing(id={self.id},name={self.name},description={self.description},depicted_by={self.depicted_by},)" + + +class HasAliases(Base): + """ + A mixin applied to any class that can have aliases/alternateNames + """ + + __tablename__ = "HasAliases" + + id: Mapped[int] = mapped_column(Integer(), primary_key=True, autoincrement=True) + + aliases_rel: Mapped[list[HasAliasesAlias]] = relationship() + aliases: AssociationProxy[list[str]] = association_proxy( + "aliases_rel", + "alias", + creator=lambda x_: HasAliasesAlias(alias=x_), + ) + + def __repr__(self): + return f"HasAliases(id={self.id},)" + + +class HasNewsEvents(Base): + """ + None + """ + + __tablename__ = "HasNewsEvents" + + id: Mapped[int] = mapped_column(Integer(), primary_key=True, autoincrement=True) + + # ManyToMany + has_news_events: Mapped[list[NewsEvent]] = relationship(secondary="HasNewsEvents_has_news_event") + + def __repr__(self): + return f"HasNewsEvents(id={self.id},)" + + +class Place(Base): + """ + None + """ + + __tablename__ = "Place" + + id: Mapped[str] = mapped_column(Text(), primary_key=True) + name: Mapped[str] = mapped_column(Text()) + depicted_by: Mapped[str | None] = mapped_column(Text()) + Container_id: Mapped[int | None] = mapped_column(Integer(), ForeignKey("Container.id")) + + aliases_rel: Mapped[list[PlaceAlias]] = relationship() + aliases: AssociationProxy[list[str]] = association_proxy( + "aliases_rel", + "alias", + creator=lambda x_: PlaceAlias(alias=x_), + ) + + def __repr__(self): + return f"Place(id={self.id},name={self.name},depicted_by={self.depicted_by},Container_id={self.Container_id},)" + + +class Address(Base): + """ + None + """ + + __tablename__ = "Address" + + id: Mapped[int] = mapped_column(Integer(), primary_key=True, autoincrement=True) + street: Mapped[str | None] = mapped_column(Text()) + city: Mapped[str | None] = mapped_column(Text()) + postal_code: Mapped[str | None] = mapped_column(Text()) + + def __repr__(self): + return f"Address(id={self.id},street={self.street},city={self.city},postal_code={self.postal_code},)" + + +class Event(Base): + """ + None + """ + + __tablename__ = "Event" + + id: Mapped[int] = mapped_column(Integer(), primary_key=True, autoincrement=True) + started_at_time: Mapped[date | None] = mapped_column(Date()) + ended_at_time: Mapped[date | None] = mapped_column(Date()) + duration: Mapped[float | None] = mapped_column(Float()) + is_current: Mapped[bool | None] = mapped_column(Boolean()) + + def __repr__(self): + return f"Event(id={self.id},started_at_time={self.started_at_time},ended_at_time={self.ended_at_time},duration={self.duration},is_current={self.is_current},)" + + +class IntegerPrimaryKeyObject(Base): + """ + None + """ + + __tablename__ = "IntegerPrimaryKeyObject" + + int_id: Mapped[int] = mapped_column(Integer(), primary_key=True) + + def __repr__(self): + return f"IntegerPrimaryKeyObject(int_id={self.int_id},)" + + +class CodeSystem(Base): + """ + None + """ + + __tablename__ = "code system" + + id: Mapped[str] = mapped_column(Text(), primary_key=True) + name: Mapped[str] = mapped_column(Text()) + + def __repr__(self): + return f"code system(id={self.id},name={self.name},)" + + +class Relationship(Base): + """ + None + """ + + __tablename__ = "Relationship" + + id: Mapped[int] = mapped_column(Integer(), primary_key=True, autoincrement=True) + started_at_time: Mapped[date | None] = mapped_column(Date()) + ended_at_time: Mapped[date | None] = mapped_column(Date()) + related_to: Mapped[str | None] = mapped_column(Text(), ForeignKey("Person.id")) + type: Mapped[str | None] = mapped_column(Text()) + + def __repr__(self): + return f"Relationship(id={self.id},started_at_time={self.started_at_time},ended_at_time={self.ended_at_time},related_to={self.related_to},type={self.type},)" + + +class WithLocation(Base): + """ + None + """ + + __tablename__ = "WithLocation" + + id: Mapped[int] = mapped_column(Integer(), primary_key=True, autoincrement=True) + in_location: Mapped[str | None] = mapped_column(Text(), ForeignKey("Place.id")) + + def __repr__(self): + return f"WithLocation(id={self.id},in_location={self.in_location},)" + + +class Container(Base): + """ + None + """ + + __tablename__ = "Container" + + id: Mapped[int] = mapped_column(Integer(), primary_key=True, autoincrement=True) + + # One-To-Many: OneToAnyMapping(source_class='Container', source_slot='persons', mapping_type=None, target_class='Person', target_slot='Container_id', join_class=None, uses_join_table=None, multivalued=False) + persons: Mapped[list[Person]] = relationship(foreign_keys="[Person.Container_id]") + + # One-To-Many: OneToAnyMapping(source_class='Container', source_slot='organizations', mapping_type=None, target_class='Organization', target_slot='Container_id', join_class=None, uses_join_table=None, multivalued=False) + organizations: Mapped[list[Organization]] = relationship(foreign_keys="[Organization.Container_id]") + + # One-To-Many: OneToAnyMapping(source_class='Container', source_slot='places', mapping_type=None, target_class='Place', target_slot='Container_id', join_class=None, uses_join_table=None, multivalued=False) + places: Mapped[list[Place]] = relationship(foreign_keys="[Place.Container_id]") + + def __repr__(self): + return f"Container(id={self.id},)" + + +class PersonAlias(Base): + """ + None + """ + + __tablename__ = "Person_alias" + + Person_id: Mapped[str] = mapped_column(Text(), ForeignKey("Person.id"), primary_key=True) + alias: Mapped[str] = mapped_column(Text(), primary_key=True) + + def __repr__(self): + return f"Person_alias(Person_id={self.Person_id},alias={self.alias},)" + + +class PersonHasNewsEvent(Base): + """ + None + """ + + __tablename__ = "Person_has_news_event" + + Person_id: Mapped[str] = mapped_column(Text(), ForeignKey("Person.id"), primary_key=True) + has_news_event_id: Mapped[int] = mapped_column(Integer(), ForeignKey("NewsEvent.id"), primary_key=True) + + def __repr__(self): + return f"Person_has_news_event(Person_id={self.Person_id},has_news_event_id={self.has_news_event_id},)" + + +class HasAliasesAlias(Base): + """ + None + """ + + __tablename__ = "HasAliases_alias" + + HasAliases_id: Mapped[int] = mapped_column(Integer(), ForeignKey("HasAliases.id"), primary_key=True) + alias: Mapped[str] = mapped_column(Text(), primary_key=True) + + def __repr__(self): + return f"HasAliases_alias(HasAliases_id={self.HasAliases_id},alias={self.alias},)" + + +class HasNewsEventsHasNewsEvent(Base): + """ + None + """ + + __tablename__ = "HasNewsEvents_has_news_event" + + HasNewsEvents_id: Mapped[int] = mapped_column(Integer(), ForeignKey("HasNewsEvents.id"), primary_key=True) + has_news_event_id: Mapped[int] = mapped_column(Integer(), ForeignKey("NewsEvent.id"), primary_key=True) + + def __repr__(self): + return f"HasNewsEvents_has_news_event(HasNewsEvents_id={self.HasNewsEvents_id},has_news_event_id={self.has_news_event_id},)" + + +class OrganizationCategories(Base): + """ + None + """ + + __tablename__ = "Organization_categories" + + Organization_id: Mapped[str] = mapped_column(Text(), ForeignKey("Organization.id"), primary_key=True) + categories: Mapped[str] = mapped_column(Enum('non profit', 'for profit', 'offshore', 'charity', 'shell company', 'loose organization', name='OrganizationType'), primary_key=True) + + def __repr__(self): + return f"Organization_categories(Organization_id={self.Organization_id},categories={self.categories},)" + + +class OrganizationAlias(Base): + """ + None + """ + + __tablename__ = "Organization_alias" + + Organization_id: Mapped[str] = mapped_column(Text(), ForeignKey("Organization.id"), primary_key=True) + alias: Mapped[str] = mapped_column(Text(), primary_key=True) + + def __repr__(self): + return f"Organization_alias(Organization_id={self.Organization_id},alias={self.alias},)" + + +class OrganizationHasNewsEvent(Base): + """ + None + """ + + __tablename__ = "Organization_has_news_event" + + Organization_id: Mapped[str] = mapped_column(Text(), ForeignKey("Organization.id"), primary_key=True) + has_news_event_id: Mapped[int] = mapped_column(Integer(), ForeignKey("NewsEvent.id"), primary_key=True) + + def __repr__(self): + return f"Organization_has_news_event(Organization_id={self.Organization_id},has_news_event_id={self.has_news_event_id},)" + + +class PlaceAlias(Base): + """ + None + """ + + __tablename__ = "Place_alias" + + Place_id: Mapped[str] = mapped_column(Text(), ForeignKey("Place.id"), primary_key=True) + alias: Mapped[str] = mapped_column(Text(), primary_key=True) + + def __repr__(self): + return f"Place_alias(Place_id={self.Place_id},alias={self.alias},)" + + +class ConceptMappings(Base): + """ + None + """ + + __tablename__ = "Concept_mappings" + + Concept_id: Mapped[str] = mapped_column(Text(), ForeignKey("Concept.id"), primary_key=True) + mappings: Mapped[str] = mapped_column(Text(), primary_key=True) + + def __repr__(self): + return f"Concept_mappings(Concept_id={self.Concept_id},mappings={self.mappings},)" + + +class DiagnosisConceptMappings(Base): + """ + None + """ + + __tablename__ = "DiagnosisConcept_mappings" + + DiagnosisConcept_id: Mapped[str] = mapped_column(Text(), ForeignKey("DiagnosisConcept.id"), primary_key=True) + mappings: Mapped[str] = mapped_column(Text(), primary_key=True) + + def __repr__(self): + return f"DiagnosisConcept_mappings(DiagnosisConcept_id={self.DiagnosisConcept_id},mappings={self.mappings},)" + + +class ProcedureConceptMappings(Base): + """ + None + """ + + __tablename__ = "ProcedureConcept_mappings" + + ProcedureConcept_id: Mapped[str] = mapped_column(Text(), ForeignKey("ProcedureConcept.id"), primary_key=True) + mappings: Mapped[str] = mapped_column(Text(), primary_key=True) + + def __repr__(self): + return f"ProcedureConcept_mappings(ProcedureConcept_id={self.ProcedureConcept_id},mappings={self.mappings},)" + + +class OperationProcedureConceptMappings(Base): + """ + None + """ + + __tablename__ = "OperationProcedureConcept_mappings" + + OperationProcedureConcept_id: Mapped[str] = mapped_column(Text(), ForeignKey("OperationProcedureConcept.id"), primary_key=True) + mappings: Mapped[str] = mapped_column(Text(), primary_key=True) + + def __repr__(self): + return f"OperationProcedureConcept_mappings(OperationProcedureConcept_id={self.OperationProcedureConcept_id},mappings={self.mappings},)" + + +class ImagingProcedureConceptMappings(Base): + """ + None + """ + + __tablename__ = "ImagingProcedureConcept_mappings" + + ImagingProcedureConcept_id: Mapped[str] = mapped_column(Text(), ForeignKey("ImagingProcedureConcept.id"), primary_key=True) + mappings: Mapped[str] = mapped_column(Text(), primary_key=True) + + def __repr__(self): + return f"ImagingProcedureConcept_mappings(ImagingProcedureConcept_id={self.ImagingProcedureConcept_id},mappings={self.mappings},)" + + +class Person(NamedThing): + """ + A person (alive, dead, undead, or fictional). + """ + + __tablename__ = "Person" + + primary_email: Mapped[str | None] = mapped_column(Text()) + birth_date: Mapped[str | None] = mapped_column(Text()) + age: Mapped[int | None] = mapped_column(Integer()) + gender: Mapped[str | None] = mapped_column(Enum('nonbinary man', 'nonbinary woman', 'transgender woman', 'transgender man', 'cisgender man', 'cisgender woman', name='GenderType')) + telephone: Mapped[str | None] = mapped_column(Text()) + id: Mapped[str] = mapped_column(Text(), primary_key=True) + name: Mapped[str] = mapped_column(Text()) + description: Mapped[str | None] = mapped_column(Text()) + depicted_by: Mapped[str | None] = mapped_column(Text()) + Container_id: Mapped[int | None] = mapped_column(Integer(), ForeignKey("Container.id")) + current_address_id: Mapped[int | None] = mapped_column(Integer(), ForeignKey("Address.id")) + current_address: Mapped[Address | None] = relationship(foreign_keys=[current_address_id]) + + # One-To-Many: OneToAnyMapping(source_class='Person', source_slot='has_employment_history', mapping_type=None, target_class='EmploymentEvent', target_slot='Person_id', join_class=None, uses_join_table=None, multivalued=False) + has_employment_history: Mapped[list[EmploymentEvent]] = relationship(foreign_keys="[EmploymentEvent.Person_id]") + + # One-To-Many: OneToAnyMapping(source_class='Person', source_slot='has_familial_relationships', mapping_type=None, target_class='FamilialRelationship', target_slot='Person_id', join_class=None, uses_join_table=None, multivalued=False) + has_familial_relationships: Mapped[list[FamilialRelationship]] = relationship(foreign_keys="[FamilialRelationship.Person_id]") + + # One-To-Many: OneToAnyMapping(source_class='Person', source_slot='has_interpersonal_relationships', mapping_type=None, target_class='InterPersonalRelationship', target_slot='Person_id', join_class=None, uses_join_table=None, multivalued=False) + has_interpersonal_relationships: Mapped[list[InterPersonalRelationship]] = relationship(foreign_keys="[InterPersonalRelationship.Person_id]") + + # One-To-Many: OneToAnyMapping(source_class='Person', source_slot='has_medical_history', mapping_type=None, target_class='MedicalEvent', target_slot='Person_id', join_class=None, uses_join_table=None, multivalued=False) + has_medical_history: Mapped[list[MedicalEvent]] = relationship(foreign_keys="[MedicalEvent.Person_id]") + + aliases_rel: Mapped[list[PersonAlias]] = relationship() + aliases: AssociationProxy[list[str]] = association_proxy( + "aliases_rel", + "alias", + creator=lambda x_: PersonAlias(alias=x_), + ) + + # ManyToMany + has_news_events: Mapped[list[NewsEvent]] = relationship(secondary="Person_has_news_event") + + def __repr__(self): + return f"Person(primary_email={self.primary_email},birth_date={self.birth_date},age={self.age},gender={self.gender},telephone={self.telephone},id={self.id},name={self.name},description={self.description},depicted_by={self.depicted_by},Container_id={self.Container_id},current_address_id={self.current_address_id},)" + + __mapper_args__ = {"concrete": True} + + +class NewsEvent(Event): + """ + None + """ + + __tablename__ = "NewsEvent" + + id: Mapped[int] = mapped_column(Integer(), primary_key=True, autoincrement=True) + headline: Mapped[str | None] = mapped_column(Text()) + started_at_time: Mapped[date | None] = mapped_column(Date()) + ended_at_time: Mapped[date | None] = mapped_column(Date()) + duration: Mapped[float | None] = mapped_column(Float()) + is_current: Mapped[bool | None] = mapped_column(Boolean()) + + def __repr__(self): + return f"NewsEvent(id={self.id},headline={self.headline},started_at_time={self.started_at_time},ended_at_time={self.ended_at_time},duration={self.duration},is_current={self.is_current},)" + + __mapper_args__ = {"concrete": True} + + +class Organization(NamedThing): + """ + An organization such as a company or university + """ + + __tablename__ = "Organization" + + mission_statement: Mapped[str | None] = mapped_column(Text()) + founding_date: Mapped[str | None] = mapped_column(Text()) + founding_location: Mapped[str | None] = mapped_column(Text(), ForeignKey("Place.id")) + score: Mapped[Decimal | None] = mapped_column(Numeric()) + min_salary: Mapped[str | None] = mapped_column(Text()) + id: Mapped[str] = mapped_column(Text(), primary_key=True) + name: Mapped[str] = mapped_column(Text()) + description: Mapped[str | None] = mapped_column(Text()) + depicted_by: Mapped[str | None] = mapped_column(Text()) + Container_id: Mapped[int | None] = mapped_column(Integer(), ForeignKey("Container.id")) + + categories_rel: Mapped[list[OrganizationCategories]] = relationship() + categories: AssociationProxy[list[str]] = association_proxy( + "categories_rel", + "categories", + creator=lambda x_: OrganizationCategories(categories=x_), + ) + + aliases_rel: Mapped[list[OrganizationAlias]] = relationship() + aliases: AssociationProxy[list[str]] = association_proxy( + "aliases_rel", + "alias", + creator=lambda x_: OrganizationAlias(alias=x_), + ) + + # ManyToMany + has_news_events: Mapped[list[NewsEvent]] = relationship(secondary="Organization_has_news_event") + + def __repr__(self): + return f"Organization(mission_statement={self.mission_statement},founding_date={self.founding_date},founding_location={self.founding_location},score={self.score},min_salary={self.min_salary},id={self.id},name={self.name},description={self.description},depicted_by={self.depicted_by},Container_id={self.Container_id},)" + + __mapper_args__ = {"concrete": True} + + +class Concept(NamedThing): + """ + None + """ + + __tablename__ = "Concept" + + code_system: Mapped[str | None] = mapped_column(Text(), ForeignKey("code system.id")) + id: Mapped[str] = mapped_column(Text(), primary_key=True) + name: Mapped[str] = mapped_column(Text()) + description: Mapped[str | None] = mapped_column(Text()) + depicted_by: Mapped[str | None] = mapped_column(Text()) + + mappings_rel: Mapped[list[ConceptMappings]] = relationship() + mappings: AssociationProxy[list[str]] = association_proxy( + "mappings_rel", + "mappings", + creator=lambda x_: ConceptMappings(mappings=x_), + ) + + def __repr__(self): + return f"Concept(code_system={self.code_system},id={self.id},name={self.name},description={self.description},depicted_by={self.depicted_by},)" + + __mapper_args__ = {"concrete": True} + + +class FamilialRelationship(Relationship): + """ + None + """ + + __tablename__ = "FamilialRelationship" + + id: Mapped[int] = mapped_column(Integer(), primary_key=True, autoincrement=True) + started_at_time: Mapped[date | None] = mapped_column(Date()) + ended_at_time: Mapped[date | None] = mapped_column(Date()) + related_to: Mapped[str | None] = mapped_column(Text(), ForeignKey("Person.id")) + type: Mapped[str] = mapped_column(Enum('SIBLING_OF', 'PARENT_OF', 'CHILD_OF', name='FamilialRelationshipType')) + Person_id: Mapped[str | None] = mapped_column(Text(), ForeignKey("Person.id")) + + def __repr__(self): + return f"FamilialRelationship(id={self.id},started_at_time={self.started_at_time},ended_at_time={self.ended_at_time},related_to={self.related_to},type={self.type},Person_id={self.Person_id},)" + + __mapper_args__ = {"concrete": True} + + +class InterPersonalRelationship(Relationship): + """ + None + """ + + __tablename__ = "InterPersonalRelationship" + + id: Mapped[int] = mapped_column(Integer(), primary_key=True, autoincrement=True) + started_at_time: Mapped[date | None] = mapped_column(Date()) + ended_at_time: Mapped[date | None] = mapped_column(Date()) + related_to: Mapped[str | None] = mapped_column(Text(), ForeignKey("Person.id")) + type: Mapped[str] = mapped_column(Text()) + Person_id: Mapped[str | None] = mapped_column(Text(), ForeignKey("Person.id")) + + def __repr__(self): + return f"InterPersonalRelationship(id={self.id},started_at_time={self.started_at_time},ended_at_time={self.ended_at_time},related_to={self.related_to},type={self.type},Person_id={self.Person_id},)" + + __mapper_args__ = {"concrete": True} + + +class EmploymentEvent(Event): + """ + None + """ + + __tablename__ = "EmploymentEvent" + + id: Mapped[int] = mapped_column(Integer(), primary_key=True, autoincrement=True) + employed_at: Mapped[str | None] = mapped_column(Text(), ForeignKey("Organization.id")) + salary: Mapped[str | None] = mapped_column(Text()) + started_at_time: Mapped[date | None] = mapped_column(Date()) + ended_at_time: Mapped[date | None] = mapped_column(Date()) + duration: Mapped[float | None] = mapped_column(Float()) + is_current: Mapped[bool | None] = mapped_column(Boolean()) + Person_id: Mapped[str | None] = mapped_column(Text(), ForeignKey("Person.id")) + + def __repr__(self): + return f"EmploymentEvent(id={self.id},employed_at={self.employed_at},salary={self.salary},started_at_time={self.started_at_time},ended_at_time={self.ended_at_time},duration={self.duration},is_current={self.is_current},Person_id={self.Person_id},)" + + __mapper_args__ = {"concrete": True} + + +class MedicalEvent(Event): + """ + None + """ + + __tablename__ = "MedicalEvent" + + id: Mapped[int] = mapped_column(Integer(), primary_key=True, autoincrement=True) + in_location: Mapped[str | None] = mapped_column(Text(), ForeignKey("Place.id")) + started_at_time: Mapped[date | None] = mapped_column(Date()) + ended_at_time: Mapped[date | None] = mapped_column(Date()) + duration: Mapped[float | None] = mapped_column(Float()) + is_current: Mapped[bool | None] = mapped_column(Boolean()) + Person_id: Mapped[str | None] = mapped_column(Text(), ForeignKey("Person.id")) + diagnosis_id: Mapped[str | None] = mapped_column(Text(), ForeignKey("DiagnosisConcept.id")) + diagnosis: Mapped[DiagnosisConcept | None] = relationship(foreign_keys=[diagnosis_id]) + procedure_id: Mapped[str | None] = mapped_column(Text(), ForeignKey("ProcedureConcept.id")) + procedure: Mapped[ProcedureConcept | None] = relationship(foreign_keys=[procedure_id]) + + def __repr__(self): + return f"MedicalEvent(id={self.id},in_location={self.in_location},started_at_time={self.started_at_time},ended_at_time={self.ended_at_time},duration={self.duration},is_current={self.is_current},Person_id={self.Person_id},diagnosis_id={self.diagnosis_id},procedure_id={self.procedure_id},)" + + __mapper_args__ = {"concrete": True} + + +class DiagnosisConcept(Concept): + """ + None + """ + + __tablename__ = "DiagnosisConcept" + + code_system: Mapped[str | None] = mapped_column(Text(), ForeignKey("code system.id")) + id: Mapped[str] = mapped_column(Text(), primary_key=True) + name: Mapped[str] = mapped_column(Text()) + description: Mapped[str | None] = mapped_column(Text()) + depicted_by: Mapped[str | None] = mapped_column(Text()) + + mappings_rel: Mapped[list[DiagnosisConceptMappings]] = relationship() + mappings: AssociationProxy[list[str]] = association_proxy( + "mappings_rel", + "mappings", + creator=lambda x_: DiagnosisConceptMappings(mappings=x_), + ) + + def __repr__(self): + return f"DiagnosisConcept(code_system={self.code_system},id={self.id},name={self.name},description={self.description},depicted_by={self.depicted_by},)" + + __mapper_args__ = {"concrete": True} + + +class ProcedureConcept(Concept): + """ + None + """ + + __tablename__ = "ProcedureConcept" + + code_system: Mapped[str | None] = mapped_column(Text(), ForeignKey("code system.id")) + id: Mapped[str] = mapped_column(Text(), primary_key=True) + name: Mapped[str] = mapped_column(Text()) + description: Mapped[str | None] = mapped_column(Text()) + depicted_by: Mapped[str | None] = mapped_column(Text()) + + mappings_rel: Mapped[list[ProcedureConceptMappings]] = relationship() + mappings: AssociationProxy[list[str]] = association_proxy( + "mappings_rel", + "mappings", + creator=lambda x_: ProcedureConceptMappings(mappings=x_), + ) + + def __repr__(self): + return f"ProcedureConcept(code_system={self.code_system},id={self.id},name={self.name},description={self.description},depicted_by={self.depicted_by},)" + + __mapper_args__ = {"concrete": True} + + +class OperationProcedureConcept(ProcedureConcept): + """ + None + """ + + __tablename__ = "OperationProcedureConcept" + + code_system: Mapped[str | None] = mapped_column(Text(), ForeignKey("code system.id")) + id: Mapped[str] = mapped_column(Text(), primary_key=True) + name: Mapped[str] = mapped_column(Text()) + description: Mapped[str | None] = mapped_column(Text()) + depicted_by: Mapped[str | None] = mapped_column(Text()) + + mappings_rel: Mapped[list[OperationProcedureConceptMappings]] = relationship() + mappings: AssociationProxy[list[str]] = association_proxy( + "mappings_rel", + "mappings", + creator=lambda x_: OperationProcedureConceptMappings(mappings=x_), + ) + + def __repr__(self): + return f"OperationProcedureConcept(code_system={self.code_system},id={self.id},name={self.name},description={self.description},depicted_by={self.depicted_by},)" + + __mapper_args__ = {"concrete": True} + + +class ImagingProcedureConcept(ProcedureConcept): + """ + None + """ + + __tablename__ = "ImagingProcedureConcept" + + code_system: Mapped[str | None] = mapped_column(Text(), ForeignKey("code system.id")) + id: Mapped[str] = mapped_column(Text(), primary_key=True) + name: Mapped[str] = mapped_column(Text()) + description: Mapped[str | None] = mapped_column(Text()) + depicted_by: Mapped[str | None] = mapped_column(Text()) + + mappings_rel: Mapped[list[ImagingProcedureConceptMappings]] = relationship() + mappings: AssociationProxy[list[str]] = association_proxy( + "mappings_rel", + "mappings", + creator=lambda x_: ImagingProcedureConceptMappings(mappings=x_), + ) + + def __repr__(self): + return f"ImagingProcedureConcept(code_system={self.code_system},id={self.id},name={self.name},description={self.description},depicted_by={self.depicted_by},)" + + __mapper_args__ = {"concrete": True} diff --git a/tests/linkml/test_generators/test_sqlalchemygen.py b/tests/linkml/test_generators/test_sqlalchemygen.py index 8efd227664..070cc52695 100644 --- a/tests/linkml/test_generators/test_sqlalchemygen.py +++ b/tests/linkml/test_generators/test_sqlalchemygen.py @@ -1,13 +1,15 @@ import logging import re from collections import Counter +from pathlib import Path import pytest +from click.testing import CliRunner from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker -from linkml.generators.sqlalchemygen import SQLAlchemyGenerator, TemplateEnum -from linkml.generators.sqltablegen import SQLTableGenerator +from linkml.generators.sqlalchemygen import SQLAlchemyGenerator, TemplateEnum, cli +from linkml.generators.sqltablegen import RANGEMAP, SQL_TYPE_TO_PYTHON_TYPE, SQLTableGenerator from linkml.utils.schema_builder import SchemaBuilder from linkml_runtime.linkml_model import SlotDefinition @@ -21,6 +23,11 @@ def schema(input_path): return str(input_path("personinfo.yaml")) +@pytest.fixture +def schema_path(input_path): + return str(input_path("personinfo.yaml")) + + def test_sqla_basic_imperative(schema): """ Test generation of DDL for imperative mode @@ -317,3 +324,152 @@ def test_sqla_declarative_exec(schema): session.commit() session.close() engine.dispose() + + +# --- 2.x declarative tests --- + + +def test_sql_type_to_python_type_covers_rangemap(): + """Every value in RANGEMAP should have a corresponding Python type.""" + for sql_type in RANGEMAP.values(): + assert repr(sql_type) in SQL_TYPE_TO_PYTHON_TYPE, f"Missing mapping for {repr(sql_type)}" + + +def test_sqla_2x_basic_declarative(schema): + """Test generation of 2.x declarative classes produces valid structure.""" + gen = SQLAlchemyGenerator(schema) + code = gen.generate_sqla(template=TemplateEnum.DECLARATIVE_2X) + + # Should use 2.x patterns + assert "class Base(DeclarativeBase):" in code + assert "Mapped[" in code + assert "mapped_column(" in code + + # Should NOT use 1.x patterns + assert "declarative_base()" not in code + assert "= Column(" not in code + + # Should still have all expected classes + expected_classes = [ + "NamedThing", + "Person", + "Organization", + "Place", + "Address", + "Event", + "Concept", + "DiagnosisConcept", + "ProcedureConcept", + "Relationship", + "FamilialRelationship", + "EmploymentEvent", + "MedicalEvent", + ] + for expected in expected_classes: + assert f"class {expected}(" in code + + +def test_sqla_2x_declarative_exec(schema): + """Full integration test: generate 2.x declarative, create DB, insert, query.""" + engine = create_engine("sqlite://") + ddl = SQLTableGenerator(schema).generate_ddl() + with engine.connect() as connection: + cur = connection.connection.cursor() + cur.executescript(ddl) + + session_class = sessionmaker(bind=engine) + session = session_class() + gen = SQLAlchemyGenerator(schema) + mod = gen.compile_sqla(template=TemplateEnum.DECLARATIVE_2X) + + # Insert and query + session.add(mod.DiagnosisConcept(id="C999", name="rash")) + e1 = mod.MedicalEvent(duration=100.0, diagnosis_id="C999") + dc = mod.DiagnosisConcept(id="C001", name="cough") + e2 = mod.MedicalEvent(duration=200.0, diagnosis=dc) + + p1 = mod.Person(id="P1", name="a b", age=22, has_medical_history=[e1, e2]) + p1.aliases = ["Anne"] + p1.aliases.append("Fred") + p1.has_familial_relationships.append(mod.FamilialRelationship(related_to="P2", type="SIBLING_OF")) + p1.current_address = mod.Address(street="1 a street", city="big city", postal_code="ZZ1 ZZ2") + session.add(p1) + session.add(mod.Person(id="P2", name="Ferdinand Giggleheim", aliases=["Fred"])) + session.commit() + + q = session.query(mod.Person).where(mod.Person.name == p1.name) + persons = q.all() + assert len(persons) == 1 + p1_from_query = persons[0] + assert p1_from_query.age == 22 + assert Counter(p1_from_query.aliases) == Counter(["Anne", "Fred"]) + assert len(p1_from_query.has_medical_history) == 2 + assert any(e for e in p1_from_query.has_medical_history if e.diagnosis_id == "C999") + assert any(e for e in p1_from_query.has_medical_history if e.diagnosis.name == "cough") + assert any(r for r in p1_from_query.has_familial_relationships if r.related_to == "P2") + + session.commit() + session.close() + engine.dispose() + + +def test_2x_mixin(): + """Test that mixins work with 2.x declarative template.""" + b = SchemaBuilder() + b.add_slot(SlotDefinition("ref_to_c1", range="my_class1", multivalued=True)) + b.add_class("my_mixin", slots=["my_mixin_slot"], mixin=True) + b.add_class("my_abstract", slots=["my_abstract_slot"], abstract=True) + b.add_class("my_class1", is_a="my_abstract", mixins=["my_mixin"]) + b.add_class("my_class2", slots=["ref_to_c1"]) + gen = SQLAlchemyGenerator(b.schema) + mod = gen.compile_sqla(template=TemplateEnum.DECLARATIVE_2X) + i1 = mod.MyClass1(my_mixin_slot="v1", my_abstract_slot="v2") + i2 = mod.MyClass2(ref_to_c1=[i1]) + assert i2.ref_to_c1[0] == i1 + + +def test_sqla_2x_enum_columns(schema): + """Enum-backed columns should use str type annotation and Enum() column type.""" + gen = SQLAlchemyGenerator(schema) + code = gen.generate_sqla(template=TemplateEnum.DECLARATIVE_2X) + + # FamilialRelationship.type is enum-backed — should appear as Mapped with Enum + assert "Enum(" in code + # Should compile and run without error + mod = gen.compile_sqla(template=TemplateEnum.DECLARATIVE_2X) + assert hasattr(mod, "FamilialRelationship") + + +def test_sqla_2x_cli(schema_path): + """Smoke test for --sqla-style 2 CLI flag.""" + runner = CliRunner() + result = runner.invoke(cli, [schema_path, "--sqla-style", "2"]) + assert result.exit_code == 0 + assert "class Base(DeclarativeBase):" in result.output + assert "Mapped[" in result.output + + +def test_sqla_style_without_declarative_fails(schema_path): + """--sqla-style with --no-declarative should error.""" + runner = CliRunner() + result = runner.invoke(cli, [schema_path, "--no-declarative", "--sqla-style", "2"]) + assert result.exit_code != 0 + assert "only applies in declarative mode" in result.output + + +GOLDEN_FILE = Path(__file__).parent / "golden" / "personinfo_sqla_2x.py" + + +def test_sqla_2x_golden_file(schema): + """Generated 2.x output should match the checked-in golden file.""" + gen = SQLAlchemyGenerator(schema) + code = gen.generate_sqla(template=TemplateEnum.DECLARATIVE_2X) + if not GOLDEN_FILE.exists(): + GOLDEN_FILE.parent.mkdir(parents=True, exist_ok=True) + GOLDEN_FILE.write_text(code) + pytest.skip("Golden file created — review and commit it, then re-run.") + expected = GOLDEN_FILE.read_text() + assert code == expected, ( + f"Generated output differs from golden file {GOLDEN_FILE}. " + "If the change is intentional, delete the golden file and re-run to regenerate." + ) From 4299993c2d1c0bcdf0c324d0cc2fa9723ddafe79 Mon Sep 17 00:00:00 2001 From: Sujay Patil Date: Thu, 2 Apr 2026 17:08:46 -0700 Subject: [PATCH 09/19] add relative page boosts on element pages and also limit search context --- .../src/linkml/generators/docgen/class.md.jinja2 | 9 +++++++++ .../linkml/src/linkml/generators/docgen/enum.md.jinja2 | 9 +++++++++ .../linkml/src/linkml/generators/docgen/slot.md.jinja2 | 9 +++++++++ .../src/linkml/generators/docgen/subset.md.jinja2 | 10 ++++++++++ .../linkml/src/linkml/generators/docgen/type.md.jinja2 | 9 +++++++++ 5 files changed, 46 insertions(+) diff --git a/packages/linkml/src/linkml/generators/docgen/class.md.jinja2 b/packages/linkml/src/linkml/generators/docgen/class.md.jinja2 index 356511e33e..7eca2c7a44 100644 --- a/packages/linkml/src/linkml/generators/docgen/class.md.jinja2 +++ b/packages/linkml/src/linkml/generators/docgen/class.md.jinja2 @@ -1,3 +1,8 @@ +--- +search: + boost: {% if element.deprecated %}0.5{% else %}10.0{% endif %} +--- + {%- if element.title %} {%- set title = element.title ~ ' (' ~ element.name ~ ')' -%} {%- else %} @@ -34,6 +39,8 @@ _{{ element_description_line }}_ {% endfor %} {% endif %} +
+ {% if element.abstract %} * __NOTE__: this is an abstract class and should not be instantiated directly {% endif %} @@ -279,3 +286,5 @@ The class must not satisfy any of: {%- if footer -%} {{ footer }} {%- endif -%} + +
\ No newline at end of file diff --git a/packages/linkml/src/linkml/generators/docgen/enum.md.jinja2 b/packages/linkml/src/linkml/generators/docgen/enum.md.jinja2 index e840fae21d..c2988f50e2 100644 --- a/packages/linkml/src/linkml/generators/docgen/enum.md.jinja2 +++ b/packages/linkml/src/linkml/generators/docgen/enum.md.jinja2 @@ -1,3 +1,8 @@ +--- +search: + boost: {% if element.deprecated %}0.5{% else %}2.0{% endif %} +--- + {%- if element.title and element.title != element.name %} {%- set title = element.title ~ ' (' ~ element.name ~ ')' -%} {%- else %} @@ -13,6 +18,8 @@ _{{ element_description_line }}_ {% endfor %} {% endif %} +
+ URI: {{ gen.uri_link(element) }} {%- if element.enum_uri %} @@ -137,3 +144,5 @@ _This is a dynamic enum_ {{ gen.yaml(element) }} ``` + +
\ No newline at end of file diff --git a/packages/linkml/src/linkml/generators/docgen/slot.md.jinja2 b/packages/linkml/src/linkml/generators/docgen/slot.md.jinja2 index 75344cf50c..45ae3a2b2d 100644 --- a/packages/linkml/src/linkml/generators/docgen/slot.md.jinja2 +++ b/packages/linkml/src/linkml/generators/docgen/slot.md.jinja2 @@ -1,3 +1,8 @@ +--- +search: + boost: {% if element.deprecated %}0.5{% else %}5.0{% endif %} +--- + {%- if element.title %} {%- set title = element.title ~ ' (' ~ element.name ~ ')' -%} {%- else %} @@ -34,6 +39,8 @@ _{{ element_description_line }}_ {% endfor %} {% endif %} +
+ {% if element.abstract %} * __NOTE__: this is an abstract slot and should not be populated directly {% endif %} @@ -452,3 +459,5 @@ Value must not satisfy any of: {%- if footer -%} {{ footer }} {%- endif -%} + +
\ No newline at end of file diff --git a/packages/linkml/src/linkml/generators/docgen/subset.md.jinja2 b/packages/linkml/src/linkml/generators/docgen/subset.md.jinja2 index 94e8985994..5e2ebf4821 100644 --- a/packages/linkml/src/linkml/generators/docgen/subset.md.jinja2 +++ b/packages/linkml/src/linkml/generators/docgen/subset.md.jinja2 @@ -1,3 +1,9 @@ +--- +search: + boost: {% if element.deprecated %}0.5{% else %}1.0{% endif %} +--- + + # Subset: {{ gen.name(element) }} {% if element.deprecated %} (DEPRECATED) {% endif %} {%- if header -%} @@ -11,6 +17,8 @@ _{{ element_description_line }}_ {% endfor %} {% endif %} +
+ URI: {{ gen.link(element) }} {% include "common_metadata.md.jinja2" %} @@ -101,3 +109,5 @@ URI: {{ gen.link(element) }} {% endfor %} {%- endif %} + +
\ No newline at end of file diff --git a/packages/linkml/src/linkml/generators/docgen/type.md.jinja2 b/packages/linkml/src/linkml/generators/docgen/type.md.jinja2 index e109b47a64..66f49f770c 100644 --- a/packages/linkml/src/linkml/generators/docgen/type.md.jinja2 +++ b/packages/linkml/src/linkml/generators/docgen/type.md.jinja2 @@ -1,3 +1,8 @@ +--- +search: + boost: {% if element.deprecated %}0.5{% else %}1.0{% endif %} +--- + {%- if element.title and element.title != element.name %} {%- set title = element.title ~ ' (' ~ element.name ~ ')' -%} {%- else %} @@ -13,6 +18,8 @@ _{{ element_description_line }}_ {% endfor %} {% endif %} +
+ URI: {{ gen.uri_link(element) }} ## Type Properties @@ -132,3 +139,5 @@ URI: {{ gen.uri_link(element) }} {% endif %} {% include "common_metadata.md.jinja2" %} + +
\ No newline at end of file From ed89a88d8e9766b7d3300c29315453db2ffad8ea Mon Sep 17 00:00:00 2001 From: David Linke Date: Fri, 3 Apr 2026 16:56:16 +0200 Subject: [PATCH 10/19] Consolidate SchemaBuilder: deprecate linkml copy in favor of linkml_runtime Add support for dict-based slots, dict-of-dicts slot_usage, and list slot_usage in add_class(). Move general SchemaBuilder tests from linkml to linkml_runtime test suite. --- docs/code/deprecation.rst | 2 +- docs/developers/schemabuilder.rst | 8 +- .../transformers/logical_model_transformer.py | 2 +- .../linkml/src/linkml/utils/deprecation.py | 18 +- .../linkml/src/linkml/utils/schema_builder.py | 278 +----------------- .../linkml_runtime/utils/schema_builder.py | 40 ++- tests/linkml/test_data/test_sqlite.py | 2 +- tests/linkml/test_generators/test_owlgen.py | 2 +- .../test_generators/test_pydanticgen.py | 2 +- .../test_generators/test_sqlalchemygen.py | 2 +- .../test_generators/test_sqltablegen.py | 2 +- .../test_generators/test_sqlvalidationgen.py | 2 +- .../test_generators/test_typescriptgen.py | 2 +- .../test_issues/test_linkml_issue_872.py | 2 +- tests/linkml/test_linter/test_linter.py | 2 +- .../test_rule_canonical_prefixes.py | 2 +- .../test_linter/test_rule_no_empty_title.py | 2 +- .../test_rule_no_invalid_slot_usage.py | 2 +- .../test_linter/test_rule_no_xsd_int_type.py | 2 +- .../test_rule_permissible_values_format.py | 2 +- .../test_linter/test_rule_recommended.py | 2 +- .../test_linter/test_rule_standard_naming.py | 2 +- .../test_linter/test_rule_tree_root_class.py | 2 +- .../test_logical_model_transformer.py | 2 +- .../test_relmodel_transformer.py | 2 +- .../test_rollup_tranformer.py | 2 +- .../test_transformers/test_sqltransform.py | 2 +- tests/linkml/test_utils/test_helpers.py | 2 +- .../linkml/test_utils/test_schema_builder.py | 61 +--- tests/linkml/test_utils/test_schema_fixer.py | 2 +- .../test_utils/test_schema_builder.py | 139 +++++++++ 31 files changed, 229 insertions(+), 365 deletions(-) diff --git a/docs/code/deprecation.rst b/docs/code/deprecation.rst index f036c6afff..7c64ae1144 100644 --- a/docs/code/deprecation.rst +++ b/docs/code/deprecation.rst @@ -3,7 +3,7 @@ Deprecation Log --------------------- -See `Deprecation Guide <../maintainers/deprecation.md>`_ +See :ref:`deprecation-guide` .. plot:: plots/deprecation_plot.py diff --git a/docs/developers/schemabuilder.rst b/docs/developers/schemabuilder.rst index 318911c9ac..32102d1e2a 100644 --- a/docs/developers/schemabuilder.rst +++ b/docs/developers/schemabuilder.rst @@ -1,10 +1,14 @@ SchemaBuilder ------------- -The SchemaBuilder class in the linkml package provides a way of programmatically constructing +The SchemaBuilder class in the linkml_runtime package provides a way of programmatically constructing schemas following a `Builder pattern `_. -.. currentmodule:: linkml.utils.schema_builder +.. deprecated:: + Importing ``SchemaBuilder`` from ``linkml.utils.schema_builder`` is deprecated. + Use ``from linkml_runtime.utils.schema_builder import SchemaBuilder`` instead. + +.. currentmodule:: linkml_runtime.utils.schema_builder .. autoclass:: SchemaBuilder :members: diff --git a/packages/linkml/src/linkml/transformers/logical_model_transformer.py b/packages/linkml/src/linkml/transformers/logical_model_transformer.py index fa53043490..e73229190f 100644 --- a/packages/linkml/src/linkml/transformers/logical_model_transformer.py +++ b/packages/linkml/src/linkml/transformers/logical_model_transformer.py @@ -145,7 +145,7 @@ class LogicalModelTransformer(ModelTransformer): To demonstrate, consider a simple schema with a class Person inheriting from Thing: - >>> from linkml.utils.schema_builder import SchemaBuilder + >>> from linkml_runtime.utils.schema_builder import SchemaBuilder >>> sb = SchemaBuilder() >>> _ = sb.add_class("Thing", slots=["id", "name"]) >>> _ = sb.add_class("Person", slots={"age": {"range": "integer"}}, is_a="Thing") diff --git a/packages/linkml/src/linkml/utils/deprecation.py b/packages/linkml/src/linkml/utils/deprecation.py index 7ebb850c99..a4ee84b3db 100644 --- a/packages/linkml/src/linkml/utils/deprecation.py +++ b/packages/linkml/src/linkml/utils/deprecation.py @@ -5,15 +5,14 @@ - Tracking deprecated and removed in versions - Fail tests when something marked as removed_in is still present in the specified version -Initial draft for deprecating Pydantic 1, to make more general, needs -- function wrapper version -- ... - To deprecate something: - Create a :class:`.Deprecation` object within the `DEPRECATIONS` tuple - Use the :func:`.deprecation_warning` function wherever the deprecated feature would be used to emit the warning +See also + +- https://linkml.io/linkml/maintainers/deprecation.html """ import functools @@ -269,6 +268,17 @@ def warn(self, stack_level=3, **kwargs): "or set it explicitly to `False` to preserve current behaviour and silence this warning.", issue=3191, ), + Deprecation( + name="schema-builder-import-location", + deprecated_in=SemVer.from_str("1.11.0"), + removed_in=SemVer.from_str("1.12.0"), + message=( + "Importing SchemaBuilder from linkml.utils.schema_builder is deprecated. " + "SchemaBuilder now lives only in linkml_runtime." + ), + recommendation="Use `from linkml_runtime.utils.schema_builder import SchemaBuilder` instead.", + issue=2372, + ), ) # type: tuple[Deprecation, ...] EMITTED = set() # type: set[str] diff --git a/packages/linkml/src/linkml/utils/schema_builder.py b/packages/linkml/src/linkml/utils/schema_builder.py index cea75e8bb0..099ea888f3 100644 --- a/packages/linkml/src/linkml/utils/schema_builder.py +++ b/packages/linkml/src/linkml/utils/schema_builder.py @@ -1,274 +1,12 @@ -from dataclasses import dataclass -from typing import Any +""" +.. deprecated:: + Import SchemaBuilder from ``linkml_runtime.utils.schema_builder`` instead. +""" -from linkml_runtime.linkml_model import ( - ClassDefinition, - EnumDefinition, - PermissibleValue, - Prefix, - SchemaDefinition, - SlotDefinition, - TypeDefinition, -) -from linkml_runtime.utils.formatutils import underscore -from linkml_runtime.utils.schema_as_dict import schema_as_dict +from linkml.utils.deprecation import deprecation_warning +deprecation_warning("schema-builder-import-location") -@dataclass -class SchemaBuilder: - """ - Builder class for SchemaDefinitions. +from linkml_runtime.utils.schema_builder import SchemaBuilder # noqa: E402 - Example: - - >>> from linkml.utils.schema_builder import SchemaBuilder - >>> sb = SchemaBuilder('test-schema') - >>> _ = sb.add_class('Person', slots=['name', 'age']) - >>> _ = sb.add_class('Organization', slots=['name', 'employees']) - >>> _ = sb.add_slot('name',description='Name of the person or organization', replace_if_present=True) - >>> _ = sb.add_slot('age',description='Age of the person', range='integer', replace_if_present=True) - >>> schema = sb.schema - - Most builder methods accepts either a string, an instance of a metamodel element, - or a dictionary. If a string is provided, then a new element is created with this - as the name. - - This follows the standard Builder pattern, so the results of a build operation - are a builder, allowing chaining. For example: - - >>> _ = SchemaBuilder('test-schema').add_class('Person', slots=['name', 'age']) - - """ - - name: str | None = None - """Initialized name for the schema.""" - - id: str | None = None - """Initialized id for the schema.""" - - schema: SchemaDefinition = None - """generated SchemaDefinition object.""" - - def __post_init__(self): - name = self.name - if name is None: - name = "test-schema" - id = self.id if self.id else f"http://example.org/{name}" - self.schema = SchemaDefinition(id=id, name=name) - - def add_class( - self, - cls: ClassDefinition | dict | str, - slots: dict | list[str | SlotDefinition] = None, - slot_usage: dict[str, SlotDefinition] | dict[str, Any] | list[SlotDefinition] = None, - replace_if_present=False, - use_attributes=False, - **kwargs, - ) -> "SchemaBuilder": - """ - Adds a class to the schema. - - :param cls: name, dict object, or ClassDefinition object to add - :param slots: slot of slot names or slot objects. - :param slot_usage: slots keyed by slot name - :param replace_if_present: if True, replace existing class if present - :param kwargs: additional ClassDefinition properties - :param use_attributes: if True, add slots as attributes - :return: builder - :raises ValueError: if class already exists and replace_if_present=False - """ - if isinstance(cls, str): - cls = ClassDefinition(cls, **kwargs) - if isinstance(cls, dict): - cls = ClassDefinition(**{**cls, **kwargs}) - if cls.name is self.schema.classes and not replace_if_present: - raise ValueError(f"Class {cls.name} already exists") - self.schema.classes[cls.name] = cls - if use_attributes: - for s in slots: - if isinstance(s, SlotDefinition): - cls.attributes[s.name] = s - elif isinstance(s, dict): - cls.attributes[s["name"]] = s - elif isinstance(s, str): - cls.attributes[s] = SlotDefinition(s) - else: - raise ValueError("If use_attributes=True then slots must be SlotDefinitions") - else: - if slots is not None: - if isinstance(slots, dict): - for k, v in slots.items(): - cls.slots.append(k) - self.add_slot(SlotDefinition(k, **v), replace_if_present=replace_if_present) - else: - for s in slots: - cls.slots.append(s.name if isinstance(s, SlotDefinition) else s) - if isinstance(s, str) and s in self.schema.slots: - # top-level slot already exists - continue - self.add_slot(s, replace_if_present=replace_if_present) - if slot_usage: - if isinstance(slot_usage, dict): - for k, v in slot_usage.items(): - if isinstance(v, dict): - v = SlotDefinition(k, **v) - cls.slot_usage[k] = v - elif isinstance(slot_usage, list): - for s in slot_usage: - cls.slot_usage[s.name] = s - else: - raise ValueError(f"slot_usage {slot_usage} must be a dict or list of SlotDefinitions") - for k, v in kwargs.items(): - setattr(cls, k, v) - return self - - def add_slot( - self, - slot: SlotDefinition | dict | str, - class_name: str = None, - replace_if_present=False, - **kwargs, - ) -> "SchemaBuilder": - """ - Adds the slot to the schema. - - :param slot: name, dict object, or SlotDefinition object to add - :param class_name: if specified, this will become a valid slot for this class - :param replace_if_present: if True, replace existing slot if present - :param kwargs: additional properties - :return: builder - :raises ValueError: if slot already exists and replace_if_present=False - """ - if isinstance(slot, str): - slot = SlotDefinition(slot, **kwargs) - elif isinstance(slot, dict): - slot = SlotDefinition(**{**slot, **kwargs}) - if not replace_if_present and slot.name in self.schema.slots: - raise ValueError(f"Slot {slot.name} already exists") - self.schema.slots[slot.name] = slot - if class_name is not None: - self.schema.classes[class_name].slots.append(slot.name) - return self - - def set_slot(self, slot_name: str, **kwargs) -> "SchemaBuilder": - """ - Set details of the slot - - :param slot_name: - :param kwargs: - :return: builder - """ - slot = self.schema.slots[slot_name] - for k, v in kwargs.items(): - setattr(slot, k, v) - return self - - def add_enum( - self, - enum_def: EnumDefinition | dict | str, - permissible_values: list[str | PermissibleValue] = None, - replace_if_present=False, - **kwargs, - ) -> "SchemaBuilder": - """ - Adds an enum to the schema - - :param enum_def: - :param permissible_values: - :param replace_if_present: - :param kwargs: - :return: builder - :raises ValueError: if enum already exists and replace_if_present=False - """ - if not isinstance(enum_def, EnumDefinition): - enum_def = EnumDefinition(enum_def, **kwargs) - if isinstance(enum_def, dict): - enum_def = EnumDefinition(**{**enum_def, **kwargs}) - if enum_def.name in self.schema.enums and not replace_if_present: - raise ValueError(f"Enum {enum_def.name} already exists") - self.schema.enums[enum_def.name] = enum_def - if permissible_values is not None: - for pv in permissible_values: - if isinstance(pv, str): - pv = PermissibleValue(text=pv) - enum_def.permissible_values[pv.text] = pv - else: - enum_def.permissible_values[pv.text] = pv - return self - - def add_prefix(self, prefix: str, url: str, replace_if_present=False) -> "SchemaBuilder": - """ - Adds a prefix for use with CURIEs - - :param prefix: - :param url: - :param replace_if_present: - :return: builder - :raises ValueError: if prefix already exists and replace_if_present=False - """ - obj = Prefix(prefix_prefix=prefix, prefix_reference=url) - if prefix in self.schema.prefixes and not replace_if_present: - raise ValueError(f"Prefix {prefix} already exists") - self.schema.prefixes[obj.prefix_prefix] = obj - return self - - def add_defaults(self) -> "SchemaBuilder": - """ - Sets defaults, including: - - - default_range - - default imports to include linkml:types - - default prefixes - - :return: builder - """ - name = underscore(self.schema.name) - uri = self.schema.id - self.schema.default_range = "string" - self.schema.default_prefix = name - self.schema.imports.append("linkml:types") - self.add_prefix("linkml", "https://w3id.org/linkml/") - self.add_prefix(name, f"{uri}/") - return self - - def add_type( - self, - type: TypeDefinition | dict | str, - typeof: str = None, - uri: str = None, - replace_if_present=False, - **kwargs, - ) -> "SchemaBuilder": - """ - Adds the type to the schema - - :param type: - :param typeof: if specified, the parent type - :param uri: if specified, the URI or curie of the type - :param replace_if_present: - :param kwargs: - :return: builder - :raises ValueError: if type already exists and replace_if_present=False - """ - if isinstance(type, str): - type = TypeDefinition(type) - elif isinstance(type, dict): - type = TypeDefinition(**type) - if typeof: - type.typeof = typeof - if not replace_if_present and type.name in self.schema.types: - raise ValueError(f"Type {type.name} already exists") - self.schema.types[type.name] = type - for k, v in kwargs.items(): - setattr(type, k, v) - return self - - def as_dict(self) -> dict: - """ - Returns the schema as a dictionary. - - Compaction is performed to eliminate redundant keys - - :return: dictionary representation - """ - return schema_as_dict(self.schema) +__all__ = ["SchemaBuilder"] diff --git a/packages/linkml_runtime/src/linkml_runtime/utils/schema_builder.py b/packages/linkml_runtime/src/linkml_runtime/utils/schema_builder.py index 7cbb897b71..b60f15c636 100644 --- a/packages/linkml_runtime/src/linkml_runtime/utils/schema_builder.py +++ b/packages/linkml_runtime/src/linkml_runtime/utils/schema_builder.py @@ -58,8 +58,8 @@ def __post_init__(self): def add_class( self, cls: ClassDefinition | dict | str, - slots: list[str | SlotDefinition] = None, - slot_usage: dict[str, SlotDefinition] = None, + slots: dict[str, dict] | list[str | SlotDefinition] = None, + slot_usage: dict[str, SlotDefinition | dict] | list[SlotDefinition] = None, replace_if_present: bool = False, use_attributes: bool = False, **kwargs, @@ -68,9 +68,12 @@ def add_class( Adds a class to the schema. :param cls: name, dict object, or ClassDefinition object to add - :param slots: list of slot names or slot objects. This must be a list of - `SlotDefinition` objects if `use_attributes=True` - :param slot_usage: slots keyed by slot name (ignored if `use_attributes=True`) + :param slots: list of slot names or slot objects, or a dict mapping slot names + to dicts of slot properties. Must be a list of `SlotDefinition` objects + if `use_attributes=True` + :param slot_usage: dict mapping slot names to `SlotDefinition` objects or dicts + of slot properties, or a list of `SlotDefinition` objects. Ignored if + `use_attributes=True` :param replace_if_present: if True, replace existing class if present :param use_attributes: Whether to specify the given slots as an inline definition of slots, attributes, in the class definition @@ -107,14 +110,25 @@ def add_class( else: raise ValueError("If use_attributes=True then slots must be SlotDefinitions") else: - for s in slots: - cls.slots.append(s.name if isinstance(s, SlotDefinition) else s) - if isinstance(s, str) and s in self.schema.slots: - # top-level slot already exists - continue - self.add_slot(s, replace_if_present=replace_if_present) - for k, v in slot_usage.items(): - cls.slot_usage[k] = v + if isinstance(slots, dict): + for k, v in slots.items(): + cls.slots.append(k) + self.add_slot(SlotDefinition(k, **v), replace_if_present=replace_if_present) + else: + for s in slots: + cls.slots.append(s.name if isinstance(s, SlotDefinition) else s) + if isinstance(s, str) and s in self.schema.slots: + # top-level slot already exists + continue + self.add_slot(s, replace_if_present=replace_if_present) + if isinstance(slot_usage, list): + for s in slot_usage: + cls.slot_usage[s.name] = s + else: + for k, v in slot_usage.items(): + if isinstance(v, dict): + v = SlotDefinition(k, **v) + cls.slot_usage[k] = v return self def add_slot( diff --git a/tests/linkml/test_data/test_sqlite.py b/tests/linkml/test_data/test_sqlite.py index ec8adfc9f3..38dce4ffb8 100644 --- a/tests/linkml/test_data/test_sqlite.py +++ b/tests/linkml/test_data/test_sqlite.py @@ -3,12 +3,12 @@ import pytest from sqlalchemy.orm import sessionmaker -from linkml.utils.schema_builder import SchemaBuilder from linkml.utils.schema_fixer import SchemaFixer from linkml.utils.sqlutils import SQLStore from linkml_runtime.dumpers import yaml_dumper from linkml_runtime.linkml_model import SlotDefinition from linkml_runtime.loaders import csv_loader, yaml_loader +from linkml_runtime.utils.schema_builder import SchemaBuilder from tests.linkml.utils.dict_comparator import compare_objs, compare_yaml diff --git a/tests/linkml/test_generators/test_owlgen.py b/tests/linkml/test_generators/test_owlgen.py index 0628647210..717295eb60 100644 --- a/tests/linkml/test_generators/test_owlgen.py +++ b/tests/linkml/test_generators/test_owlgen.py @@ -6,9 +6,9 @@ from rdflib.namespace import OWL, RDF from linkml.generators.owlgen import MetadataProfile, OwlSchemaGenerator -from linkml.utils.schema_builder import SchemaBuilder from linkml_runtime.linkml_model import SlotDefinition from linkml_runtime.linkml_model.meta import PermissibleValue +from linkml_runtime.utils.schema_builder import SchemaBuilder SYMP = Namespace("http://purl.obolibrary.org/obo/SYMP_") KS = Namespace("https://w3id.org/linkml/tests/kitchen_sink/") diff --git a/tests/linkml/test_generators/test_pydanticgen.py b/tests/linkml/test_generators/test_pydanticgen.py index 3cecb51e69..7e78b9bc92 100644 --- a/tests/linkml/test_generators/test_pydanticgen.py +++ b/tests/linkml/test_generators/test_pydanticgen.py @@ -40,12 +40,12 @@ PydanticValidator, ) from linkml.utils.exceptions import ValidationError as ArrayValidationError -from linkml.utils.schema_builder import SchemaBuilder from linkml_runtime import SchemaView from linkml_runtime.dumpers import yaml_dumper from linkml_runtime.linkml_model import ClassDefinition, Definition, SchemaDefinition, SlotDefinition from linkml_runtime.utils.compile_python import compile_python from linkml_runtime.utils.formatutils import camelcase, remove_empty_items, underscore +from linkml_runtime.utils.schema_builder import SchemaBuilder from linkml_runtime.utils.schemaview import load_schema_wrap from .conftest import MyInjectedClass diff --git a/tests/linkml/test_generators/test_sqlalchemygen.py b/tests/linkml/test_generators/test_sqlalchemygen.py index 070cc52695..f64d1ed759 100644 --- a/tests/linkml/test_generators/test_sqlalchemygen.py +++ b/tests/linkml/test_generators/test_sqlalchemygen.py @@ -10,8 +10,8 @@ from linkml.generators.sqlalchemygen import SQLAlchemyGenerator, TemplateEnum, cli from linkml.generators.sqltablegen import RANGEMAP, SQL_TYPE_TO_PYTHON_TYPE, SQLTableGenerator -from linkml.utils.schema_builder import SchemaBuilder from linkml_runtime.linkml_model import SlotDefinition +from linkml_runtime.utils.schema_builder import SchemaBuilder logger = logging.getLogger(__name__) diff --git a/tests/linkml/test_generators/test_sqltablegen.py b/tests/linkml/test_generators/test_sqltablegen.py index b8f6be7f54..dcf7253062 100644 --- a/tests/linkml/test_generators/test_sqltablegen.py +++ b/tests/linkml/test_generators/test_sqltablegen.py @@ -7,9 +7,9 @@ from sqlalchemy.sql.sqltypes import Boolean, Date, DateTime, Enum, Float, Integer, Numeric, Text, Time from linkml.generators.sqltablegen import ORACLE_MAX_VARCHAR_LENGTH, SQLTableGenerator, cli -from linkml.utils.schema_builder import SchemaBuilder from linkml_runtime.linkml_model.meta import Annotation, SlotDefinition, UniqueKey from linkml_runtime.utils.introspection import package_schemaview +from linkml_runtime.utils.schema_builder import SchemaBuilder from linkml_runtime.utils.schemaview import SchemaView # from tests.linkml.test_generators.environment import env diff --git a/tests/linkml/test_generators/test_sqlvalidationgen.py b/tests/linkml/test_generators/test_sqlvalidationgen.py index 3f1eb87645..78e49106a2 100644 --- a/tests/linkml/test_generators/test_sqlvalidationgen.py +++ b/tests/linkml/test_generators/test_sqlvalidationgen.py @@ -8,8 +8,8 @@ from linkml.generators.sqltablegen import SQLTableGenerator from linkml.generators.sqlvalidationgen import SQLValidationGenerator, cli -from linkml.utils.schema_builder import SchemaBuilder from linkml_runtime.linkml_model.meta import SlotDefinition, UniqueKey +from linkml_runtime.utils.schema_builder import SchemaBuilder @pytest.fixture diff --git a/tests/linkml/test_generators/test_typescriptgen.py b/tests/linkml/test_generators/test_typescriptgen.py index bd9a915c1e..20867a3726 100644 --- a/tests/linkml/test_generators/test_typescriptgen.py +++ b/tests/linkml/test_generators/test_typescriptgen.py @@ -3,9 +3,9 @@ from click.testing import CliRunner from linkml.generators.typescriptgen import TypescriptGenerator, cli -from linkml.utils.schema_builder import SchemaBuilder from linkml_runtime import SchemaView from linkml_runtime.linkml_model import SlotDefinition +from linkml_runtime.utils.schema_builder import SchemaBuilder def test_tsgen(kitchen_sink_path): diff --git a/tests/linkml/test_issues/test_linkml_issue_872.py b/tests/linkml/test_issues/test_linkml_issue_872.py index ddc38d7d36..7a9804d597 100644 --- a/tests/linkml/test_issues/test_linkml_issue_872.py +++ b/tests/linkml/test_issues/test_linkml_issue_872.py @@ -1,7 +1,7 @@ from linkml.generators.jsonldgen import JSONLDGenerator -from linkml.utils.schema_builder import SchemaBuilder from linkml_runtime.dumpers import yaml_dumper from linkml_runtime.linkml_model import SlotDefinition +from linkml_runtime.utils.schema_builder import SchemaBuilder def test_monotonic(): diff --git a/tests/linkml/test_linter/test_linter.py b/tests/linkml/test_linter/test_linter.py index 5626f76f50..339dce8a1f 100644 --- a/tests/linkml/test_linter/test_linter.py +++ b/tests/linkml/test_linter/test_linter.py @@ -2,7 +2,7 @@ from linkml.linter.config.datamodel.config import RuleLevel from linkml.linter.linter import Linter -from linkml.utils.schema_builder import SchemaBuilder +from linkml_runtime.utils.schema_builder import SchemaBuilder def test_rule_level_error(): diff --git a/tests/linkml/test_linter/test_rule_canonical_prefixes.py b/tests/linkml/test_linter/test_rule_canonical_prefixes.py index b96201503a..fdda8af35b 100644 --- a/tests/linkml/test_linter/test_rule_canonical_prefixes.py +++ b/tests/linkml/test_linter/test_rule_canonical_prefixes.py @@ -2,8 +2,8 @@ from linkml.linter.config.datamodel.config import CanonicalPrefixesConfig, RuleLevel from linkml.linter.rules import CanonicalPrefixesRule -from linkml.utils.schema_builder import SchemaBuilder from linkml_runtime import SchemaView +from linkml_runtime.utils.schema_builder import SchemaBuilder pytestmark = pytest.mark.xdist_group("linter_cli") diff --git a/tests/linkml/test_linter/test_rule_no_empty_title.py b/tests/linkml/test_linter/test_rule_no_empty_title.py index e81cfb7373..85a76328a1 100644 --- a/tests/linkml/test_linter/test_rule_no_empty_title.py +++ b/tests/linkml/test_linter/test_rule_no_empty_title.py @@ -1,7 +1,7 @@ from linkml.linter.config.datamodel.config import NoEmptyTitleConfig, RuleConfig, RuleLevel from linkml.linter.rules import NoEmptyTitleRule -from linkml.utils.schema_builder import SchemaBuilder from linkml_runtime import SchemaView +from linkml_runtime.utils.schema_builder import SchemaBuilder def test_elements_with_empty_title(): diff --git a/tests/linkml/test_linter/test_rule_no_invalid_slot_usage.py b/tests/linkml/test_linter/test_rule_no_invalid_slot_usage.py index ab25d7cceb..54d9c323bd 100644 --- a/tests/linkml/test_linter/test_rule_no_invalid_slot_usage.py +++ b/tests/linkml/test_linter/test_rule_no_invalid_slot_usage.py @@ -1,8 +1,8 @@ from linkml.linter.config.datamodel.config import RuleConfig, RuleLevel from linkml.linter.rules import NoInvalidSlotUsageRule -from linkml.utils.schema_builder import SchemaBuilder from linkml_runtime import SchemaView from linkml_runtime.linkml_model import ClassDefinition +from linkml_runtime.utils.schema_builder import SchemaBuilder def test_invalid_slot_usage(): diff --git a/tests/linkml/test_linter/test_rule_no_xsd_int_type.py b/tests/linkml/test_linter/test_rule_no_xsd_int_type.py index 343f213c93..d9fe8e4bad 100644 --- a/tests/linkml/test_linter/test_rule_no_xsd_int_type.py +++ b/tests/linkml/test_linter/test_rule_no_xsd_int_type.py @@ -1,7 +1,7 @@ from linkml.linter.config.datamodel.config import RuleConfig, RuleLevel from linkml.linter.rules import NoXsdIntTypeRule -from linkml.utils.schema_builder import SchemaBuilder from linkml_runtime import SchemaView +from linkml_runtime.utils.schema_builder import SchemaBuilder def test_xsd_int_type_no_fix(): diff --git a/tests/linkml/test_linter/test_rule_permissible_values_format.py b/tests/linkml/test_linter/test_rule_permissible_values_format.py index e17f16f40f..35244c0066 100644 --- a/tests/linkml/test_linter/test_rule_permissible_values_format.py +++ b/tests/linkml/test_linter/test_rule_permissible_values_format.py @@ -2,8 +2,8 @@ from linkml.linter.config.datamodel.config import PermissibleValuesFormatRuleConfig, RuleLevel from linkml.linter.rules import PermissibleValuesFormatRule -from linkml.utils.schema_builder import SchemaBuilder from linkml_runtime import SchemaView +from linkml_runtime.utils.schema_builder import SchemaBuilder @pytest.fixture diff --git a/tests/linkml/test_linter/test_rule_recommended.py b/tests/linkml/test_linter/test_rule_recommended.py index 382189a372..791b94b5bc 100644 --- a/tests/linkml/test_linter/test_rule_recommended.py +++ b/tests/linkml/test_linter/test_rule_recommended.py @@ -1,8 +1,8 @@ from linkml.linter.config.datamodel.config import RecommendedRuleConfig, RuleLevel from linkml.linter.rules import RecommendedRule -from linkml.utils.schema_builder import SchemaBuilder from linkml_runtime import SchemaView from linkml_runtime.linkml_model import ClassDefinition, EnumDefinition, SlotDefinition +from linkml_runtime.utils.schema_builder import SchemaBuilder def test_missing_descriptions(): diff --git a/tests/linkml/test_linter/test_rule_standard_naming.py b/tests/linkml/test_linter/test_rule_standard_naming.py index 5cb89beb22..ed1d905d1d 100644 --- a/tests/linkml/test_linter/test_rule_standard_naming.py +++ b/tests/linkml/test_linter/test_rule_standard_naming.py @@ -2,8 +2,8 @@ from linkml.linter.config.datamodel.config import RuleLevel, StandardNamingConfig from linkml.linter.rules import StandardNamingRule -from linkml.utils.schema_builder import SchemaBuilder from linkml_runtime import SchemaView +from linkml_runtime.utils.schema_builder import SchemaBuilder @pytest.fixture diff --git a/tests/linkml/test_linter/test_rule_tree_root_class.py b/tests/linkml/test_linter/test_rule_tree_root_class.py index 05a4ab8a24..37f4bd5502 100644 --- a/tests/linkml/test_linter/test_rule_tree_root_class.py +++ b/tests/linkml/test_linter/test_rule_tree_root_class.py @@ -2,9 +2,9 @@ from linkml.linter.config.datamodel.config import RuleLevel, TreeRootClassRuleConfig from linkml.linter.rules import TreeRootClassRule -from linkml.utils.schema_builder import SchemaBuilder from linkml_runtime import SchemaView from linkml_runtime.linkml_model import ClassDefinition, SlotDefinition +from linkml_runtime.utils.schema_builder import SchemaBuilder MY_CLASS = "MyClass" MY_ENUM = "MyEnum" diff --git a/tests/linkml/test_transformers/test_logical_model_transformer.py b/tests/linkml/test_transformers/test_logical_model_transformer.py index 5750dec776..ba5eab851b 100644 --- a/tests/linkml/test_transformers/test_logical_model_transformer.py +++ b/tests/linkml/test_transformers/test_logical_model_transformer.py @@ -6,10 +6,10 @@ LogicalModelTransformer, UnsatisfiableAttribute, ) -from linkml.utils.schema_builder import SchemaBuilder from linkml_runtime import SchemaView from linkml_runtime.dumpers import yaml_dumper from linkml_runtime.linkml_model import SchemaDefinition, SlotDefinition +from linkml_runtime.utils.schema_builder import SchemaBuilder THIS_DIR = Path(__file__).parent OUTPUT_DIR = THIS_DIR / "output" diff --git a/tests/linkml/test_transformers/test_relmodel_transformer.py b/tests/linkml/test_transformers/test_relmodel_transformer.py index 02640127b1..f3ac66b1c7 100644 --- a/tests/linkml/test_transformers/test_relmodel_transformer.py +++ b/tests/linkml/test_transformers/test_relmodel_transformer.py @@ -1,6 +1,6 @@ from linkml.transformers.relmodel_transformer import RelationalModelTransformer -from linkml.utils.schema_builder import SchemaBuilder from linkml_runtime import SchemaView +from linkml_runtime.utils.schema_builder import SchemaBuilder def test_nested_key(): diff --git a/tests/linkml/test_transformers/test_rollup_tranformer.py b/tests/linkml/test_transformers/test_rollup_tranformer.py index 3fa9cced58..fab4c7e8ab 100644 --- a/tests/linkml/test_transformers/test_rollup_tranformer.py +++ b/tests/linkml/test_transformers/test_rollup_tranformer.py @@ -8,7 +8,7 @@ FlattenTransformerConfiguration, RollupTransformer, ) -from linkml.utils.schema_builder import SchemaBuilder +from linkml_runtime.utils.schema_builder import SchemaBuilder THIS_DIR = Path(__file__).parent OUTPUT_DIR = THIS_DIR / "output" diff --git a/tests/linkml/test_transformers/test_sqltransform.py b/tests/linkml/test_transformers/test_sqltransform.py index 7b96eec88b..0af706b940 100644 --- a/tests/linkml/test_transformers/test_sqltransform.py +++ b/tests/linkml/test_transformers/test_sqltransform.py @@ -18,9 +18,9 @@ get_foreign_key_map, get_primary_key_attributes, ) -from linkml.utils.schema_builder import SchemaBuilder from linkml_runtime.linkml_model import SlotDefinition from linkml_runtime.utils.introspection import package_schemaview +from linkml_runtime.utils.schema_builder import SchemaBuilder from linkml_runtime.utils.schemaview import SchemaView DUMMY_CLASS = "c" diff --git a/tests/linkml/test_utils/test_helpers.py b/tests/linkml/test_utils/test_helpers.py index 0ab618c863..95051dd3d6 100644 --- a/tests/linkml/test_utils/test_helpers.py +++ b/tests/linkml/test_utils/test_helpers.py @@ -1,6 +1,6 @@ from linkml.utils.helpers import is_simple_dict -from linkml.utils.schema_builder import SchemaBuilder from linkml_runtime.linkml_model import ClassDefinition, SchemaDefinition, SlotDefinition +from linkml_runtime.utils.schema_builder import SchemaBuilder from linkml_runtime.utils.schemaview import SchemaView SCHEMA = SchemaDefinition( diff --git a/tests/linkml/test_utils/test_schema_builder.py b/tests/linkml/test_utils/test_schema_builder.py index 9d901deb57..6985780ecd 100644 --- a/tests/linkml/test_utils/test_schema_builder.py +++ b/tests/linkml/test_utils/test_schema_builder.py @@ -1,57 +1,16 @@ -import pytest +from linkml_runtime.utils.schema_builder import SchemaBuilder -from linkml.utils.schema_builder import SchemaBuilder -MY_CLASS = "MyClass" -MY_ENUM = "MyEnum" -FULL_NAME = "full name" -DESC = "description" -AGE = "age" -LIVING = "Living" -DEAD = "Dead" - - -def test_build_schema(): +def test_deprecated_import_from_linkml(): """ - test a minimal schema with no primary names declared + Importing SchemaBuilder from linkml.utils.schema_builder should trigger + the deprecation system and re-export the same class from linkml_runtime. """ - b = SchemaBuilder("my-schema") - slots = [FULL_NAME, DESC] - b.add_class(MY_CLASS, slots, description="A test class") - b.add_enum(MY_ENUM, [LIVING, DEAD]) - s = b.schema - assert s.name == "my-schema" - c = s.classes[MY_CLASS] - e = s.enums[MY_ENUM] - assert c.name == MY_CLASS - assert c.description == "A test class" - assert sorted(slots) == sorted(c.slots) - assert e.name == MY_ENUM - assert [DEAD, LIVING] == sorted(e.permissible_values) - b.add_type("MyType", typeof="string", description="A test type") - assert s.types["MyType"].description == "A test type" - d = b.as_dict() - assert d["classes"][MY_CLASS]["slots"] == [FULL_NAME, DESC] - # no defaults by default - assert [] == s.imports - # defaults added - b.add_defaults() - assert ["linkml:types"] == s.imports - d = b.as_dict() + import linkml.utils.deprecation as dep_mod + import linkml.utils.schema_builder as mod + # Verify the deprecation was registered during module import + assert "schema-builder-import-location" in dep_mod.EMITTED -def test_slot_overrides(): - """ - tests for edge cases involving overrides - """ - b = SchemaBuilder() - b.add_slot(AGE, range="integer") - assert b.schema.slots[AGE].range == "integer" - b.add_class(MY_CLASS, [AGE]) - # add_class will (a) add the slot name to the list of applicable slots - # (b) add a slot definition to the top level slot definitions - # Note that (b) should only happen if the slot is not already present - assert b.schema.slots[AGE].range == "integer" - with pytest.raises(ValueError): - b.add_slot(AGE, range="string") - b.add_slot(AGE, range="string", replace_if_present=True) + # Verify the re-exported class is the canonical one from linkml_runtime + assert mod.SchemaBuilder is SchemaBuilder diff --git a/tests/linkml/test_utils/test_schema_fixer.py b/tests/linkml/test_utils/test_schema_fixer.py index a6958e9599..ff8f64bd6d 100644 --- a/tests/linkml/test_utils/test_schema_fixer.py +++ b/tests/linkml/test_utils/test_schema_fixer.py @@ -6,9 +6,9 @@ import pytest -from linkml.utils.schema_builder import SchemaBuilder from linkml.utils.schema_fixer import SchemaFixer from linkml_runtime.linkml_model import SlotDefinition, SlotDefinitionName +from linkml_runtime.utils.schema_builder import SchemaBuilder MY_CLASS = "MyClass" MY_CLASS2 = "MyClass2" diff --git a/tests/linkml_runtime/test_utils/test_schema_builder.py b/tests/linkml_runtime/test_utils/test_schema_builder.py index cfe4609fcd..8ac2417a16 100644 --- a/tests/linkml_runtime/test_utils/test_schema_builder.py +++ b/tests/linkml_runtime/test_utils/test_schema_builder.py @@ -150,6 +150,96 @@ def test_add_class_with_extra_kwargs( assert added_class == expected_added_class +# === Tests for `SchemaBuilder.add_class` with dict-based slots === + + +def test_add_class_with_dict_slots(): + """ + Test adding a class with slots specified as a dict of {name: {properties}}. + """ + builder = SchemaBuilder() + builder.add_class( + "Person", + slots={"age": {"range": "integer"}, "name": {"range": "string"}}, + ) + + assert sorted(builder.schema.classes["Person"].slots) == ["age", "name"] + assert builder.schema.slots["age"].range == "integer" + assert builder.schema.slots["name"].range == "string" + + +def test_add_class_with_dict_slots_and_kwargs(): + """ + Test adding a class with dict-based slots and extra kwargs. + """ + builder = SchemaBuilder() + builder.add_class( + "Person", + slots={"age": {"range": "integer"}}, + is_a="Thing", + ) + + assert builder.schema.classes["Person"].is_a == "Thing" + assert builder.schema.slots["age"].range == "integer" + + +# === Tests for `SchemaBuilder.add_class` with dict-of-dicts slot_usage === + + +def test_add_class_with_dict_slot_usage(): + """ + Test adding a class with slot_usage specified as a dict where values are dicts. + """ + builder = SchemaBuilder() + builder.add_class("Thing", slots=["name", "age"]) + builder.add_class( + ClassDefinition(name="Person", is_a="Thing"), + slot_usage={"age": {"minimum_value": 18}}, + ) + + slot_usage = builder.schema.classes["Person"].slot_usage + assert "age" in slot_usage + assert isinstance(slot_usage["age"], SlotDefinition) + assert slot_usage["age"].minimum_value == 18 + + +def test_add_class_with_list_slot_usage(): + """ + Test adding a class with slot_usage specified as a list of SlotDefinitions. + """ + builder = SchemaBuilder() + builder.add_class("Thing", slots=["name", "age"]) + builder.add_class( + ClassDefinition(name="Person", is_a="Thing"), + slot_usage=[SlotDefinition(name="age", minimum_value=18)], + ) + + slot_usage = builder.schema.classes["Person"].slot_usage + assert "age" in slot_usage + assert isinstance(slot_usage["age"], SlotDefinition) + assert slot_usage["age"].minimum_value == 18 + + +def test_add_class_with_mixed_slot_usage(): + """ + Test slot_usage with a mix of dict values and SlotDefinition values. + """ + builder = SchemaBuilder() + builder.add_class("Thing", slots=["name", "age"]) + builder.add_class( + ClassDefinition(name="Person", is_a="Thing"), + slot_usage={ + "age": {"minimum_value": 18}, + "name": SlotDefinition(name="name", range="string"), + }, + ) + + slot_usage = builder.schema.classes["Person"].slot_usage + assert slot_usage["age"].minimum_value == 18 + assert isinstance(slot_usage["age"], SlotDefinition) + assert slot_usage["name"].range == "string" + + # === Tests for `SchemaBuilder.add_class` end === @@ -263,3 +353,52 @@ def test_add_enum_with_extra_kwargs( # === Tests for `SchemaBuilder.add_enum` end === + + +# === General SchemaBuilder tests === + + +def test_build_schema(): + """ + Test a minimal schema with no primary names declared. + """ + b = SchemaBuilder("my-schema") + slots = ["full name", "description"] + b.add_class("MyClass", slots, description="A test class") + b.add_enum("MyEnum", ["Living", "Dead"]) + s = b.schema + assert s.name == "my-schema" + c = s.classes["MyClass"] + e = s.enums["MyEnum"] + assert c.name == "MyClass" + assert c.description == "A test class" + assert sorted(slots) == sorted(c.slots) + assert e.name == "MyEnum" + assert ["Dead", "Living"] == sorted(e.permissible_values) + b.add_type("MyType", typeof="string", description="A test type") + assert s.types["MyType"].description == "A test type" + d = b.as_dict() + assert d["classes"]["MyClass"]["slots"] == ["full name", "description"] + # no defaults by default + assert [] == s.imports + # defaults added + b.add_defaults() + assert ["linkml:types"] == s.imports + d = b.as_dict() + + +def test_slot_overrides(): + """ + Tests for edge cases involving overrides. + """ + b = SchemaBuilder() + b.add_slot("age", range="integer") + assert b.schema.slots["age"].range == "integer" + b.add_class("MyClass", ["age"]) + # add_class will (a) add the slot name to the list of applicable slots + # (b) add a slot definition to the top level slot definitions + # Note that (b) should only happen if the slot is not already present + assert b.schema.slots["age"].range == "integer" + with pytest.raises(ValueError): + b.add_slot("age", range="string") + b.add_slot("age", range="string", replace_if_present=True) From 667b725797877ef65512a703feed55aa04cc4ee6 Mon Sep 17 00:00:00 2001 From: Sujay Patil Date: Fri, 3 Apr 2026 11:53:03 -0700 Subject: [PATCH 11/19] add new line at the end of jinja files --- packages/linkml/src/linkml/generators/docgen/class.md.jinja2 | 2 +- packages/linkml/src/linkml/generators/docgen/enum.md.jinja2 | 2 +- packages/linkml/src/linkml/generators/docgen/slot.md.jinja2 | 2 +- packages/linkml/src/linkml/generators/docgen/subset.md.jinja2 | 2 +- packages/linkml/src/linkml/generators/docgen/type.md.jinja2 | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/linkml/src/linkml/generators/docgen/class.md.jinja2 b/packages/linkml/src/linkml/generators/docgen/class.md.jinja2 index 7eca2c7a44..d68301bd57 100644 --- a/packages/linkml/src/linkml/generators/docgen/class.md.jinja2 +++ b/packages/linkml/src/linkml/generators/docgen/class.md.jinja2 @@ -287,4 +287,4 @@ The class must not satisfy any of: {{ footer }} {%- endif -%} - \ No newline at end of file + diff --git a/packages/linkml/src/linkml/generators/docgen/enum.md.jinja2 b/packages/linkml/src/linkml/generators/docgen/enum.md.jinja2 index c2988f50e2..65dc990762 100644 --- a/packages/linkml/src/linkml/generators/docgen/enum.md.jinja2 +++ b/packages/linkml/src/linkml/generators/docgen/enum.md.jinja2 @@ -145,4 +145,4 @@ _This is a dynamic enum_ ``` - \ No newline at end of file + diff --git a/packages/linkml/src/linkml/generators/docgen/slot.md.jinja2 b/packages/linkml/src/linkml/generators/docgen/slot.md.jinja2 index 45ae3a2b2d..2d9d21caa1 100644 --- a/packages/linkml/src/linkml/generators/docgen/slot.md.jinja2 +++ b/packages/linkml/src/linkml/generators/docgen/slot.md.jinja2 @@ -460,4 +460,4 @@ Value must not satisfy any of: {{ footer }} {%- endif -%} - \ No newline at end of file + diff --git a/packages/linkml/src/linkml/generators/docgen/subset.md.jinja2 b/packages/linkml/src/linkml/generators/docgen/subset.md.jinja2 index 5e2ebf4821..0a05161d11 100644 --- a/packages/linkml/src/linkml/generators/docgen/subset.md.jinja2 +++ b/packages/linkml/src/linkml/generators/docgen/subset.md.jinja2 @@ -110,4 +110,4 @@ URI: {{ gen.link(element) }} {%- endif %} - \ No newline at end of file + diff --git a/packages/linkml/src/linkml/generators/docgen/type.md.jinja2 b/packages/linkml/src/linkml/generators/docgen/type.md.jinja2 index 66f49f770c..3b2fc44526 100644 --- a/packages/linkml/src/linkml/generators/docgen/type.md.jinja2 +++ b/packages/linkml/src/linkml/generators/docgen/type.md.jinja2 @@ -140,4 +140,4 @@ URI: {{ gen.uri_link(element) }} {% include "common_metadata.md.jinja2" %} - \ No newline at end of file + From bd83909f0834d96e02dea1c48fe000bc6369bf98 Mon Sep 17 00:00:00 2001 From: Charles Tapley Hoyt Date: Sat, 4 Apr 2026 21:18:04 +0200 Subject: [PATCH 12/19] Safer generation of OWL union, intersection, and none of (#3359) Closes #3358 --- .../linkml/src/linkml/generators/owlgen.py | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/linkml/src/linkml/generators/owlgen.py b/packages/linkml/src/linkml/generators/owlgen.py index 33c58b0ecd..e2034d5754 100644 --- a/packages/linkml/src/linkml/generators/owlgen.py +++ b/packages/linkml/src/linkml/generators/owlgen.py @@ -708,24 +708,25 @@ def transform_class_slot_expression( if slot.all_members: owl_exprs.append(self.transform_class_slot_expression(cls, slot.all_members, main_slot, owl_types)) - if slot.any_of: - owl_exprs.append( - self._union_of( - [self.transform_class_slot_expression(cls, x, main_slot, owl_types) for x in slot.any_of] - ) - ) - if slot.all_of: - owl_exprs.append( - self._intersection_of( - [self.transform_class_slot_expression(cls, x, main_slot, owl_types) for x in slot.all_of] - ) - ) - if slot.none_of: - owl_exprs.append( - self._complement_of_union_of( - [self.transform_class_slot_expression(cls, x, main_slot, owl_types) for x in slot.none_of] - ) - ) + def _get_slot_nodes(slot_definition) -> list[BNode | URIRef] | None: + if not slot_definition: + return None + rdflib_nodes = [ + rdflib_node + for slot_expression in slot.any_of + if (rdflib_node := self.transform_class_slot_expression(cls, slot_expression, main_slot, owl_types)) + ] + if rdflib_nodes: + return rdflib_nodes + return None + + if any_of_rdflib_nodes := _get_slot_nodes(slot.any_of): + owl_exprs.append(self._union_of(any_of_rdflib_nodes)) + if all_of_rdflib_nodes := _get_slot_nodes(slot.all_of): + owl_exprs.append(self._intersection_of(all_of_rdflib_nodes)) + if none_of_rdflib_nodes := _get_slot_nodes(slot.none_of): + owl_exprs.append(self._complement_of_union_of(none_of_rdflib_nodes)) + if slot.exactly_one_of: disj_exprs = [] for i, operand in enumerate(slot.exactly_one_of): From 8302fd13b06f78afd80e46ed11bf9ae73b64e609 Mon Sep 17 00:00:00 2001 From: matentzn <7070631+matentzn@users.noreply.github.com> Date: Wed, 8 Apr 2026 13:36:42 +0000 Subject: [PATCH 13/19] docs: update feature dashboard from compliance tests --- docs/generators/dashboard.md | 40 +++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/docs/generators/dashboard.md b/docs/generators/dashboard.md index ef44998444..2ba2ab4330 100644 --- a/docs/generators/dashboard.md +++ b/docs/generators/dashboard.md @@ -25,7 +25,7 @@ Each cell shows the aggregate result across all tests in that category. Scroll d | Arrays | ✅ | ❓ | ⚠️ | ⚠️ | ❓ | ⚠️ | ❓ | ⚠️ | ❓ | ⚠️ | ⚠️ | ⚠️ | | Boolean Expressions | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | | Cardinality & Presence | ⚠️ | ⚠️ | ✅ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | -| Core Structure | ⚠️ | ⚠️ | ✅ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | +| Core Structure | ⚠️ | ⚠️ | ⚠️ | ❓ | ⚠️ | ❓ | ⚠️ | ✅ | ⚠️ | ❓ | ⚠️ | ✅ | | Defaults & Computed | ✅ | ✅ | ⚠️ | ❓ | ✅ | ❓ | ❓ | ✅ | ⚠️ | ❓ | ✅ | ✅ | | Enumerations | ⚠️ | ⚠️ | ✅ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | | Identity & Keys | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ❓ | ⚠️ | @@ -37,6 +37,7 @@ Each cell shows the aggregate result across all tests in that category. Scroll d | Schema-Level | ✅ | ✅ | ✅ | ❓ | ⚠️ | ❓ | ❓ | ✅ | ✅ | ❓ | ⚠️ | ✅ | | Slot Typing & Ranges | ⚠️ | ⚠️ | ⚠️ | ❓ | ⚠️ | ⚠️ | ⚠️ | ✅ | ⚠️ | ❓ | ⚠️ | ⚠️ | | Value Constraints | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | +| Uncategorized | ⚠️ | ⚠️ | ✅ | ⚠️ | ⚠️ | ⚠️ | ❓ | ⚠️ | ⚠️ | ⚠️ | ❓ | ⚠️ | ## Coverage Scores @@ -44,18 +45,18 @@ Percentage of tests where the generator fully implements the feature (excluding | Generator | Implements | Partial | Ignores | N/A | Total | Score | |-----------|:----------:|:-------:|:-------:|:---:|:-----:|:-----:| -| Pydantic | 29 | 31 | 1 | 0 | 61 | 48% | -| Python DC | 16 | 36 | 9 | 0 | 61 | 26% | -| JSON Schema | 39 | 20 | 2 | 0 | 61 | 64% | -| Java | 0 | 22 | 39 | 0 | 61 | 0% | -| SHACL | 19 | 23 | 19 | 0 | 61 | 31% | -| ShEx | 0 | 23 | 38 | 0 | 61 | 0% | -| OWL | 0 | 25 | 36 | 0 | 61 | 0% | -| JSON-LD Ctx | 33 | 22 | 6 | 0 | 61 | 54% | -| SQLite DDL | 12 | 38 | 11 | 0 | 61 | 20% | -| Postgres DDL | 0 | 22 | 39 | 0 | 61 | 0% | -| Pandera | 14 | 27 | 20 | 0 | 61 | 23% | -| Polars Schema | 27 | 21 | 13 | 0 | 61 | 44% | +| Pydantic | 29 | 33 | 1 | 0 | 63 | 46% | +| Python DC | 16 | 37 | 10 | 0 | 63 | 25% | +| JSON Schema | 38 | 23 | 2 | 0 | 63 | 60% | +| Java | 0 | 23 | 40 | 0 | 63 | 0% | +| SHACL | 19 | 24 | 20 | 0 | 63 | 30% | +| ShEx | 0 | 23 | 40 | 0 | 63 | 0% | +| OWL | 0 | 25 | 38 | 0 | 63 | 0% | +| JSON-LD Ctx | 34 | 23 | 6 | 0 | 63 | 54% | +| SQLite DDL | 12 | 39 | 12 | 0 | 63 | 19% | +| Postgres DDL | 0 | 22 | 41 | 0 | 63 | 0% | +| Pandera | 14 | 27 | 22 | 0 | 63 | 22% | +| Polars Schema | 28 | 22 | 13 | 0 | 63 | 44% | ## Details by Category @@ -91,11 +92,11 @@ Percentage of tests where the generator fully implements the feature (excluding | Test | Pydantic | Python DC | JSON Schema | Java | SHACL | ShEx | OWL | JSON-LD Ctx | SQLite DDL | Postgres DDL | Pandera | Polars Schema | |------| :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | -| Abstract classes | ⚠️ | ⚠️ | ✅ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | +| Abstract classes | ⚠️ | ⚠️ | ⚠️ | ❓ | ⚠️ | ❓ | ⚠️ | ✅ | ⚠️ | ❓ | ⚠️ | ✅ | | Attribute refinement | ✅ | ✅ | ✅ | ❓ | ✅ | ❓ | ❓ | ✅ | ✅ | ❓ | ⚠️ | ✅ | | Attributes | ✅ | ✅ | ✅ | ❓ | ✅ | ❓ | ❓ | ✅ | ✅ | ❓ | ✅ | ✅ | -| Class inheritance (is_a) | ⚠️ | ⚠️ | ✅ | ❓ | ⚠️ | ❓ | ⚠️ | ✅ | ⚠️ | ❓ | ⚠️ | ✅ | -| Mixins | ⚠️ | ⚠️ | ✅ | ❓ | ⚠️ | ❓ | ⚠️ | ✅ | ⚠️ | ❓ | ⚠️ | ✅ | +| Class inheritance (is_a) | ⚠️ | ⚠️ | ⚠️ | ❓ | ⚠️ | ❓ | ⚠️ | ✅ | ⚠️ | ❓ | ⚠️ | ✅ | +| Mixins | ⚠️ | ⚠️ | ⚠️ | ❓ | ⚠️ | ❓ | ⚠️ | ✅ | ⚠️ | ❓ | ⚠️ | ✅ | | Slot inheritance | ✅ | ⚠️ | ✅ | ❓ | ⚠️ | ❓ | ❓ | ✅ | ⚠️ | ❓ | ⚠️ | ✅ | | Slot usage | ✅ | ⚠️ | ✅ | ❓ | ⚠️ | ❓ | ❓ | ✅ | ⚠️ | ❓ | ⚠️ | ✅ | @@ -195,6 +196,13 @@ Percentage of tests where the generator fully implements the feature (excluding | Min/max value | ✅ | ⚠️ | ✅ | ❓ | ✅ | ❓ | ❓ | ✅ | ⚠️ | ❓ | ✅ | ✅ | | Regex pattern | ⚠️ | ⚠️ | ⚠️ | ❓ | ✅ | ❓ | ❓ | ✅ | ⚠️ | ❓ | ⚠️ | ✅ | +### Uncategorized + +| Test | Pydantic | Python DC | JSON Schema | Java | SHACL | ShEx | OWL | JSON-LD Ctx | SQLite DDL | Postgres DDL | Pandera | Polars Schema | +|------| :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | +| range expression booleans | ⚠️ | ❓ | ✅ | ⚠️ | ❓ | ❓ | ❓ | ⚠️ | ❓ | ❓ | ❓ | ⚠️ | +| range expression nesting | ⚠️ | ⚠️ | ✅ | ⚠️ | ⚠️ | ⚠️ | ❓ | ⚠️ | ⚠️ | ⚠️ | ❓ | ⚠️ | + --- *This dashboard is auto-generated from compliance test results. To update: run the compliance tests with `--with-output`, then run `uv run python scripts/generate_dashboard.py`. To add features, write a new compliance test and decorate it with `@feature_category("Category Name", "Display Name")`.* From 7a99df91b80a38277de601c24f5bfa0f78647ce1 Mon Sep 17 00:00:00 2001 From: Cody Mitchell Date: Wed, 8 Apr 2026 09:09:34 -0500 Subject: [PATCH 14/19] test: reduce issue snapshot file count first pass (#3315) * test: reduce issue snapshot file count first pass * test: migrate four issue snapshot cases to target suites * test: fold migrated issue cases into target suites --- .../input/issue_121.yaml | 0 .../input/issue_121_imports.yaml | 0 .../input/linkml_issue_388.yaml | 0 .../test_generators/test_jsonschemagen.py | 16 + tests/linkml/test_generators/test_owlgen.py | 18 + .../linkml/test_generators/test_pythongen.py | 21 + tests/linkml/test_generators/test_rdfgen.py | 13 +- .../__snapshots__/curie_case.context.jsonld | 20 - .../test_issues/__snapshots__/curie_case.yaml | 35 - .../curie_prefix_matching.context.jsonld | 18 - .../__snapshots__/curie_prefix_matching.yaml | 60 - .../test_issues/__snapshots__/issue_121.py | 113 -- .../__snapshots__/issue_121_1.json | 3 - .../__snapshots__/issue_121_2.json | 1 - .../__snapshots__/issue_80.context.jsonld | 21 - .../test_issues/__snapshots__/issue_80.json | 5 - .../test_issues/__snapshots__/issue_80.py | 127 -- .../test_issues/__snapshots__/issue_80.ttl | 8 - .../linkml_issue_384-False-False.owl | 165 --- .../linkml_issue_384-True-True.owl | 178 --- .../linkml_issue_384.context.jsonld | 51 - .../__snapshots__/linkml_issue_384.other.txt | 1269 +++++++++++++++++ .../__snapshots__/linkml_issue_384.owl.txt | 368 +++++ .../__snapshots__/linkml_issue_384.py | 242 ---- .../__snapshots__/linkml_issue_384.ttl | 476 ------- .../__snapshots__/linkml_issue_384.yaml | 537 ------- .../__snapshots__/linkml_issue_388.owl | 66 - .../linkml_issue_388.schema.json | 60 - .../__snapshots__/linkml_issue_388.ttl | 105 -- .../__snapshots__/linkml_issue_388.yaml | 108 -- tests/linkml/test_issues/conftest.py | 34 + .../test_issues/test_curie_prefix_matching.py | 22 - tests/linkml/test_issues/test_issue_121.py | 32 - tests/linkml/test_issues/test_issue_80.py | 27 - .../test_issues/test_linkml_issue_384.py | 30 +- .../test_issues/test_linkml_issue_388.py | 43 - .../input/curie_case.yaml | 0 .../input/curie_prefix_matching.yaml | 0 tests/linkml/test_prefixes/test_prefixes.py | 31 + .../input/issue_80.yaml | 0 tests/linkml/test_utils/test_uri_and_curie.py | 34 +- 41 files changed, 1820 insertions(+), 2537 deletions(-) rename tests/linkml/{test_issues => test_generators}/input/issue_121.yaml (100%) rename tests/linkml/{test_issues => test_generators}/input/issue_121_imports.yaml (100%) rename tests/linkml/{test_issues => test_generators}/input/linkml_issue_388.yaml (100%) delete mode 100644 tests/linkml/test_issues/__snapshots__/curie_case.context.jsonld delete mode 100644 tests/linkml/test_issues/__snapshots__/curie_case.yaml delete mode 100644 tests/linkml/test_issues/__snapshots__/curie_prefix_matching.context.jsonld delete mode 100644 tests/linkml/test_issues/__snapshots__/curie_prefix_matching.yaml delete mode 100644 tests/linkml/test_issues/__snapshots__/issue_121.py delete mode 100644 tests/linkml/test_issues/__snapshots__/issue_121_1.json delete mode 100644 tests/linkml/test_issues/__snapshots__/issue_121_2.json delete mode 100644 tests/linkml/test_issues/__snapshots__/issue_80.context.jsonld delete mode 100644 tests/linkml/test_issues/__snapshots__/issue_80.json delete mode 100644 tests/linkml/test_issues/__snapshots__/issue_80.py delete mode 100644 tests/linkml/test_issues/__snapshots__/issue_80.ttl delete mode 100644 tests/linkml/test_issues/__snapshots__/linkml_issue_384-False-False.owl delete mode 100644 tests/linkml/test_issues/__snapshots__/linkml_issue_384-True-True.owl delete mode 100644 tests/linkml/test_issues/__snapshots__/linkml_issue_384.context.jsonld create mode 100644 tests/linkml/test_issues/__snapshots__/linkml_issue_384.other.txt create mode 100644 tests/linkml/test_issues/__snapshots__/linkml_issue_384.owl.txt delete mode 100644 tests/linkml/test_issues/__snapshots__/linkml_issue_384.py delete mode 100644 tests/linkml/test_issues/__snapshots__/linkml_issue_384.ttl delete mode 100644 tests/linkml/test_issues/__snapshots__/linkml_issue_384.yaml delete mode 100644 tests/linkml/test_issues/__snapshots__/linkml_issue_388.owl delete mode 100644 tests/linkml/test_issues/__snapshots__/linkml_issue_388.schema.json delete mode 100644 tests/linkml/test_issues/__snapshots__/linkml_issue_388.ttl delete mode 100644 tests/linkml/test_issues/__snapshots__/linkml_issue_388.yaml delete mode 100644 tests/linkml/test_issues/test_curie_prefix_matching.py delete mode 100644 tests/linkml/test_issues/test_issue_121.py delete mode 100644 tests/linkml/test_issues/test_issue_80.py delete mode 100644 tests/linkml/test_issues/test_linkml_issue_388.py rename tests/linkml/{test_issues => test_prefixes}/input/curie_case.yaml (100%) rename tests/linkml/{test_issues => test_prefixes}/input/curie_prefix_matching.yaml (100%) rename tests/linkml/{test_issues => test_utils}/input/issue_80.yaml (100%) diff --git a/tests/linkml/test_issues/input/issue_121.yaml b/tests/linkml/test_generators/input/issue_121.yaml similarity index 100% rename from tests/linkml/test_issues/input/issue_121.yaml rename to tests/linkml/test_generators/input/issue_121.yaml diff --git a/tests/linkml/test_issues/input/issue_121_imports.yaml b/tests/linkml/test_generators/input/issue_121_imports.yaml similarity index 100% rename from tests/linkml/test_issues/input/issue_121_imports.yaml rename to tests/linkml/test_generators/input/issue_121_imports.yaml diff --git a/tests/linkml/test_issues/input/linkml_issue_388.yaml b/tests/linkml/test_generators/input/linkml_issue_388.yaml similarity index 100% rename from tests/linkml/test_issues/input/linkml_issue_388.yaml rename to tests/linkml/test_generators/input/linkml_issue_388.yaml diff --git a/tests/linkml/test_generators/test_jsonschemagen.py b/tests/linkml/test_generators/test_jsonschemagen.py index ca1654771b..fd7b3a106f 100644 --- a/tests/linkml/test_generators/test_jsonschemagen.py +++ b/tests/linkml/test_generators/test_jsonschemagen.py @@ -8,6 +8,7 @@ import yaml from linkml.generators.jsonschemagen import JsonSchemaGenerator +from linkml.generators.yamlgen import YAMLGenerator from linkml_runtime import SchemaView from linkml_runtime.dumpers import json_dumper from linkml_runtime.linkml_model import ( @@ -25,6 +26,21 @@ pytestmark = pytest.mark.jsonschemagen +@pytest.mark.network +def test_issue_388_attribute_slot_uri_conflicts_stay_disambiguated_in_yaml_and_jsonschema(input_path): + """Ambiguous attribute URIs should keep minimal shared metadata.""" + schema = input_path("linkml_issue_388.yaml") + + generated_yaml = yaml.safe_load(YAMLGenerator(schema).serialize()) + assert set(generated_yaml["slots"]) == {"c1__a", "c2__a", "c3__a"} + assert generated_yaml["slots"]["c3__a"]["slot_uri"] == "other:a" + + generated_schema = json.loads(JsonSchemaGenerator(schema).serialize()) + assert generated_schema["$defs"]["C1"]["properties"]["a"]["type"] == ["string", "null"] + assert generated_schema["$defs"]["C2"]["properties"]["a"]["type"] == ["integer", "null"] + assert generated_schema["$defs"]["C3"]["properties"]["a"]["anyOf"][0]["$ref"] == "#/$defs/C1" + + def test_jsonschema_integration(kitchen_sink_path, input_path): """Integration test for JsonSchemaGenerator. diff --git a/tests/linkml/test_generators/test_owlgen.py b/tests/linkml/test_generators/test_owlgen.py index 717295eb60..7f18808bf2 100644 --- a/tests/linkml/test_generators/test_owlgen.py +++ b/tests/linkml/test_generators/test_owlgen.py @@ -5,6 +5,7 @@ from rdflib.collection import Collection from rdflib.namespace import OWL, RDF +from linkml import METAMODEL_CONTEXT_URI from linkml.generators.owlgen import MetadataProfile, OwlSchemaGenerator from linkml_runtime.linkml_model import SlotDefinition from linkml_runtime.linkml_model.meta import PermissibleValue @@ -102,6 +103,23 @@ def test_rdfs_profile(kitchen_sink_path): assert Literal("A person, living or dead") in g.objects(KS.Person, RDFS.comment) +@pytest.mark.network +def test_issue_388_attribute_slot_uri_conflicts_stay_disambiguated_in_owl(input_path): + """Ambiguous attribute URIs should keep the minimal shared OWL identity.""" + generated_owl = OwlSchemaGenerator( + input_path("linkml_issue_388.yaml"), + metaclasses=False, + skip_vacuous_min_zero_cardinality_axioms=False, + skip_vacuous_local_range_axioms=False, + consolidate_cardinality_axioms=False, + ).serialize(context=[METAMODEL_CONTEXT_URI]) + + owl_graph = Graph() + owl_graph.parse(data=generated_owl, format="turtle") + this_a = URIRef("https://example.org/this/a") + assert len(list(owl_graph.triples((this_a, None, None)))) == 1 + + @pytest.mark.parametrize( "description,metadata_profiles,use_native_uris,metaclasses,assert_equivalent_classes,expected", [ diff --git a/tests/linkml/test_generators/test_pythongen.py b/tests/linkml/test_generators/test_pythongen.py index 137d55e40a..3ab4cd99ba 100644 --- a/tests/linkml/test_generators/test_pythongen.py +++ b/tests/linkml/test_generators/test_pythongen.py @@ -1,7 +1,9 @@ +import json import re from types import ModuleType import pytest +from jsonasobj2 import as_json from linkml.generators.pythongen import PythonGenerator from linkml_runtime.loaders import json_loader @@ -119,6 +121,25 @@ def test_enum_ifabsent_snake_case_name(): assert greeting.mood.code == module.CordialityLevel.heartfelt +def test_issue_121_imported_type_is_emitted_once(input_path): + """Imported LinkML types should remain available in generated Python.""" + python = PythonGenerator(input_path("issue_121.yaml")).serialize() + + type_import_lines = [ + line for line in python.splitlines() if line.startswith("from linkml_runtime.linkml_model.types ") + ] + assert type_import_lines == ["from linkml_runtime.linkml_model.types import String"] + + module = compile_python(python) + + biosample = module.Biosample(depth="test") + assert biosample.depth == "test" + assert json.loads(as_json(biosample)) == {"depth": "test"} + + imported = module.ImportedClass() + assert json.loads(as_json(imported)) == {} + + def test_head(): """Validate the head/nohead parameter""" yaml = """id: "https://w3id.org/biolink/metamodel" diff --git a/tests/linkml/test_generators/test_rdfgen.py b/tests/linkml/test_generators/test_rdfgen.py index 8425f3e8a6..77d674d34f 100644 --- a/tests/linkml/test_generators/test_rdfgen.py +++ b/tests/linkml/test_generators/test_rdfgen.py @@ -1,7 +1,8 @@ import pytest import rdflib -from rdflib import Graph +from rdflib import Graph, URIRef +from linkml import METAMODEL_CONTEXT_URI from linkml.generators.rdfgen import RDFGenerator pytestmark = pytest.mark.xdist_group("rdfgen") @@ -79,6 +80,16 @@ def test_annotation_extensions(): assert isinstance(tag, rdflib.URIRef | rdflib.Literal) +@pytest.mark.network +def test_issue_388_attribute_slot_uri_conflicts_stay_disambiguated_in_rdf(input_path): + generated_rdf = RDFGenerator(input_path("linkml_issue_388.yaml")).serialize(context=[METAMODEL_CONTEXT_URI]) + rdf_graph = Graph() + rdf_graph.parse(data=generated_rdf, format="turtle") + + for slot in ("c1__a", "c2__a", "c3__a"): + assert len(list(rdf_graph.triples((URIRef(f"https://example.org/this/{slot}"), None, None)))) > 0 + + @pytest.mark.skip("TODO") def test_rdfgen(kitchen_sink_path): """rdf""" diff --git a/tests/linkml/test_issues/__snapshots__/curie_case.context.jsonld b/tests/linkml/test_issues/__snapshots__/curie_case.context.jsonld deleted file mode 100644 index f79c3dd23f..0000000000 --- a/tests/linkml/test_issues/__snapshots__/curie_case.context.jsonld +++ /dev/null @@ -1,20 +0,0 @@ -{ - "@context": { - "xsd": "http://www.w3.org/2001/XMLSchema#", - "ABC": "http://example.org/abc#", - "aBC": "http://example.org/abc#", - "aBc": "http://example.org/abc#", - "abc": "http://example.org/abc#", - "curiecase": "http://example.org/tests/curiecase/", - "@vocab": "http://example.org/tests/curiecase/", - "C1": { - "@id": "aBC:c1" - }, - "C2": { - "@id": "aBC:t2" - }, - "C3": { - "@id": "aBC:t3" - } - } -} diff --git a/tests/linkml/test_issues/__snapshots__/curie_case.yaml b/tests/linkml/test_issues/__snapshots__/curie_case.yaml deleted file mode 100644 index 81ebd5c14b..0000000000 --- a/tests/linkml/test_issues/__snapshots__/curie_case.yaml +++ /dev/null @@ -1,35 +0,0 @@ -name: curiecase -id: http://example.org/tests/curiecase -prefixes: - aBC: - prefix_prefix: aBC - prefix_reference: http://example.org/abc# -default_prefix: http://example.org/tests/curiecase/ -default_range: string -classes: - c1: - name: c1 - definition_uri: http://example.org/tests/curiecase/C1 - from_schema: http://example.org/tests/curiecase - mappings: - - abc:c1 - class_uri: abc:c1 - c2: - name: c2 - definition_uri: http://example.org/tests/curiecase/C2 - from_schema: http://example.org/tests/curiecase - mappings: - - ABC:t2 - class_uri: ABC:t2 - c3: - name: c3 - definition_uri: http://example.org/tests/curiecase/C3 - from_schema: http://example.org/tests/curiecase - mappings: - - aBc:t3 - class_uri: aBc:t3 -metamodel_version: 1.7.0 -source_file: curie_case.yaml -source_file_date: '2000-01-01T00:00:00' -source_file_size: 1 -generation_date: '2000-01-01T00:00:00' diff --git a/tests/linkml/test_issues/__snapshots__/curie_prefix_matching.context.jsonld b/tests/linkml/test_issues/__snapshots__/curie_prefix_matching.context.jsonld deleted file mode 100644 index e145a6a2ed..0000000000 --- a/tests/linkml/test_issues/__snapshots__/curie_prefix_matching.context.jsonld +++ /dev/null @@ -1,18 +0,0 @@ -{ - "@context": { - "xsd": "http://www.w3.org/2001/XMLSchema#", - "curietest": "http://example.org/tests/curietest/", - "p1": "http://example.org/prefixes/a/b/", - "p2": "http://example.org/prefixes/a/b/c/", - "@vocab": "http://example.org/tests/curietest/", - "C1": { - "@id": "p1:/c/c1" - }, - "C2": { - "@id": "p2:/c2" - }, - "C3": { - "@id": "p2:c3" - } - } -} diff --git a/tests/linkml/test_issues/__snapshots__/curie_prefix_matching.yaml b/tests/linkml/test_issues/__snapshots__/curie_prefix_matching.yaml deleted file mode 100644 index 566d185ee5..0000000000 --- a/tests/linkml/test_issues/__snapshots__/curie_prefix_matching.yaml +++ /dev/null @@ -1,60 +0,0 @@ -name: curietest -id: http://example.org/tests/curietest -prefixes: - p1: - prefix_prefix: p1 - prefix_reference: http://example.org/prefixes/a/b/ - p2: - prefix_prefix: p2 - prefix_reference: http://example.org/prefixes/a/b/c/ - xsd: - prefix_prefix: xsd - prefix_reference: http://www.w3.org/2001/XMLSchema# -default_prefix: http://example.org/tests/curietest/ -default_range: string -types: - t1: - name: t1 - definition_uri: http://example.org/tests/curietest/T1 - from_schema: http://example.org/tests/curietest - base: str - uri: p1:c/suffix1 - t2: - name: t2 - definition_uri: http://example.org/tests/curietest/T2 - from_schema: http://example.org/tests/curietest - base: str - uri: p2:suffix2 - t3: - name: t3 - definition_uri: http://example.org/tests/curietest/T3 - from_schema: http://example.org/tests/curietest - base: str - uri: http://example.org/prefixes/a/b/c/suffix3 -classes: - c1: - name: c1 - definition_uri: http://example.org/tests/curietest/C1 - from_schema: http://example.org/tests/curietest - mappings: - - p1:/c/c1 - class_uri: p1:/c/c1 - c2: - name: c2 - definition_uri: http://example.org/tests/curietest/C2 - from_schema: http://example.org/tests/curietest - mappings: - - p2:/c2 - class_uri: p2:/c2 - c3: - name: c3 - definition_uri: http://example.org/tests/curietest/C3 - from_schema: http://example.org/tests/curietest - mappings: - - http://example.org/prefixes/a/b/c/c3 - class_uri: http://example.org/prefixes/a/b/c/c3 -metamodel_version: 1.7.0 -source_file: curie_prefix_matching.yaml -source_file_date: '2000-01-01T00:00:00' -source_file_size: 1 -generation_date: '2000-01-01T00:00:00' diff --git a/tests/linkml/test_issues/__snapshots__/issue_121.py b/tests/linkml/test_issues/__snapshots__/issue_121.py deleted file mode 100644 index 5ae48d6f3a..0000000000 --- a/tests/linkml/test_issues/__snapshots__/issue_121.py +++ /dev/null @@ -1,113 +0,0 @@ -# Auto generated from issue_121.yaml by pythongen.py version: 0.0.1 -# Generation date: 2000-01-01T00:00:00 -# Schema: schema -# -# id: https://microbiomedata/schema -# description: -# license: https://creativecommons.org/publicdomain/zero/1.0/ - -import dataclasses -import re -from dataclasses import dataclass -from datetime import ( - date, - datetime, - time -) -from typing import ( - Any, - ClassVar, - Dict, - List, - Optional, - Union -) - -from jsonasobj2 import ( - JsonObj, - as_dict -) -from linkml_runtime.linkml_model.meta import ( - EnumDefinition, - PermissibleValue, - PvFormulaOptions -) -from linkml_runtime.utils.curienamespace import CurieNamespace -from linkml_runtime.utils.enumerations import EnumDefinitionImpl -from linkml_runtime.utils.formatutils import ( - camelcase, - sfx, - underscore -) -from linkml_runtime.utils.metamodelcore import ( - bnode, - empty_dict, - empty_list -) -from linkml_runtime.utils.slot import Slot -from linkml_runtime.utils.yamlutils import ( - YAMLRoot, - extended_float, - extended_int, - extended_str -) -from rdflib import ( - Namespace, - URIRef -) - -from linkml_runtime.linkml_model.types import String - -metamodel_version = "1.7.0" -version = None - -# Namespaces -LINKML = CurieNamespace('linkml', 'https://w3id.org/linkml/') -DEFAULT_ = CurieNamespace('', 'https://microbiomedata/schema/') - - -# Types - -# Class references - - - -@dataclass(repr=False) -class Biosample(YAMLRoot): - _inherited_slots: ClassVar[list[str]] = [] - - class_class_uri: ClassVar[URIRef] = URIRef("https://microbiomedata/schema/Biosample") - class_class_curie: ClassVar[str] = None - class_name: ClassVar[str] = "biosample" - class_model_uri: ClassVar[URIRef] = URIRef("https://microbiomedata/schema/Biosample") - - depth: Optional[str] = None - - def __post_init__(self, *_: str, **kwargs: Any): - if self.depth is not None and not isinstance(self.depth, str): - self.depth = str(self.depth) - - super().__post_init__(**kwargs) - - -class ImportedClass(YAMLRoot): - _inherited_slots: ClassVar[list[str]] = [] - - class_class_uri: ClassVar[URIRef] = URIRef("https://microbiomedata/schema/mixs/ImportedClass") - class_class_curie: ClassVar[str] = None - class_name: ClassVar[str] = "imported class" - class_model_uri: ClassVar[URIRef] = URIRef("https://microbiomedata/schema/ImportedClass") - - -# Enumerations - - -# Slots -class slots: - pass - -slots.depth = Slot(uri=DEFAULT_['mixs/depth'], name="depth", curie=DEFAULT_.curie('mixs/depth'), - model_uri=DEFAULT_.depth, domain=None, range=Optional[str]) - -slots.biosample_depth = Slot(uri=DEFAULT_['mixs/depth'], name="biosample_depth", curie=DEFAULT_.curie('mixs/depth'), - model_uri=DEFAULT_.biosample_depth, domain=Biosample, range=Optional[str]) diff --git a/tests/linkml/test_issues/__snapshots__/issue_121_1.json b/tests/linkml/test_issues/__snapshots__/issue_121_1.json deleted file mode 100644 index b2e90554c2..0000000000 --- a/tests/linkml/test_issues/__snapshots__/issue_121_1.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "depth": "test" -} \ No newline at end of file diff --git a/tests/linkml/test_issues/__snapshots__/issue_121_2.json b/tests/linkml/test_issues/__snapshots__/issue_121_2.json deleted file mode 100644 index 9e26dfeeb6..0000000000 --- a/tests/linkml/test_issues/__snapshots__/issue_121_2.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/tests/linkml/test_issues/__snapshots__/issue_80.context.jsonld b/tests/linkml/test_issues/__snapshots__/issue_80.context.jsonld deleted file mode 100644 index 41243f149a..0000000000 --- a/tests/linkml/test_issues/__snapshots__/issue_80.context.jsonld +++ /dev/null @@ -1,21 +0,0 @@ -{ - "@context": { - "xsd": "http://www.w3.org/2001/XMLSchema#", - "biolink": "https://w3id.org/biolink/vocab/", - "ex": "http://example.org/", - "linkml": "https://w3id.org/linkml/", - "model": "https://w3id.org/biolink/", - "@vocab": "https://w3id.org/biolink/vocab/", - "age": { - "@type": "xsd:integer", - "@id": "age" - }, - "id": "@id", - "name": { - "@id": "name" - }, - "Person": { - "@id": "ex:PERSON" - } - } -} diff --git a/tests/linkml/test_issues/__snapshots__/issue_80.json b/tests/linkml/test_issues/__snapshots__/issue_80.json deleted file mode 100644 index d1cc916f19..0000000000 --- a/tests/linkml/test_issues/__snapshots__/issue_80.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "http://example.org/person/17", - "name": "Fred Jones", - "age": 43 -} \ No newline at end of file diff --git a/tests/linkml/test_issues/__snapshots__/issue_80.py b/tests/linkml/test_issues/__snapshots__/issue_80.py deleted file mode 100644 index 944c09933c..0000000000 --- a/tests/linkml/test_issues/__snapshots__/issue_80.py +++ /dev/null @@ -1,127 +0,0 @@ -# Auto generated from issue_80.yaml by pythongen.py version: 0.0.1 -# Generation date: 2000-01-01T00:00:00 -# Schema: Issue_80_test_case -# -# id: http://example.org/issues/80 -# description: Example identifier -# license: https://creativecommons.org/publicdomain/zero/1.0/ - -import dataclasses -import re -from dataclasses import dataclass -from datetime import ( - date, - datetime, - time -) -from typing import ( - Any, - ClassVar, - Dict, - List, - Optional, - Union -) - -from jsonasobj2 import ( - JsonObj, - as_dict -) -from linkml_runtime.linkml_model.meta import ( - EnumDefinition, - PermissibleValue, - PvFormulaOptions -) -from linkml_runtime.utils.curienamespace import CurieNamespace -from linkml_runtime.utils.enumerations import EnumDefinitionImpl -from linkml_runtime.utils.formatutils import ( - camelcase, - sfx, - underscore -) -from linkml_runtime.utils.metamodelcore import ( - bnode, - empty_dict, - empty_list -) -from linkml_runtime.utils.slot import Slot -from linkml_runtime.utils.yamlutils import ( - YAMLRoot, - extended_float, - extended_int, - extended_str -) -from rdflib import ( - Namespace, - URIRef -) - -from linkml_runtime.linkml_model.types import Integer, Objectidentifier, String -from linkml_runtime.utils.metamodelcore import ElementIdentifier - -metamodel_version = "1.7.0" -version = None - -# Namespaces -BIOLINK = CurieNamespace('biolink', 'https://w3id.org/biolink/vocab/') -EX = CurieNamespace('ex', 'http://example.org/') -LINKML = CurieNamespace('linkml', 'https://w3id.org/linkml/') -MODEL = CurieNamespace('model', 'https://w3id.org/biolink/') -DEFAULT_ = BIOLINK - - -# Types - -# Class references -class PersonId(ElementIdentifier): - pass - - -@dataclass(repr=False) -class Person(YAMLRoot): - """ - A person, living or dead - """ - _inherited_slots: ClassVar[list[str]] = [] - - class_class_uri: ClassVar[URIRef] = EX["PERSON"] - class_class_curie: ClassVar[str] = "ex:PERSON" - class_name: ClassVar[str] = "person" - class_model_uri: ClassVar[URIRef] = BIOLINK.Person - - id: Union[str, PersonId] = None - name: str = None - age: Optional[int] = None - - def __post_init__(self, *_: str, **kwargs: Any): - if self._is_empty(self.id): - self.MissingRequiredField("id") - if not isinstance(self.id, PersonId): - self.id = PersonId(self.id) - - if self._is_empty(self.name): - self.MissingRequiredField("name") - if not isinstance(self.name, str): - self.name = str(self.name) - - if self.age is not None and not isinstance(self.age, int): - self.age = int(self.age) - - super().__post_init__(**kwargs) - - -# Enumerations - - -# Slots -class slots: - pass - -slots.id = Slot(uri=BIOLINK.id, name="id", curie=BIOLINK.curie('id'), - model_uri=BIOLINK.id, domain=None, range=URIRef) - -slots.name = Slot(uri=BIOLINK.name, name="name", curie=BIOLINK.curie('name'), - model_uri=BIOLINK.name, domain=None, range=str) - -slots.age = Slot(uri=BIOLINK.age, name="age", curie=BIOLINK.curie('age'), - model_uri=BIOLINK.age, domain=None, range=Optional[int]) diff --git a/tests/linkml/test_issues/__snapshots__/issue_80.ttl b/tests/linkml/test_issues/__snapshots__/issue_80.ttl deleted file mode 100644 index 908f67ca0b..0000000000 --- a/tests/linkml/test_issues/__snapshots__/issue_80.ttl +++ /dev/null @@ -1,8 +0,0 @@ -@prefix biolink: . -@prefix ex: . -@prefix xsd: . - - a ex:PERSON ; - biolink:age 43 ; - biolink:name "Fred Jones" . - diff --git a/tests/linkml/test_issues/__snapshots__/linkml_issue_384-False-False.owl b/tests/linkml/test_issues/__snapshots__/linkml_issue_384-False-False.owl deleted file mode 100644 index 90fe44863b..0000000000 --- a/tests/linkml/test_issues/__snapshots__/linkml_issue_384-False-False.owl +++ /dev/null @@ -1,165 +0,0 @@ -@prefix dcterms: . -@prefix ex: . -@prefix owl: . -@prefix rdfs: . -@prefix sdo: . -@prefix skos: . -@prefix xsd: . - -ex:GeoObject a owl:Class ; - rdfs:label "GeoObject" ; - rdfs:subClassOf [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:aliases ], - [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:age ], - [ a owl:Restriction ; - owl:maxCardinality 1 ; - owl:onProperty ex:age ], - [ a owl:Restriction ; - owl:allValuesFrom xsd:string ; - owl:onProperty ex:aliases ], - [ a owl:Restriction ; - owl:maxCardinality 1 ; - owl:onProperty ex:aliases ], - [ a owl:Restriction ; - owl:allValuesFrom ex:GeoAge ; - owl:onProperty ex:age ], - ex:Thing ; - skos:inScheme . - -ex:GeoAge a owl:Class ; - rdfs:label "GeoAge" ; - rdfs:subClassOf [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:unit ], - [ a owl:Restriction ; - owl:allValuesFrom xsd:float ; - owl:onProperty ex:value ], - [ a owl:Restriction ; - owl:maxCardinality 1 ; - owl:onProperty ex:value ], - [ a owl:Restriction ; - owl:maxCardinality 1 ; - owl:onProperty ex:unit ], - [ a owl:Restriction ; - owl:allValuesFrom xsd:string ; - owl:onProperty ex:unit ], - [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:value ] ; - skos:inScheme . - -ex:Organization a owl:Class ; - rdfs:label "Organization" ; - rdfs:subClassOf [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:parent ], - [ a owl:Restriction ; - owl:allValuesFrom ex:Organization ; - owl:onProperty ex:parent ], - [ a owl:Restriction ; - owl:maxCardinality 1 ; - owl:onProperty ex:full_name ], - [ a owl:Restriction ; - owl:allValuesFrom xsd:string ; - owl:onProperty ex:full_name ], - [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:full_name ], - ex:Thing ; - skos:inScheme . - -ex:Person a owl:Class ; - rdfs:label "Person" ; - rdfs:subClassOf [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:aliases ], - [ a owl:Restriction ; - owl:maxCardinality 1 ; - owl:onProperty ex:phone ], - [ a owl:Restriction ; - owl:allValuesFrom xsd:integer ; - owl:onProperty ex:age ], - [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:parent ], - [ a owl:Restriction ; - owl:allValuesFrom xsd:string ; - owl:onProperty ex:phone ], - [ a owl:Restriction ; - owl:maxCardinality 1 ; - owl:onProperty ex:age ], - [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:phone ], - [ a owl:Restriction ; - owl:allValuesFrom xsd:string ; - owl:onProperty ex:aliases ], - [ a owl:Restriction ; - owl:allValuesFrom ex:Person ; - owl:onProperty ex:parent ], - [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:age ], - ex:Thing ; - skos:exactMatch sdo:Person ; - skos:inScheme . - -ex:id a owl:DatatypeProperty ; - rdfs:label "id" ; - skos:inScheme . - -ex:phone a owl:DatatypeProperty ; - rdfs:label "phone" ; - skos:inScheme . - -ex:unit a owl:DatatypeProperty ; - rdfs:label "unit" ; - skos:inScheme . - -ex:value a owl:DatatypeProperty ; - rdfs:label "value" ; - skos:inScheme . - -ex:Thing a owl:Class ; - rdfs:label "Thing" ; - rdfs:subClassOf [ a owl:Restriction ; - owl:allValuesFrom xsd:string ; - owl:onProperty ex:id ], - [ a owl:Restriction ; - owl:maxCardinality 1 ; - owl:onProperty ex:full_name ], - [ a owl:Restriction ; - owl:maxCardinality 1 ; - owl:onProperty ex:id ], - [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:full_name ], - [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:id ], - [ a owl:Restriction ; - owl:allValuesFrom xsd:string ; - owl:onProperty ex:full_name ] ; - skos:inScheme . - -ex:parent a owl:ObjectProperty ; - rdfs:label "parent" ; - rdfs:range ex:Thing ; - skos:inScheme . - -ex:aliases a owl:DatatypeProperty . - -ex:age a owl:DatatypeProperty, - owl:ObjectProperty . - -ex:full_name a owl:DatatypeProperty ; - rdfs:label "full_name" ; - dcterms:title "full name" ; - skos:inScheme . - - a owl:Ontology ; - rdfs:label "personinfo" . - diff --git a/tests/linkml/test_issues/__snapshots__/linkml_issue_384-True-True.owl b/tests/linkml/test_issues/__snapshots__/linkml_issue_384-True-True.owl deleted file mode 100644 index f40ae79d39..0000000000 --- a/tests/linkml/test_issues/__snapshots__/linkml_issue_384-True-True.owl +++ /dev/null @@ -1,178 +0,0 @@ -@prefix dcterms: . -@prefix ex: . -@prefix linkml: . -@prefix owl: . -@prefix rdfs: . -@prefix sdo: . -@prefix skos: . -@prefix xsd: . - -ex:GeoObject a owl:Class, - linkml:ClassDefinition ; - rdfs:label "GeoObject" ; - rdfs:subClassOf [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:aliases ], - [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:age ], - [ a owl:Restriction ; - owl:maxCardinality 1 ; - owl:onProperty ex:aliases ], - [ a owl:Restriction ; - owl:maxCardinality 1 ; - owl:onProperty ex:age ], - [ a owl:Restriction ; - owl:allValuesFrom ex:GeoAge ; - owl:onProperty ex:age ], - [ a owl:Restriction ; - owl:allValuesFrom linkml:String ; - owl:onProperty ex:aliases ], - ex:Thing ; - skos:inScheme . - -ex:GeoAge a owl:Class, - linkml:ClassDefinition ; - rdfs:label "GeoAge" ; - rdfs:subClassOf [ a owl:Restriction ; - owl:maxCardinality 1 ; - owl:onProperty ex:value ], - [ a owl:Restriction ; - owl:allValuesFrom linkml:Float ; - owl:onProperty ex:value ], - [ a owl:Restriction ; - owl:maxCardinality 1 ; - owl:onProperty ex:unit ], - [ a owl:Restriction ; - owl:allValuesFrom linkml:String ; - owl:onProperty ex:unit ], - [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:unit ], - [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:value ] ; - skos:inScheme . - -ex:Organization a owl:Class, - linkml:ClassDefinition ; - rdfs:label "Organization" ; - rdfs:subClassOf [ a owl:Restriction ; - owl:allValuesFrom ex:Organization ; - owl:onProperty ex:parent ], - [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:full_name ], - [ a owl:Restriction ; - owl:allValuesFrom linkml:String ; - owl:onProperty ex:full_name ], - [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:parent ], - [ a owl:Restriction ; - owl:maxCardinality 1 ; - owl:onProperty ex:full_name ], - ex:Thing ; - skos:inScheme . - -ex:Person a owl:Class, - linkml:ClassDefinition ; - rdfs:label "Person" ; - rdfs:subClassOf [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:phone ], - [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:aliases ], - [ a owl:Restriction ; - owl:maxCardinality 1 ; - owl:onProperty ex:age ], - [ a owl:Restriction ; - owl:allValuesFrom linkml:Integer ; - owl:onProperty ex:age ], - [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:parent ], - [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:age ], - [ a owl:Restriction ; - owl:allValuesFrom linkml:String ; - owl:onProperty ex:phone ], - [ a owl:Restriction ; - owl:allValuesFrom ex:Person ; - owl:onProperty ex:parent ], - [ a owl:Restriction ; - owl:maxCardinality 1 ; - owl:onProperty ex:phone ], - [ a owl:Restriction ; - owl:allValuesFrom linkml:String ; - owl:onProperty ex:aliases ], - ex:Thing ; - skos:exactMatch sdo:Person ; - skos:inScheme . - -ex:id a owl:ObjectProperty, - linkml:SlotDefinition ; - rdfs:label "id" ; - skos:inScheme . - -ex:phone a owl:ObjectProperty, - linkml:SlotDefinition ; - rdfs:label "phone" ; - skos:inScheme . - -ex:unit a owl:ObjectProperty, - linkml:SlotDefinition ; - rdfs:label "unit" ; - skos:inScheme . - -ex:value a owl:ObjectProperty, - linkml:SlotDefinition ; - rdfs:label "value" ; - skos:inScheme . - -ex:Thing a owl:Class, - linkml:ClassDefinition ; - rdfs:label "Thing" ; - rdfs:subClassOf [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:full_name ], - [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty ex:id ], - [ a owl:Restriction ; - owl:maxCardinality 1 ; - owl:onProperty ex:id ], - [ a owl:Restriction ; - owl:maxCardinality 1 ; - owl:onProperty ex:full_name ], - [ a owl:Restriction ; - owl:allValuesFrom linkml:String ; - owl:onProperty ex:id ], - [ a owl:Restriction ; - owl:allValuesFrom linkml:String ; - owl:onProperty ex:full_name ] ; - skos:inScheme . - -ex:parent a owl:ObjectProperty, - linkml:SlotDefinition ; - rdfs:label "parent" ; - rdfs:range ex:Thing ; - skos:inScheme . - -ex:aliases a owl:ObjectProperty, - linkml:SlotDefinition . - -ex:age a owl:ObjectProperty, - linkml:SlotDefinition . - -ex:full_name a owl:ObjectProperty, - linkml:SlotDefinition ; - rdfs:label "full_name" ; - dcterms:title "full name" ; - skos:inScheme . - - a owl:Ontology ; - rdfs:label "personinfo" . - diff --git a/tests/linkml/test_issues/__snapshots__/linkml_issue_384.context.jsonld b/tests/linkml/test_issues/__snapshots__/linkml_issue_384.context.jsonld deleted file mode 100644 index 76f8be2347..0000000000 --- a/tests/linkml/test_issues/__snapshots__/linkml_issue_384.context.jsonld +++ /dev/null @@ -1,51 +0,0 @@ -{ - "@context": { - "xsd": "http://www.w3.org/2001/XMLSchema#", - "ex": "https://w3id.org/linkml/examples/personinfo/", - "linkml": "https://w3id.org/linkml/", - "sdo": "http://schema.org/", - "@vocab": "https://w3id.org/linkml/examples/personinfo/", - "full_name": { - "@id": "full_name" - }, - "unit": { - "@id": "unit" - }, - "value": { - "@type": "xsd:float", - "@id": "value" - }, - "age": { - "@type": "xsd:integer", - "@id": "age" - }, - "aliases": { - "@id": "aliases" - }, - "id": { - "@id": "id" - }, - "parent": { - "@type": "@id", - "@id": "parent" - }, - "phone": { - "@id": "phone" - }, - "GeoAge": { - "@id": "GeoAge" - }, - "GeoObject": { - "@id": "GeoObject" - }, - "Organization": { - "@id": "Organization" - }, - "Person": { - "@id": "sdo:Person" - }, - "Thing": { - "@id": "Thing" - } - } -} diff --git a/tests/linkml/test_issues/__snapshots__/linkml_issue_384.other.txt b/tests/linkml/test_issues/__snapshots__/linkml_issue_384.other.txt new file mode 100644 index 0000000000..8f024c84fc --- /dev/null +++ b/tests/linkml/test_issues/__snapshots__/linkml_issue_384.other.txt @@ -0,0 +1,1269 @@ +# --- linkml_issue_384.yaml --- +name: personinfo +id: https://w3id.org/linkml/examples/personinfo +imports: +- linkml:types +license: https://creativecommons.org/publicdomain/zero/1.0/ +prefixes: + linkml: + prefix_prefix: linkml + prefix_reference: https://w3id.org/linkml/ + sdo: + prefix_prefix: sdo + prefix_reference: http://schema.org/ + ex: + prefix_prefix: ex + prefix_reference: https://w3id.org/linkml/examples/personinfo/ +default_prefix: ex +default_range: string +types: + string: + name: string + definition_uri: https://w3id.org/linkml/String + description: A character string + notes: + - In RDF serializations, a slot with range of string is treated as a literal or + type xsd:string. If you are authoring schemas in LinkML YAML, the type is referenced + with the lower case "string". + from_schema: https://w3id.org/linkml/types + imported_from: linkml:types + exact_mappings: + - schema:Text + base: str + uri: xsd:string + integer: + name: integer + definition_uri: https://w3id.org/linkml/Integer + description: An integer + notes: + - If you are authoring schemas in LinkML YAML, the type is referenced with the + lower case "integer". + from_schema: https://w3id.org/linkml/types + imported_from: linkml:types + exact_mappings: + - schema:Integer + base: int + uri: xsd:integer + boolean: + name: boolean + definition_uri: https://w3id.org/linkml/Boolean + description: A binary (true or false) value + notes: + - If you are authoring schemas in LinkML YAML, the type is referenced with the + lower case "boolean". + from_schema: https://w3id.org/linkml/types + imported_from: linkml:types + exact_mappings: + - schema:Boolean + base: Bool + uri: xsd:boolean + repr: bool + float: + name: float + definition_uri: https://w3id.org/linkml/Float + description: A real number that conforms to the xsd:float specification + notes: + - If you are authoring schemas in LinkML YAML, the type is referenced with the + lower case "float". + from_schema: https://w3id.org/linkml/types + imported_from: linkml:types + exact_mappings: + - schema:Float + base: float + uri: xsd:float + double: + name: double + definition_uri: https://w3id.org/linkml/Double + description: A real number that conforms to the xsd:double specification + notes: + - If you are authoring schemas in LinkML YAML, the type is referenced with the + lower case "double". + from_schema: https://w3id.org/linkml/types + imported_from: linkml:types + close_mappings: + - schema:Float + base: float + uri: xsd:double + decimal: + name: decimal + definition_uri: https://w3id.org/linkml/Decimal + description: A real number with arbitrary precision that conforms to the xsd:decimal + specification + notes: + - If you are authoring schemas in LinkML YAML, the type is referenced with the + lower case "decimal". + from_schema: https://w3id.org/linkml/types + imported_from: linkml:types + broad_mappings: + - schema:Number + base: Decimal + uri: xsd:decimal + time: + name: time + definition_uri: https://w3id.org/linkml/Time + description: A time object represents a (local) time of day, independent of any + particular day + notes: + - URI is dateTime because OWL reasoners do not work with straight date or time + - If you are authoring schemas in LinkML YAML, the type is referenced with the + lower case "time". + from_schema: https://w3id.org/linkml/types + imported_from: linkml:types + exact_mappings: + - schema:Time + base: XSDTime + uri: xsd:time + repr: str + date: + name: date + definition_uri: https://w3id.org/linkml/Date + description: a date (year, month and day) in an idealized calendar + notes: + - URI is dateTime because OWL reasoners don't work with straight date or time + - If you are authoring schemas in LinkML YAML, the type is referenced with the + lower case "date". + from_schema: https://w3id.org/linkml/types + imported_from: linkml:types + exact_mappings: + - schema:Date + base: XSDDate + uri: xsd:date + repr: str + datetime: + name: datetime + definition_uri: https://w3id.org/linkml/Datetime + description: The combination of a date and time + notes: + - If you are authoring schemas in LinkML YAML, the type is referenced with the + lower case "datetime". + from_schema: https://w3id.org/linkml/types + imported_from: linkml:types + exact_mappings: + - schema:DateTime + base: XSDDateTime + uri: xsd:dateTime + repr: str + date_or_datetime: + name: date_or_datetime + definition_uri: https://w3id.org/linkml/DateOrDatetime + description: Either a date or a datetime + notes: + - If you are authoring schemas in LinkML YAML, the type is referenced with the + lower case "date_or_datetime". + from_schema: https://w3id.org/linkml/types + imported_from: linkml:types + base: str + uri: linkml:DateOrDatetime + repr: str + uriorcurie: + name: uriorcurie + definition_uri: https://w3id.org/linkml/Uriorcurie + description: a URI or a CURIE + notes: + - If you are authoring schemas in LinkML YAML, the type is referenced with the + lower case "uriorcurie". + from_schema: https://w3id.org/linkml/types + imported_from: linkml:types + base: URIorCURIE + uri: xsd:anyURI + repr: str + curie: + name: curie + definition_uri: https://w3id.org/linkml/Curie + conforms_to: https://www.w3.org/TR/curie/ + description: a compact URI + notes: + - If you are authoring schemas in LinkML YAML, the type is referenced with the + lower case "curie". + comments: + - in RDF serializations this MUST be expanded to a URI + - in non-RDF serializations MAY be serialized as the compact representation + from_schema: https://w3id.org/linkml/types + imported_from: linkml:types + base: Curie + uri: xsd:string + repr: str + uri: + name: uri + definition_uri: https://w3id.org/linkml/Uri + conforms_to: https://www.ietf.org/rfc/rfc3987.txt + description: a complete URI + notes: + - If you are authoring schemas in LinkML YAML, the type is referenced with the + lower case "uri". + comments: + - in RDF serializations a slot with range of uri is treated as a literal or type + xsd:anyURI unless it is an identifier or a reference to an identifier, in which + case it is translated directly to a node + from_schema: https://w3id.org/linkml/types + imported_from: linkml:types + close_mappings: + - schema:URL + base: URI + uri: xsd:anyURI + repr: str + ncname: + name: ncname + definition_uri: https://w3id.org/linkml/Ncname + description: Prefix part of CURIE + notes: + - If you are authoring schemas in LinkML YAML, the type is referenced with the + lower case "ncname". + from_schema: https://w3id.org/linkml/types + imported_from: linkml:types + base: NCName + uri: xsd:string + repr: str + objectidentifier: + name: objectidentifier + definition_uri: https://w3id.org/linkml/Objectidentifier + description: A URI or CURIE that represents an object in the model. + notes: + - If you are authoring schemas in LinkML YAML, the type is referenced with the + lower case "objectidentifier". + comments: + - Used for inheritance and type checking + from_schema: https://w3id.org/linkml/types + imported_from: linkml:types + base: ElementIdentifier + uri: shex:iri + repr: str + nodeidentifier: + name: nodeidentifier + definition_uri: https://w3id.org/linkml/Nodeidentifier + description: A URI, CURIE or BNODE that represents a node in a model. + notes: + - If you are authoring schemas in LinkML YAML, the type is referenced with the + lower case "nodeidentifier". + from_schema: https://w3id.org/linkml/types + imported_from: linkml:types + base: NodeIdentifier + uri: shex:nonLiteral + repr: str + jsonpointer: + name: jsonpointer + definition_uri: https://w3id.org/linkml/Jsonpointer + conforms_to: https://datatracker.ietf.org/doc/html/rfc6901 + description: A string encoding a JSON Pointer. The value of the string MUST conform + to JSON Point syntax and SHOULD dereference to a valid object within the current + instance document when encoded in tree form. + notes: + - If you are authoring schemas in LinkML YAML, the type is referenced with the + lower case "jsonpointer". + from_schema: https://w3id.org/linkml/types + imported_from: linkml:types + base: str + uri: xsd:string + repr: str + jsonpath: + name: jsonpath + definition_uri: https://w3id.org/linkml/Jsonpath + conforms_to: https://www.ietf.org/archive/id/draft-goessner-dispatch-jsonpath-00.html + description: A string encoding a JSON Path. The value of the string MUST conform + to JSON Point syntax and SHOULD dereference to zero or more valid objects within + the current instance document when encoded in tree form. + notes: + - If you are authoring schemas in LinkML YAML, the type is referenced with the + lower case "jsonpath". + from_schema: https://w3id.org/linkml/types + imported_from: linkml:types + base: str + uri: xsd:string + repr: str + sparqlpath: + name: sparqlpath + definition_uri: https://w3id.org/linkml/Sparqlpath + conforms_to: https://www.w3.org/TR/sparql11-query/#propertypaths + description: A string encoding a SPARQL Property Path. The value of the string + MUST conform to SPARQL syntax and SHOULD dereference to zero or more valid objects + within the current instance document when encoded as RDF. + notes: + - If you are authoring schemas in LinkML YAML, the type is referenced with the + lower case "sparqlpath". + from_schema: https://w3id.org/linkml/types + imported_from: linkml:types + base: str + uri: xsd:string + repr: str +slots: + id: + name: id + definition_uri: https://w3id.org/linkml/examples/personinfo/id + from_schema: https://w3id.org/linkml/examples/personinfo + slot_uri: ex:id + owner: Thing + domain_of: + - Thing + range: string + full_name: + name: full_name + definition_uri: https://w3id.org/linkml/examples/personinfo/full_name + title: full name + from_schema: https://w3id.org/linkml/examples/personinfo + slot_uri: ex:full_name + owner: Thing + domain_of: + - Thing + range: string + parent: + name: parent + definition_uri: https://w3id.org/linkml/examples/personinfo/parent + from_schema: https://w3id.org/linkml/examples/personinfo + slot_uri: ex:parent + range: Thing + multivalued: true + inlined: true + inlined_as_list: true + person__aliases: + name: person__aliases + from_schema: https://w3id.org/linkml/examples/personinfo + slot_uri: ex:aliases + alias: aliases + owner: Person + domain_of: + - Person + range: string + multivalued: true + person__phone: + name: person__phone + from_schema: https://w3id.org/linkml/examples/personinfo + slot_uri: ex:phone + alias: phone + owner: Person + domain_of: + - Person + range: string + person__age: + name: person__age + description: age in years + from_schema: https://w3id.org/linkml/examples/personinfo + slot_uri: ex:age + alias: age + owner: Person + domain_of: + - Person + range: integer + geoObject__aliases: + name: geoObject__aliases + comments: + - we introduce a deliberate conflict (single vs multivalied) with the aliases + attribute that is local to person + from_schema: https://w3id.org/linkml/examples/personinfo + slot_uri: ex:aliases + alias: aliases + owner: GeoObject + domain_of: + - GeoObject + range: string + multivalued: false + geoObject__age: + name: geoObject__age + comments: + - we introduce a deliberate conflict (type vs class range) with the age attribute + that is local to person + from_schema: https://w3id.org/linkml/examples/personinfo + slot_uri: ex:age + alias: age + owner: GeoObject + domain_of: + - GeoObject + range: GeoAge + inlined: true + inlined_as_list: true + geoAge__unit: + name: geoAge__unit + from_schema: https://w3id.org/linkml/examples/personinfo + slot_uri: ex:unit + alias: unit + owner: GeoAge + domain_of: + - GeoAge + range: string + geoAge__value: + name: geoAge__value + from_schema: https://w3id.org/linkml/examples/personinfo + slot_uri: ex:value + alias: value + owner: GeoAge + domain_of: + - GeoAge + range: float + Person_parent: + name: Person_parent + definition_uri: https://w3id.org/linkml/examples/personinfo/parent + from_schema: https://w3id.org/linkml/examples/personinfo + is_a: parent + domain: Person + slot_uri: ex:parent + alias: parent + owner: Person + domain_of: + - Person + is_usage_slot: true + usage_slot_name: parent + range: Person + multivalued: true + inlined: true + inlined_as_list: true + Organization_full_name: + name: Organization_full_name + definition_uri: https://w3id.org/linkml/examples/personinfo/full_name + description: name of the organization, e.g. ACME inc + title: full name + from_schema: https://w3id.org/linkml/examples/personinfo + is_a: full_name + domain: Organization + slot_uri: ex:full_name + alias: full_name + owner: Organization + domain_of: + - Organization + is_usage_slot: true + usage_slot_name: full_name + range: string + Organization_parent: + name: Organization_parent + definition_uri: https://w3id.org/linkml/examples/personinfo/parent + from_schema: https://w3id.org/linkml/examples/personinfo + is_a: parent + domain: Organization + slot_uri: ex:parent + alias: parent + owner: Organization + domain_of: + - Organization + is_usage_slot: true + usage_slot_name: parent + range: Organization + multivalued: true + inlined: true + inlined_as_list: true +classes: + Thing: + name: Thing + definition_uri: https://w3id.org/linkml/examples/personinfo/Thing + from_schema: https://w3id.org/linkml/examples/personinfo + slots: + - id + - full_name + class_uri: ex:Thing + Person: + name: Person + definition_uri: https://w3id.org/linkml/examples/personinfo/Person + from_schema: https://w3id.org/linkml/examples/personinfo + mappings: + - sdo:Person + is_a: Thing + slots: + - id + - full_name + - person__aliases + - person__phone + - person__age + - Person_parent + slot_usage: + parent: + name: parent + range: Person + attributes: + aliases: + name: aliases + multivalued: true + phone: + name: phone + age: + name: age + description: age in years + range: integer + class_uri: sdo:Person + Organization: + name: Organization + definition_uri: https://w3id.org/linkml/examples/personinfo/Organization + from_schema: https://w3id.org/linkml/examples/personinfo + is_a: Thing + slots: + - id + - Organization_full_name + - Organization_parent + slot_usage: + full_name: + name: full_name + description: name of the organization, e.g. ACME inc + parent: + name: parent + range: Organization + class_uri: ex:Organization + GeoObject: + name: GeoObject + definition_uri: https://w3id.org/linkml/examples/personinfo/GeoObject + from_schema: https://w3id.org/linkml/examples/personinfo + is_a: Thing + slots: + - id + - full_name + - geoObject__aliases + - geoObject__age + attributes: + aliases: + name: aliases + comments: + - we introduce a deliberate conflict (single vs multivalied) with the aliases + attribute that is local to person + multivalued: false + age: + name: age + comments: + - we introduce a deliberate conflict (type vs class range) with the age attribute + that is local to person + range: GeoAge + class_uri: ex:GeoObject + GeoAge: + name: GeoAge + definition_uri: https://w3id.org/linkml/examples/personinfo/GeoAge + from_schema: https://w3id.org/linkml/examples/personinfo + slots: + - geoAge__unit + - geoAge__value + attributes: + unit: + name: unit + value: + name: value + range: float + class_uri: ex:GeoAge +metamodel_version: 1.7.0 +source_file: linkml_issue_384.yaml +source_file_date: '2000-01-01T00:00:00' +source_file_size: 1 +generation_date: '2000-01-01T00:00:00' + +# --- linkml_issue_384.context.jsonld --- +{ + "@context": { + "@vocab": "https://w3id.org/linkml/examples/personinfo/", + "GeoAge": { + "@id": "GeoAge" + }, + "GeoObject": { + "@id": "GeoObject" + }, + "Organization": { + "@id": "Organization" + }, + "Person": { + "@id": "sdo:Person" + }, + "Thing": { + "@id": "Thing" + }, + "age": { + "@id": "age", + "@type": "xsd:integer" + }, + "aliases": { + "@id": "aliases" + }, + "ex": "https://w3id.org/linkml/examples/personinfo/", + "full_name": { + "@id": "full_name" + }, + "id": { + "@id": "id" + }, + "linkml": "https://w3id.org/linkml/", + "parent": { + "@id": "parent", + "@type": "@id" + }, + "phone": { + "@id": "phone" + }, + "sdo": "http://schema.org/", + "unit": { + "@id": "unit" + }, + "value": { + "@id": "value", + "@type": "xsd:float" + }, + "xsd": "http://www.w3.org/2001/XMLSchema#" + } +} + +# --- linkml_issue_384.ttl --- + . + "https://w3id.org/linkml/examples/personinfo"^^ . + . + . + "https://w3id.org/linkml/examples/personinfo/GeoAge"^^ . + "https://w3id.org/linkml/examples/personinfo/GeoAge"^^ . + _:cb87aa37e3ad3dfbc100de41e622ae57a55c01252d953cd7f596f3f617c04a97ed . + . + . + . + "https://w3id.org/linkml/examples/personinfo"^^ . + . + . + "https://w3id.org/linkml/examples/personinfo/GeoObject"^^ . + "https://w3id.org/linkml/examples/personinfo/GeoObject"^^ . + . + _:cb6f98b99448d1238734b7af2cc92e23f15ba5c211db30144440eb739460c2cd83 . + . + . + . + . + . + "https://w3id.org/linkml/examples/personinfo"^^ . + "https://w3id.org/linkml/examples/personinfo/Organization"^^ . + "https://w3id.org/linkml/examples/personinfo/Organization"^^ . + . + _:cbb30ac86ca541a453eee19cdf1b7946df090933188ade517a9677564c8beac5bd . + . + . + . + "full name" . + . + "https://w3id.org/linkml/examples/personinfo"^^ . + "full_name" . + "https://w3id.org/linkml/examples/personinfo/full_name"^^ . + "name of the organization, e.g. ACME inc" . + . + . + . + "true"^^ . + . + . + "https://w3id.org/linkml/examples/personinfo/full_name"^^ . + "full_name" . + . + "https://w3id.org/linkml/examples/personinfo"^^ . + "parent" . + "https://w3id.org/linkml/examples/personinfo/parent"^^ . + . + . + "true"^^ . + "true"^^ . + . + "true"^^ . + "true"^^ . + . + . + "https://w3id.org/linkml/examples/personinfo/parent"^^ . + "parent" . + . + "https://w3id.org/linkml/examples/personinfo"^^ . + "sdo:Person"^^ . + . + . + . + "http://schema.org/Person"^^ . + "https://w3id.org/linkml/examples/personinfo/Person"^^ . + . + _:cba5af5c45ef581571107e9f40ba038543b617d27dfa8e41f71bfa140c3e5b8c09 . + . + . + . + . + . + . + . + "https://w3id.org/linkml/examples/personinfo"^^ . + "parent" . + "https://w3id.org/linkml/examples/personinfo/parent"^^ . + . + . + "true"^^ . + "true"^^ . + . + "true"^^ . + "true"^^ . + . + . + "https://w3id.org/linkml/examples/personinfo/parent"^^ . + "parent" . + . + "https://w3id.org/linkml/examples/personinfo"^^ . + "https://w3id.org/linkml/examples/personinfo/Thing"^^ . + "https://w3id.org/linkml/examples/personinfo/Thing"^^ . + _:cb0 . + . + . + . + "we introduce a deliberate conflict (type vs class range) with the age attribute that is local to person" . + "age in years" . + . + . + . + "we introduce a deliberate conflict (single vs multivalied) with the aliases attribute that is local to person" . + "true"^^ . + . + "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"boolean\"." . + "schema:Boolean"^^ . + "https://w3id.org/linkml/types"^^ . + "Bool" . + "https://w3id.org/linkml/Boolean"^^ . + "A binary (true or false) value" . + "linkml:types" . + "bool" . + "http://www.w3.org/2001/XMLSchema#boolean"^^ . + "https://www.w3.org/TR/curie/" . + . + "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"curie\"." . + "https://w3id.org/linkml/types"^^ . + "in RDF serializations this MUST be expanded to a URI" . + "in non-RDF serializations MAY be serialized as the compact representation" . + "Curie" . + "https://w3id.org/linkml/Curie"^^ . + "a compact URI" . + "linkml:types" . + "str" . + "http://www.w3.org/2001/XMLSchema#string"^^ . + . + "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"date\"." . + "URI is dateTime because OWL reasoners don't work with straight date or time" . + "schema:Date"^^ . + "https://w3id.org/linkml/types"^^ . + "XSDDate" . + "https://w3id.org/linkml/Date"^^ . + "a date (year, month and day) in an idealized calendar" . + "linkml:types" . + "str" . + "http://www.w3.org/2001/XMLSchema#date"^^ . + . + "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"date_or_datetime\"." . + "https://w3id.org/linkml/types"^^ . + "str" . + "https://w3id.org/linkml/DateOrDatetime"^^ . + "Either a date or a datetime" . + "linkml:types" . + "str" . + "https://w3id.org/linkml/DateOrDatetime"^^ . + . + "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"datetime\"." . + "schema:DateTime"^^ . + "https://w3id.org/linkml/types"^^ . + "XSDDateTime" . + "https://w3id.org/linkml/Datetime"^^ . + "The combination of a date and time" . + "linkml:types" . + "str" . + "http://www.w3.org/2001/XMLSchema#dateTime"^^ . + . + "schema:Number"^^ . + "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"decimal\"." . + "https://w3id.org/linkml/types"^^ . + "Decimal" . + "https://w3id.org/linkml/Decimal"^^ . + "A real number with arbitrary precision that conforms to the xsd:decimal specification" . + "linkml:types" . + "http://www.w3.org/2001/XMLSchema#decimal"^^ . + . + "schema:Float"^^ . + "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"double\"." . + "https://w3id.org/linkml/types"^^ . + "float" . + "https://w3id.org/linkml/Double"^^ . + "A real number that conforms to the xsd:double specification" . + "linkml:types" . + "http://www.w3.org/2001/XMLSchema#double"^^ . + . + "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"float\"." . + "schema:Float"^^ . + "https://w3id.org/linkml/types"^^ . + "float" . + "https://w3id.org/linkml/Float"^^ . + "A real number that conforms to the xsd:float specification" . + "linkml:types" . + "http://www.w3.org/2001/XMLSchema#float"^^ . + "full name" . + . + "https://w3id.org/linkml/examples/personinfo"^^ . + "https://w3id.org/linkml/examples/personinfo/full_name"^^ . + . + . + . + "https://w3id.org/linkml/examples/personinfo/full_name"^^ . + . + "https://w3id.org/linkml/examples/personinfo"^^ . + "unit" . + . + . + . + "https://w3id.org/linkml/examples/personinfo/unit"^^ . + . + "https://w3id.org/linkml/examples/personinfo"^^ . + "value" . + . + . + . + "https://w3id.org/linkml/examples/personinfo/value"^^ . + . + "https://w3id.org/linkml/examples/personinfo"^^ . + "we introduce a deliberate conflict (type vs class range) with the age attribute that is local to person" . + "age" . + . + "true"^^ . + "true"^^ . + . + . + "https://w3id.org/linkml/examples/personinfo/age"^^ . + . + "https://w3id.org/linkml/examples/personinfo"^^ . + "we introduce a deliberate conflict (single vs multivalied) with the aliases attribute that is local to person" . + "aliases" . + . + . + . + "https://w3id.org/linkml/examples/personinfo/aliases"^^ . + . + "https://w3id.org/linkml/examples/personinfo"^^ . + "https://w3id.org/linkml/examples/personinfo/id"^^ . + . + . + . + "https://w3id.org/linkml/examples/personinfo/id"^^ . + . + "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"integer\"." . + "schema:Integer"^^ . + "https://w3id.org/linkml/types"^^ . + "int" . + "https://w3id.org/linkml/Integer"^^ . + "An integer" . + "linkml:types" . + "http://www.w3.org/2001/XMLSchema#integer"^^ . + "https://www.ietf.org/archive/id/draft-goessner-dispatch-jsonpath-00.html" . + . + "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"jsonpath\"." . + "https://w3id.org/linkml/types"^^ . + "str" . + "https://w3id.org/linkml/Jsonpath"^^ . + "A string encoding a JSON Path. The value of the string MUST conform to JSON Point syntax and SHOULD dereference to zero or more valid objects within the current instance document when encoded in tree form." . + "linkml:types" . + "str" . + "http://www.w3.org/2001/XMLSchema#string"^^ . + "https://datatracker.ietf.org/doc/html/rfc6901" . + . + "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"jsonpointer\"." . + "https://w3id.org/linkml/types"^^ . + "str" . + "https://w3id.org/linkml/Jsonpointer"^^ . + "A string encoding a JSON Pointer. The value of the string MUST conform to JSON Point syntax and SHOULD dereference to a valid object within the current instance document when encoded in tree form." . + "linkml:types" . + "str" . + "http://www.w3.org/2001/XMLSchema#string"^^ . + . + "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"ncname\"." . + "https://w3id.org/linkml/types"^^ . + "NCName" . + "https://w3id.org/linkml/Ncname"^^ . + "Prefix part of CURIE" . + "linkml:types" . + "str" . + "http://www.w3.org/2001/XMLSchema#string"^^ . + . + "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"nodeidentifier\"." . + "https://w3id.org/linkml/types"^^ . + "NodeIdentifier" . + "https://w3id.org/linkml/Nodeidentifier"^^ . + "A URI, CURIE or BNODE that represents a node in a model." . + "linkml:types" . + "str" . + "http://www.w3.org/ns/shex#nonLiteral"^^ . + . + "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"objectidentifier\"." . + "https://w3id.org/linkml/types"^^ . + "Used for inheritance and type checking" . + "ElementIdentifier" . + "https://w3id.org/linkml/Objectidentifier"^^ . + "A URI or CURIE that represents an object in the model." . + "linkml:types" . + "str" . + "http://www.w3.org/ns/shex#iri"^^ . + . + "https://w3id.org/linkml/examples/personinfo"^^ . + "https://w3id.org/linkml/examples/personinfo/parent"^^ . + "true"^^ . + "true"^^ . + "true"^^ . + . + "https://w3id.org/linkml/examples/personinfo/parent"^^ . + . + "https://w3id.org/linkml/examples/personinfo"^^ . + "age" . + "age in years" . + . + . + . + "https://w3id.org/linkml/examples/personinfo/age"^^ . + . + "https://w3id.org/linkml/examples/personinfo"^^ . + "aliases" . + . + "true"^^ . + . + . + "https://w3id.org/linkml/examples/personinfo/aliases"^^ . + . + "https://w3id.org/linkml/examples/personinfo"^^ . + "phone" . + . + . + . + "https://w3id.org/linkml/examples/personinfo/phone"^^ . + "https://creativecommons.org/publicdomain/zero/1.0/" . + . + _:cb12ea35aa63cc721cd40fd34b4d5d9273803f97d45aa2daf1ba2eaa4b56057c201 . + _:cb143d2c4dab8e5bf48fda351a4d8564e15c151870b769a85f734179402d94c77f6 . + _:cb1a41ad3544cb8764d54c45fc982a5303f0bd602841d715a83a410416873890504 . + . + . + . + . + . + "ex" . + . + "2000-01-01T00:00:00"^^ . + "https://w3id.org/linkml/examples/personinfo"^^ . + "linkml:types"^^ . + "1.7.0" . + . + . + . + . + . + . + . + . + . + . + . + . + . + "linkml_issue_384.yaml" . + "2000-01-01T00:00:00"^^ . + "1"^^ . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + "https://www.w3.org/TR/sparql11-query/#propertypaths" . + . + "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"sparqlpath\"." . + "https://w3id.org/linkml/types"^^ . + "str" . + "https://w3id.org/linkml/Sparqlpath"^^ . + "A string encoding a SPARQL Property Path. The value of the string MUST conform to SPARQL syntax and SHOULD dereference to zero or more valid objects within the current instance document when encoded as RDF." . + "linkml:types" . + "str" . + "http://www.w3.org/2001/XMLSchema#string"^^ . + . + "In RDF serializations, a slot with range of string is treated as a literal or type xsd:string. If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"string\"." . + "schema:Text"^^ . + "https://w3id.org/linkml/types"^^ . + "str" . + "https://w3id.org/linkml/String"^^ . + "A character string" . + "linkml:types" . + "http://www.w3.org/2001/XMLSchema#string"^^ . + . + "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"time\"." . + "URI is dateTime because OWL reasoners do not work with straight date or time" . + "schema:Time"^^ . + "https://w3id.org/linkml/types"^^ . + "XSDTime" . + "https://w3id.org/linkml/Time"^^ . + "A time object represents a (local) time of day, independent of any particular day" . + "linkml:types" . + "str" . + "http://www.w3.org/2001/XMLSchema#time"^^ . + . + "https://www.ietf.org/rfc/rfc3987.txt" . + . + "schema:URL"^^ . + "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"uri\"." . + "https://w3id.org/linkml/types"^^ . + "in RDF serializations a slot with range of uri is treated as a literal or type xsd:anyURI unless it is an identifier or a reference to an identifier, in which case it is translated directly to a node" . + "URI" . + "https://w3id.org/linkml/Uri"^^ . + "a complete URI" . + "linkml:types" . + "str" . + "http://www.w3.org/2001/XMLSchema#anyURI"^^ . + . + "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"uriorcurie\"." . + "https://w3id.org/linkml/types"^^ . + "URIorCURIE" . + "https://w3id.org/linkml/Uriorcurie"^^ . + "a URI or a CURIE" . + "linkml:types" . + "str" . + "http://www.w3.org/2001/XMLSchema#anyURI"^^ . + . + . +_:cb12ea35aa63cc721cd40fd34b4d5d9273803f97d45aa2daf1ba2eaa4b56057c201 "https://w3id.org/linkml/"^^ . +_:cb12ea35aa63cc721cd40fd34b4d5d9273803f97d45aa2daf1ba2eaa4b56057c201 "linkml" . +_:cb143d2c4dab8e5bf48fda351a4d8564e15c151870b769a85f734179402d94c77f6 "http://schema.org/"^^ . +_:cb143d2c4dab8e5bf48fda351a4d8564e15c151870b769a85f734179402d94c77f6 "sdo" . +_:cb1a41ad3544cb8764d54c45fc982a5303f0bd602841d715a83a410416873890504 "https://w3id.org/linkml/examples/personinfo/"^^ . +_:cb1a41ad3544cb8764d54c45fc982a5303f0bd602841d715a83a410416873890504 "ex" . + +# --- linkml_issue_384.py --- +# Auto generated from linkml_issue_384.yaml by pythongen.py version: 0.0.1 +# Generation date: 2000-01-01T00:00:00 +# Schema: personinfo +# +# id: https://w3id.org/linkml/examples/personinfo +# description: +# license: https://creativecommons.org/publicdomain/zero/1.0/ + +import dataclasses +import re +from dataclasses import dataclass +from datetime import ( + date, + datetime, + time +) +from typing import ( + Any, + ClassVar, + Dict, + List, + Optional, + Union +) + +from jsonasobj2 import ( + JsonObj, + as_dict +) +from linkml_runtime.linkml_model.meta import ( + EnumDefinition, + PermissibleValue, + PvFormulaOptions +) +from linkml_runtime.utils.curienamespace import CurieNamespace +from linkml_runtime.utils.enumerations import EnumDefinitionImpl +from linkml_runtime.utils.formatutils import ( + camelcase, + sfx, + underscore +) +from linkml_runtime.utils.metamodelcore import ( + bnode, + empty_dict, + empty_list +) +from linkml_runtime.utils.slot import Slot +from linkml_runtime.utils.yamlutils import ( + YAMLRoot, + extended_float, + extended_int, + extended_str +) +from rdflib import ( + Namespace, + URIRef +) + +from linkml_runtime.linkml_model.types import Float, Integer, String + +metamodel_version = "1.7.0" +version = None + +# Namespaces +EX = CurieNamespace('ex', 'https://w3id.org/linkml/examples/personinfo/') +LINKML = CurieNamespace('linkml', 'https://w3id.org/linkml/') +SDO = CurieNamespace('sdo', 'http://schema.org/') +DEFAULT_ = EX + + +# Types + +# Class references + + + +@dataclass(repr=False) +class Thing(YAMLRoot): + _inherited_slots: ClassVar[list[str]] = [] + + class_class_uri: ClassVar[URIRef] = EX["Thing"] + class_class_curie: ClassVar[str] = "ex:Thing" + class_name: ClassVar[str] = "Thing" + class_model_uri: ClassVar[URIRef] = EX.Thing + + id: Optional[str] = None + full_name: Optional[str] = None + + def __post_init__(self, *_: str, **kwargs: Any): + if self.id is not None and not isinstance(self.id, str): + self.id = str(self.id) + + if self.full_name is not None and not isinstance(self.full_name, str): + self.full_name = str(self.full_name) + + super().__post_init__(**kwargs) + + +@dataclass(repr=False) +class Person(Thing): + _inherited_slots: ClassVar[list[str]] = [] + + class_class_uri: ClassVar[URIRef] = SDO["Person"] + class_class_curie: ClassVar[str] = "sdo:Person" + class_name: ClassVar[str] = "Person" + class_model_uri: ClassVar[URIRef] = EX.Person + + aliases: Optional[Union[str, list[str]]] = empty_list() + phone: Optional[str] = None + age: Optional[int] = None + parent: Optional[Union[Union[dict, "Person"], list[Union[dict, "Person"]]]] = empty_list() + + def __post_init__(self, *_: str, **kwargs: Any): + if not isinstance(self.aliases, list): + self.aliases = [self.aliases] if self.aliases is not None else [] + self.aliases = [v if isinstance(v, str) else str(v) for v in self.aliases] + + if self.phone is not None and not isinstance(self.phone, str): + self.phone = str(self.phone) + + if self.age is not None and not isinstance(self.age, int): + self.age = int(self.age) + + if not isinstance(self.parent, list): + self.parent = [self.parent] if self.parent is not None else [] + self.parent = [v if isinstance(v, Person) else Person(**as_dict(v)) for v in self.parent] + + super().__post_init__(**kwargs) + + +@dataclass(repr=False) +class Organization(Thing): + _inherited_slots: ClassVar[list[str]] = [] + + class_class_uri: ClassVar[URIRef] = EX["Organization"] + class_class_curie: ClassVar[str] = "ex:Organization" + class_name: ClassVar[str] = "Organization" + class_model_uri: ClassVar[URIRef] = EX.Organization + + full_name: Optional[str] = None + parent: Optional[Union[Union[dict, "Organization"], list[Union[dict, "Organization"]]]] = empty_list() + + def __post_init__(self, *_: str, **kwargs: Any): + if self.full_name is not None and not isinstance(self.full_name, str): + self.full_name = str(self.full_name) + + if not isinstance(self.parent, list): + self.parent = [self.parent] if self.parent is not None else [] + self.parent = [v if isinstance(v, Organization) else Organization(**as_dict(v)) for v in self.parent] + + super().__post_init__(**kwargs) + + +@dataclass(repr=False) +class GeoObject(Thing): + _inherited_slots: ClassVar[list[str]] = [] + + class_class_uri: ClassVar[URIRef] = EX["GeoObject"] + class_class_curie: ClassVar[str] = "ex:GeoObject" + class_name: ClassVar[str] = "GeoObject" + class_model_uri: ClassVar[URIRef] = EX.GeoObject + + aliases: Optional[str] = None + age: Optional[Union[dict, "GeoAge"]] = None + + def __post_init__(self, *_: str, **kwargs: Any): + if self.aliases is not None and not isinstance(self.aliases, str): + self.aliases = str(self.aliases) + + if self.age is not None and not isinstance(self.age, GeoAge): + self.age = GeoAge(**as_dict(self.age)) + + super().__post_init__(**kwargs) + + +@dataclass(repr=False) +class GeoAge(YAMLRoot): + _inherited_slots: ClassVar[list[str]] = [] + + class_class_uri: ClassVar[URIRef] = EX["GeoAge"] + class_class_curie: ClassVar[str] = "ex:GeoAge" + class_name: ClassVar[str] = "GeoAge" + class_model_uri: ClassVar[URIRef] = EX.GeoAge + + unit: Optional[str] = None + value: Optional[float] = None + + def __post_init__(self, *_: str, **kwargs: Any): + if self.unit is not None and not isinstance(self.unit, str): + self.unit = str(self.unit) + + if self.value is not None and not isinstance(self.value, float): + self.value = float(self.value) + + super().__post_init__(**kwargs) + + +# Enumerations + + +# Slots +class slots: + pass + +slots.id = Slot(uri=EX.id, name="id", curie=EX.curie('id'), + model_uri=EX.id, domain=None, range=Optional[str]) + +slots.full_name = Slot(uri=EX.full_name, name="full_name", curie=EX.curie('full_name'), + model_uri=EX.full_name, domain=None, range=Optional[str]) + +slots.parent = Slot(uri=EX.parent, name="parent", curie=EX.curie('parent'), + model_uri=EX.parent, domain=None, range=Optional[Union[Union[dict, Thing], list[Union[dict, Thing]]]]) + +slots.person__aliases = Slot(uri=EX.aliases, name="person__aliases", curie=EX.curie('aliases'), + model_uri=EX.person__aliases, domain=None, range=Optional[Union[str, list[str]]]) + +slots.person__phone = Slot(uri=EX.phone, name="person__phone", curie=EX.curie('phone'), + model_uri=EX.person__phone, domain=None, range=Optional[str]) + +slots.person__age = Slot(uri=EX.age, name="person__age", curie=EX.curie('age'), + model_uri=EX.person__age, domain=None, range=Optional[int]) + +slots.geoObject__aliases = Slot(uri=EX.aliases, name="geoObject__aliases", curie=EX.curie('aliases'), + model_uri=EX.geoObject__aliases, domain=None, range=Optional[str]) + +slots.geoObject__age = Slot(uri=EX.age, name="geoObject__age", curie=EX.curie('age'), + model_uri=EX.geoObject__age, domain=None, range=Optional[Union[dict, GeoAge]]) + +slots.geoAge__unit = Slot(uri=EX.unit, name="geoAge__unit", curie=EX.curie('unit'), + model_uri=EX.geoAge__unit, domain=None, range=Optional[str]) + +slots.geoAge__value = Slot(uri=EX.value, name="geoAge__value", curie=EX.curie('value'), + model_uri=EX.geoAge__value, domain=None, range=Optional[float]) + +slots.Person_parent = Slot(uri=EX.parent, name="Person_parent", curie=EX.curie('parent'), + model_uri=EX.Person_parent, domain=Person, range=Optional[Union[Union[dict, "Person"], list[Union[dict, "Person"]]]]) + +slots.Organization_full_name = Slot(uri=EX.full_name, name="Organization_full_name", curie=EX.curie('full_name'), + model_uri=EX.Organization_full_name, domain=Organization, range=Optional[str]) + +slots.Organization_parent = Slot(uri=EX.parent, name="Organization_parent", curie=EX.curie('parent'), + model_uri=EX.Organization_parent, domain=Organization, range=Optional[Union[Union[dict, "Organization"], list[Union[dict, "Organization"]]]]) + diff --git a/tests/linkml/test_issues/__snapshots__/linkml_issue_384.owl.txt b/tests/linkml/test_issues/__snapshots__/linkml_issue_384.owl.txt new file mode 100644 index 0000000000..c1772f2805 --- /dev/null +++ b/tests/linkml/test_issues/__snapshots__/linkml_issue_384.owl.txt @@ -0,0 +1,368 @@ +# --- linkml_issue_384-False-False.owl --- + . + "GeoAge" . + _:cb1a82d378de24ceee4d880fb97fa60edc01c846e778d2e8849eb068d68cf447011 . + _:cb1e3ca21d885a6aa9198c7e0da80bb8945cc88e60fa7a050b328feea3ef8631fda . + _:cb1f07ec28cdb8029f73418d769b10652eadf89c2930f7e5d948ba3665125ffdd3b . + _:cb23b1f6eed54b7e50afd4742d834d2c517acf217864b10b109eee245892d46cb49 . + _:cb26e0d87f901e35c7138955124b4f6c3cdc7262aa37e10fdd8e0173f74bfdff98d . + _:cb276bc5937f811a0b7bd8e281abb2d609d5cf68f1e658279732cdaa25f56657b12 . + . + . + "GeoObject" . + . + _:cb19bb3d4e5067478c4b42732ad7814ace5f746c27d7053c41587dd574fcf2f4606 . + _:cb1d750bf2fa9ce3471746e17effe6f486ba74b3a158ac58c7ec5d5b425f84df5cf . + _:cb223ace63a5d4c89c0969a4be05e62755cf1328abcf1fc6fdf5e09256462f70a2b . + _:cb27cb1dd111a3ff7c3f28148cef9e09a6bb7205916d18918af480e011edec108ba . + _:cb2af9ff61cc76b6f2a2dcf571b7a049921d1546c340489657e3942fb0a715a36fe . + _:cb2b84ec75bbd99b370b2c82e11803b35f16724d0aeebfae11886065df507dfb883 . + . + . + "Organization" . + . + _:cb23e14b200c6b61b63e1f4757ffefca473fb646f496f81f18937ae0a96fa6a777e . + _:cb24a85419af6cf0d15e7bae94c471eaf0632069a8e2af85f5833415d38d3e2a7c1 . + _:cb27102cb0c73e192ca1d4283cc7f20a32a15988266a2823e5828e304828d03a5c2 . + _:cb2766e399d6d56b5602562bcdfa8bcade0989eca8ed336f11b67f3c7ad7edf0c62 . + _:cb279b19c4b6a0fd710a23b5ac285573ff9ab68e6e189f3b9f275a6676d23892747 . + . + . + "Person" . + . + _:cb2005559eb2b21a3c9355da151f33d642e4af7b0d372076ce53935aec788fa6f67 . + _:cb23bf24435ce7b5f75f5a486947997ffb3fafc286b8c79354e772e0b9db2191f30 . + _:cb27e50cad98ec340969db736cbaf4bc5f6bbe583f92bac10dd75e1deb7cf54953f . + _:cb2901721e4c091193c4a20c1bd5ca511041ba22625906480032d1dded426e4c3a8 . + _:cb2a3227bee2c818a432f61adb96aa923b9f4dccb58093bf6fc5490945bc59586f7 . + _:cb2c3053af06dbc90a2856ed009dcc90fba35d63942c364ccd21e52d8bfb97df1ec . + _:cb2ca4bb2d727b7c18cdb247369b15b04322625d0ebc5721da8bb8c6e344edaf78b . + _:cb2cbb40c2f63ead4e90a67a6ffe2ffac89cba69dbdaad6486c6b163baa50037371 . + _:cb2e15362173eed22c873b7b773750951b40ad1476cd33cc17ef9665896988c321b . + _:cb314417b22ec189a2eaf05c5bff52d506a25055a8a063d0e4dea9b52822b25605f . + . + . + . + "Thing" . + _:cb1a0196aabf80e00feb710af29d4b2330953104cd23be4266ef6f0890e1e938a89 . + _:cb1d30783b7a5397864f25ebd7654d631bf6d445fef6ee4733de82582f9b12cb8cd . + _:cb1dbb654f69b67bcab7757946c5b0cce8f0314c46a5655eed834e8e5e447b23a52 . + _:cb1f249ba3e141c783753ca7f6bc46c4ce0ddb60ded99080b4958fc47b2e65e1e5a . + _:cb22537d349c147ef9d8f188db844904b96f7ea210acc0858184a31419e78f74c9e . + _:cb22de6a488b77633e4141164ae4ac6e8668dba8585b379d3b296f4a4890f7cce23 . + . + . + . + . + "full name" . + . + "full_name" . + . + . + "id" . + . + . + "parent" . + . + . + . + "phone" . + . + . + "unit" . + . + . + "value" . + . + . + "personinfo" . +_:cb19bb3d4e5067478c4b42732ad7814ace5f746c27d7053c41587dd574fcf2f4606 . +_:cb19bb3d4e5067478c4b42732ad7814ace5f746c27d7053c41587dd574fcf2f4606 "0"^^ . +_:cb19bb3d4e5067478c4b42732ad7814ace5f746c27d7053c41587dd574fcf2f4606 . +_:cb1a0196aabf80e00feb710af29d4b2330953104cd23be4266ef6f0890e1e938a89 . +_:cb1a0196aabf80e00feb710af29d4b2330953104cd23be4266ef6f0890e1e938a89 "0"^^ . +_:cb1a0196aabf80e00feb710af29d4b2330953104cd23be4266ef6f0890e1e938a89 . +_:cb1a82d378de24ceee4d880fb97fa60edc01c846e778d2e8849eb068d68cf447011 . +_:cb1a82d378de24ceee4d880fb97fa60edc01c846e778d2e8849eb068d68cf447011 "0"^^ . +_:cb1a82d378de24ceee4d880fb97fa60edc01c846e778d2e8849eb068d68cf447011 . +_:cb1d30783b7a5397864f25ebd7654d631bf6d445fef6ee4733de82582f9b12cb8cd . +_:cb1d30783b7a5397864f25ebd7654d631bf6d445fef6ee4733de82582f9b12cb8cd . +_:cb1d30783b7a5397864f25ebd7654d631bf6d445fef6ee4733de82582f9b12cb8cd . +_:cb1d750bf2fa9ce3471746e17effe6f486ba74b3a158ac58c7ec5d5b425f84df5cf . +_:cb1d750bf2fa9ce3471746e17effe6f486ba74b3a158ac58c7ec5d5b425f84df5cf "1"^^ . +_:cb1d750bf2fa9ce3471746e17effe6f486ba74b3a158ac58c7ec5d5b425f84df5cf . +_:cb1dbb654f69b67bcab7757946c5b0cce8f0314c46a5655eed834e8e5e447b23a52 . +_:cb1dbb654f69b67bcab7757946c5b0cce8f0314c46a5655eed834e8e5e447b23a52 "1"^^ . +_:cb1dbb654f69b67bcab7757946c5b0cce8f0314c46a5655eed834e8e5e447b23a52 . +_:cb1e3ca21d885a6aa9198c7e0da80bb8945cc88e60fa7a050b328feea3ef8631fda . +_:cb1e3ca21d885a6aa9198c7e0da80bb8945cc88e60fa7a050b328feea3ef8631fda "1"^^ . +_:cb1e3ca21d885a6aa9198c7e0da80bb8945cc88e60fa7a050b328feea3ef8631fda . +_:cb1f07ec28cdb8029f73418d769b10652eadf89c2930f7e5d948ba3665125ffdd3b . +_:cb1f07ec28cdb8029f73418d769b10652eadf89c2930f7e5d948ba3665125ffdd3b . +_:cb1f07ec28cdb8029f73418d769b10652eadf89c2930f7e5d948ba3665125ffdd3b . +_:cb1f249ba3e141c783753ca7f6bc46c4ce0ddb60ded99080b4958fc47b2e65e1e5a . +_:cb1f249ba3e141c783753ca7f6bc46c4ce0ddb60ded99080b4958fc47b2e65e1e5a "0"^^ . +_:cb1f249ba3e141c783753ca7f6bc46c4ce0ddb60ded99080b4958fc47b2e65e1e5a . +_:cb2005559eb2b21a3c9355da151f33d642e4af7b0d372076ce53935aec788fa6f67 . +_:cb2005559eb2b21a3c9355da151f33d642e4af7b0d372076ce53935aec788fa6f67 "0"^^ . +_:cb2005559eb2b21a3c9355da151f33d642e4af7b0d372076ce53935aec788fa6f67 . +_:cb223ace63a5d4c89c0969a4be05e62755cf1328abcf1fc6fdf5e09256462f70a2b . +_:cb223ace63a5d4c89c0969a4be05e62755cf1328abcf1fc6fdf5e09256462f70a2b . +_:cb223ace63a5d4c89c0969a4be05e62755cf1328abcf1fc6fdf5e09256462f70a2b . +_:cb22537d349c147ef9d8f188db844904b96f7ea210acc0858184a31419e78f74c9e . +_:cb22537d349c147ef9d8f188db844904b96f7ea210acc0858184a31419e78f74c9e . +_:cb22537d349c147ef9d8f188db844904b96f7ea210acc0858184a31419e78f74c9e . +_:cb22de6a488b77633e4141164ae4ac6e8668dba8585b379d3b296f4a4890f7cce23 . +_:cb22de6a488b77633e4141164ae4ac6e8668dba8585b379d3b296f4a4890f7cce23 "1"^^ . +_:cb22de6a488b77633e4141164ae4ac6e8668dba8585b379d3b296f4a4890f7cce23 . +_:cb23b1f6eed54b7e50afd4742d834d2c517acf217864b10b109eee245892d46cb49 . +_:cb23b1f6eed54b7e50afd4742d834d2c517acf217864b10b109eee245892d46cb49 "0"^^ . +_:cb23b1f6eed54b7e50afd4742d834d2c517acf217864b10b109eee245892d46cb49 . +_:cb23bf24435ce7b5f75f5a486947997ffb3fafc286b8c79354e772e0b9db2191f30 . +_:cb23bf24435ce7b5f75f5a486947997ffb3fafc286b8c79354e772e0b9db2191f30 "1"^^ . +_:cb23bf24435ce7b5f75f5a486947997ffb3fafc286b8c79354e772e0b9db2191f30 . +_:cb23e14b200c6b61b63e1f4757ffefca473fb646f496f81f18937ae0a96fa6a777e . +_:cb23e14b200c6b61b63e1f4757ffefca473fb646f496f81f18937ae0a96fa6a777e "0"^^ . +_:cb23e14b200c6b61b63e1f4757ffefca473fb646f496f81f18937ae0a96fa6a777e . +_:cb24a85419af6cf0d15e7bae94c471eaf0632069a8e2af85f5833415d38d3e2a7c1 . +_:cb24a85419af6cf0d15e7bae94c471eaf0632069a8e2af85f5833415d38d3e2a7c1 . +_:cb24a85419af6cf0d15e7bae94c471eaf0632069a8e2af85f5833415d38d3e2a7c1 . +_:cb26e0d87f901e35c7138955124b4f6c3cdc7262aa37e10fdd8e0173f74bfdff98d . +_:cb26e0d87f901e35c7138955124b4f6c3cdc7262aa37e10fdd8e0173f74bfdff98d . +_:cb26e0d87f901e35c7138955124b4f6c3cdc7262aa37e10fdd8e0173f74bfdff98d . +_:cb27102cb0c73e192ca1d4283cc7f20a32a15988266a2823e5828e304828d03a5c2 . +_:cb27102cb0c73e192ca1d4283cc7f20a32a15988266a2823e5828e304828d03a5c2 . +_:cb27102cb0c73e192ca1d4283cc7f20a32a15988266a2823e5828e304828d03a5c2 . +_:cb2766e399d6d56b5602562bcdfa8bcade0989eca8ed336f11b67f3c7ad7edf0c62 . +_:cb2766e399d6d56b5602562bcdfa8bcade0989eca8ed336f11b67f3c7ad7edf0c62 "0"^^ . +_:cb2766e399d6d56b5602562bcdfa8bcade0989eca8ed336f11b67f3c7ad7edf0c62 . +_:cb276bc5937f811a0b7bd8e281abb2d609d5cf68f1e658279732cdaa25f56657b12 . +_:cb276bc5937f811a0b7bd8e281abb2d609d5cf68f1e658279732cdaa25f56657b12 "1"^^ . +_:cb276bc5937f811a0b7bd8e281abb2d609d5cf68f1e658279732cdaa25f56657b12 . +_:cb279b19c4b6a0fd710a23b5ac285573ff9ab68e6e189f3b9f275a6676d23892747 . +_:cb279b19c4b6a0fd710a23b5ac285573ff9ab68e6e189f3b9f275a6676d23892747 "1"^^ . +_:cb279b19c4b6a0fd710a23b5ac285573ff9ab68e6e189f3b9f275a6676d23892747 . +_:cb27cb1dd111a3ff7c3f28148cef9e09a6bb7205916d18918af480e011edec108ba . +_:cb27cb1dd111a3ff7c3f28148cef9e09a6bb7205916d18918af480e011edec108ba "0"^^ . +_:cb27cb1dd111a3ff7c3f28148cef9e09a6bb7205916d18918af480e011edec108ba . +_:cb27e50cad98ec340969db736cbaf4bc5f6bbe583f92bac10dd75e1deb7cf54953f . +_:cb27e50cad98ec340969db736cbaf4bc5f6bbe583f92bac10dd75e1deb7cf54953f . +_:cb27e50cad98ec340969db736cbaf4bc5f6bbe583f92bac10dd75e1deb7cf54953f . +_:cb2901721e4c091193c4a20c1bd5ca511041ba22625906480032d1dded426e4c3a8 . +_:cb2901721e4c091193c4a20c1bd5ca511041ba22625906480032d1dded426e4c3a8 "0"^^ . +_:cb2901721e4c091193c4a20c1bd5ca511041ba22625906480032d1dded426e4c3a8 . +_:cb2a3227bee2c818a432f61adb96aa923b9f4dccb58093bf6fc5490945bc59586f7 . +_:cb2a3227bee2c818a432f61adb96aa923b9f4dccb58093bf6fc5490945bc59586f7 "0"^^ . +_:cb2a3227bee2c818a432f61adb96aa923b9f4dccb58093bf6fc5490945bc59586f7 . +_:cb2af9ff61cc76b6f2a2dcf571b7a049921d1546c340489657e3942fb0a715a36fe . +_:cb2af9ff61cc76b6f2a2dcf571b7a049921d1546c340489657e3942fb0a715a36fe . +_:cb2af9ff61cc76b6f2a2dcf571b7a049921d1546c340489657e3942fb0a715a36fe . +_:cb2b84ec75bbd99b370b2c82e11803b35f16724d0aeebfae11886065df507dfb883 . +_:cb2b84ec75bbd99b370b2c82e11803b35f16724d0aeebfae11886065df507dfb883 "1"^^ . +_:cb2b84ec75bbd99b370b2c82e11803b35f16724d0aeebfae11886065df507dfb883 . +_:cb2c3053af06dbc90a2856ed009dcc90fba35d63942c364ccd21e52d8bfb97df1ec . +_:cb2c3053af06dbc90a2856ed009dcc90fba35d63942c364ccd21e52d8bfb97df1ec . +_:cb2c3053af06dbc90a2856ed009dcc90fba35d63942c364ccd21e52d8bfb97df1ec . +_:cb2ca4bb2d727b7c18cdb247369b15b04322625d0ebc5721da8bb8c6e344edaf78b . +_:cb2ca4bb2d727b7c18cdb247369b15b04322625d0ebc5721da8bb8c6e344edaf78b . +_:cb2ca4bb2d727b7c18cdb247369b15b04322625d0ebc5721da8bb8c6e344edaf78b . +_:cb2cbb40c2f63ead4e90a67a6ffe2ffac89cba69dbdaad6486c6b163baa50037371 . +_:cb2cbb40c2f63ead4e90a67a6ffe2ffac89cba69dbdaad6486c6b163baa50037371 "1"^^ . +_:cb2cbb40c2f63ead4e90a67a6ffe2ffac89cba69dbdaad6486c6b163baa50037371 . +_:cb2e15362173eed22c873b7b773750951b40ad1476cd33cc17ef9665896988c321b . +_:cb2e15362173eed22c873b7b773750951b40ad1476cd33cc17ef9665896988c321b "0"^^ . +_:cb2e15362173eed22c873b7b773750951b40ad1476cd33cc17ef9665896988c321b . +_:cb314417b22ec189a2eaf05c5bff52d506a25055a8a063d0e4dea9b52822b25605f . +_:cb314417b22ec189a2eaf05c5bff52d506a25055a8a063d0e4dea9b52822b25605f . +_:cb314417b22ec189a2eaf05c5bff52d506a25055a8a063d0e4dea9b52822b25605f . + +# --- linkml_issue_384-True-True.owl --- + . + . + "GeoAge" . + _:cb1a82d378de24ceee4d880fb97fa60edc01c846e778d2e8849eb068d68cf447011 . + _:cb1def0490d3233d18bbcdc75ea440636f2bbe33bf0d9fd842e9ac6c85d56782eb1 . + _:cb1e3ca21d885a6aa9198c7e0da80bb8945cc88e60fa7a050b328feea3ef8631fda . + _:cb2251a2cc284802be192c79b4968c5e3e0d0a6310ef22b3c5b7486c1426634bfd4 . + _:cb23b1f6eed54b7e50afd4742d834d2c517acf217864b10b109eee245892d46cb49 . + _:cb276bc5937f811a0b7bd8e281abb2d609d5cf68f1e658279732cdaa25f56657b12 . + . + . + . + "GeoObject" . + . + _:cb19bb3d4e5067478c4b42732ad7814ace5f746c27d7053c41587dd574fcf2f4606 . + _:cb1d750bf2fa9ce3471746e17effe6f486ba74b3a158ac58c7ec5d5b425f84df5cf . + _:cb223ace63a5d4c89c0969a4be05e62755cf1328abcf1fc6fdf5e09256462f70a2b . + _:cb266ac9ae64a083e9a8801a1402dd3b934dad4729f78a3a400cdb27cd817aefd45 . + _:cb27cb1dd111a3ff7c3f28148cef9e09a6bb7205916d18918af480e011edec108ba . + _:cb2b84ec75bbd99b370b2c82e11803b35f16724d0aeebfae11886065df507dfb883 . + . + . + . + "Organization" . + . + _:cb2280f6fd5f67e623a7774cdf132efc33d1f1888d2169c7cdabd52865033586c09 . + _:cb23e14b200c6b61b63e1f4757ffefca473fb646f496f81f18937ae0a96fa6a777e . + _:cb24a85419af6cf0d15e7bae94c471eaf0632069a8e2af85f5833415d38d3e2a7c1 . + _:cb2766e399d6d56b5602562bcdfa8bcade0989eca8ed336f11b67f3c7ad7edf0c62 . + _:cb279b19c4b6a0fd710a23b5ac285573ff9ab68e6e189f3b9f275a6676d23892747 . + . + . + . + "Person" . + . + _:cb2005559eb2b21a3c9355da151f33d642e4af7b0d372076ce53935aec788fa6f67 . + _:cb23bf24435ce7b5f75f5a486947997ffb3fafc286b8c79354e772e0b9db2191f30 . + _:cb2437057497c9a38863099ba911a51798d2811dd43bf7614a1a792873f19481ee8 . + _:cb27a11dfb9f0596012dfa11a2e90982fcd3f563fae377f0b54b2c25a8d5fd2b833 . + _:cb2901721e4c091193c4a20c1bd5ca511041ba22625906480032d1dded426e4c3a8 . + _:cb2a3227bee2c818a432f61adb96aa923b9f4dccb58093bf6fc5490945bc59586f7 . + _:cb2ca4bb2d727b7c18cdb247369b15b04322625d0ebc5721da8bb8c6e344edaf78b . + _:cb2cb4e1fec6eb5699f09380fe4a8fc707d2e8560f57a574cd07f0ad44fd17a26a6 . + _:cb2cbb40c2f63ead4e90a67a6ffe2ffac89cba69dbdaad6486c6b163baa50037371 . + _:cb2e15362173eed22c873b7b773750951b40ad1476cd33cc17ef9665896988c321b . + . + . + . + . + "Thing" . + _:cb18a14288127d647d54c91079b08a551d276c4665ae2feb1c07c9504c757817f14 . + _:cb1a0196aabf80e00feb710af29d4b2330953104cd23be4266ef6f0890e1e938a89 . + _:cb1dbb654f69b67bcab7757946c5b0cce8f0314c46a5655eed834e8e5e447b23a52 . + _:cb1dc44781343e4bf0de94ad7dcf85f6baa016a27764022969adea0c36c1f4c12e5 . + _:cb1f249ba3e141c783753ca7f6bc46c4ce0ddb60ded99080b4958fc47b2e65e1e5a . + _:cb22de6a488b77633e4141164ae4ac6e8668dba8585b379d3b296f4a4890f7cce23 . + . + . + . + . + . + "full name" . + . + . + "full_name" . + . + . + . + "id" . + . + . + . + "parent" . + . + . + . + . + "phone" . + . + . + . + "unit" . + . + . + . + "value" . + . + . + "personinfo" . +_:cb18a14288127d647d54c91079b08a551d276c4665ae2feb1c07c9504c757817f14 . +_:cb18a14288127d647d54c91079b08a551d276c4665ae2feb1c07c9504c757817f14 . +_:cb18a14288127d647d54c91079b08a551d276c4665ae2feb1c07c9504c757817f14 . +_:cb19bb3d4e5067478c4b42732ad7814ace5f746c27d7053c41587dd574fcf2f4606 . +_:cb19bb3d4e5067478c4b42732ad7814ace5f746c27d7053c41587dd574fcf2f4606 "0"^^ . +_:cb19bb3d4e5067478c4b42732ad7814ace5f746c27d7053c41587dd574fcf2f4606 . +_:cb1a0196aabf80e00feb710af29d4b2330953104cd23be4266ef6f0890e1e938a89 . +_:cb1a0196aabf80e00feb710af29d4b2330953104cd23be4266ef6f0890e1e938a89 "0"^^ . +_:cb1a0196aabf80e00feb710af29d4b2330953104cd23be4266ef6f0890e1e938a89 . +_:cb1a82d378de24ceee4d880fb97fa60edc01c846e778d2e8849eb068d68cf447011 . +_:cb1a82d378de24ceee4d880fb97fa60edc01c846e778d2e8849eb068d68cf447011 "0"^^ . +_:cb1a82d378de24ceee4d880fb97fa60edc01c846e778d2e8849eb068d68cf447011 . +_:cb1d750bf2fa9ce3471746e17effe6f486ba74b3a158ac58c7ec5d5b425f84df5cf . +_:cb1d750bf2fa9ce3471746e17effe6f486ba74b3a158ac58c7ec5d5b425f84df5cf "1"^^ . +_:cb1d750bf2fa9ce3471746e17effe6f486ba74b3a158ac58c7ec5d5b425f84df5cf . +_:cb1dbb654f69b67bcab7757946c5b0cce8f0314c46a5655eed834e8e5e447b23a52 . +_:cb1dbb654f69b67bcab7757946c5b0cce8f0314c46a5655eed834e8e5e447b23a52 "1"^^ . +_:cb1dbb654f69b67bcab7757946c5b0cce8f0314c46a5655eed834e8e5e447b23a52 . +_:cb1dc44781343e4bf0de94ad7dcf85f6baa016a27764022969adea0c36c1f4c12e5 . +_:cb1dc44781343e4bf0de94ad7dcf85f6baa016a27764022969adea0c36c1f4c12e5 . +_:cb1dc44781343e4bf0de94ad7dcf85f6baa016a27764022969adea0c36c1f4c12e5 . +_:cb1def0490d3233d18bbcdc75ea440636f2bbe33bf0d9fd842e9ac6c85d56782eb1 . +_:cb1def0490d3233d18bbcdc75ea440636f2bbe33bf0d9fd842e9ac6c85d56782eb1 . +_:cb1def0490d3233d18bbcdc75ea440636f2bbe33bf0d9fd842e9ac6c85d56782eb1 . +_:cb1e3ca21d885a6aa9198c7e0da80bb8945cc88e60fa7a050b328feea3ef8631fda . +_:cb1e3ca21d885a6aa9198c7e0da80bb8945cc88e60fa7a050b328feea3ef8631fda "1"^^ . +_:cb1e3ca21d885a6aa9198c7e0da80bb8945cc88e60fa7a050b328feea3ef8631fda . +_:cb1f249ba3e141c783753ca7f6bc46c4ce0ddb60ded99080b4958fc47b2e65e1e5a . +_:cb1f249ba3e141c783753ca7f6bc46c4ce0ddb60ded99080b4958fc47b2e65e1e5a "0"^^ . +_:cb1f249ba3e141c783753ca7f6bc46c4ce0ddb60ded99080b4958fc47b2e65e1e5a . +_:cb2005559eb2b21a3c9355da151f33d642e4af7b0d372076ce53935aec788fa6f67 . +_:cb2005559eb2b21a3c9355da151f33d642e4af7b0d372076ce53935aec788fa6f67 "0"^^ . +_:cb2005559eb2b21a3c9355da151f33d642e4af7b0d372076ce53935aec788fa6f67 . +_:cb223ace63a5d4c89c0969a4be05e62755cf1328abcf1fc6fdf5e09256462f70a2b . +_:cb223ace63a5d4c89c0969a4be05e62755cf1328abcf1fc6fdf5e09256462f70a2b . +_:cb223ace63a5d4c89c0969a4be05e62755cf1328abcf1fc6fdf5e09256462f70a2b . +_:cb2251a2cc284802be192c79b4968c5e3e0d0a6310ef22b3c5b7486c1426634bfd4 . +_:cb2251a2cc284802be192c79b4968c5e3e0d0a6310ef22b3c5b7486c1426634bfd4 . +_:cb2251a2cc284802be192c79b4968c5e3e0d0a6310ef22b3c5b7486c1426634bfd4 . +_:cb2280f6fd5f67e623a7774cdf132efc33d1f1888d2169c7cdabd52865033586c09 . +_:cb2280f6fd5f67e623a7774cdf132efc33d1f1888d2169c7cdabd52865033586c09 . +_:cb2280f6fd5f67e623a7774cdf132efc33d1f1888d2169c7cdabd52865033586c09 . +_:cb22de6a488b77633e4141164ae4ac6e8668dba8585b379d3b296f4a4890f7cce23 . +_:cb22de6a488b77633e4141164ae4ac6e8668dba8585b379d3b296f4a4890f7cce23 "1"^^ . +_:cb22de6a488b77633e4141164ae4ac6e8668dba8585b379d3b296f4a4890f7cce23 . +_:cb23b1f6eed54b7e50afd4742d834d2c517acf217864b10b109eee245892d46cb49 . +_:cb23b1f6eed54b7e50afd4742d834d2c517acf217864b10b109eee245892d46cb49 "0"^^ . +_:cb23b1f6eed54b7e50afd4742d834d2c517acf217864b10b109eee245892d46cb49 . +_:cb23bf24435ce7b5f75f5a486947997ffb3fafc286b8c79354e772e0b9db2191f30 . +_:cb23bf24435ce7b5f75f5a486947997ffb3fafc286b8c79354e772e0b9db2191f30 "1"^^ . +_:cb23bf24435ce7b5f75f5a486947997ffb3fafc286b8c79354e772e0b9db2191f30 . +_:cb23e14b200c6b61b63e1f4757ffefca473fb646f496f81f18937ae0a96fa6a777e . +_:cb23e14b200c6b61b63e1f4757ffefca473fb646f496f81f18937ae0a96fa6a777e "0"^^ . +_:cb23e14b200c6b61b63e1f4757ffefca473fb646f496f81f18937ae0a96fa6a777e . +_:cb2437057497c9a38863099ba911a51798d2811dd43bf7614a1a792873f19481ee8 . +_:cb2437057497c9a38863099ba911a51798d2811dd43bf7614a1a792873f19481ee8 . +_:cb2437057497c9a38863099ba911a51798d2811dd43bf7614a1a792873f19481ee8 . +_:cb24a85419af6cf0d15e7bae94c471eaf0632069a8e2af85f5833415d38d3e2a7c1 . +_:cb24a85419af6cf0d15e7bae94c471eaf0632069a8e2af85f5833415d38d3e2a7c1 . +_:cb24a85419af6cf0d15e7bae94c471eaf0632069a8e2af85f5833415d38d3e2a7c1 . +_:cb266ac9ae64a083e9a8801a1402dd3b934dad4729f78a3a400cdb27cd817aefd45 . +_:cb266ac9ae64a083e9a8801a1402dd3b934dad4729f78a3a400cdb27cd817aefd45 . +_:cb266ac9ae64a083e9a8801a1402dd3b934dad4729f78a3a400cdb27cd817aefd45 . +_:cb2766e399d6d56b5602562bcdfa8bcade0989eca8ed336f11b67f3c7ad7edf0c62 . +_:cb2766e399d6d56b5602562bcdfa8bcade0989eca8ed336f11b67f3c7ad7edf0c62 "0"^^ . +_:cb2766e399d6d56b5602562bcdfa8bcade0989eca8ed336f11b67f3c7ad7edf0c62 . +_:cb276bc5937f811a0b7bd8e281abb2d609d5cf68f1e658279732cdaa25f56657b12 . +_:cb276bc5937f811a0b7bd8e281abb2d609d5cf68f1e658279732cdaa25f56657b12 "1"^^ . +_:cb276bc5937f811a0b7bd8e281abb2d609d5cf68f1e658279732cdaa25f56657b12 . +_:cb279b19c4b6a0fd710a23b5ac285573ff9ab68e6e189f3b9f275a6676d23892747 . +_:cb279b19c4b6a0fd710a23b5ac285573ff9ab68e6e189f3b9f275a6676d23892747 "1"^^ . +_:cb279b19c4b6a0fd710a23b5ac285573ff9ab68e6e189f3b9f275a6676d23892747 . +_:cb27a11dfb9f0596012dfa11a2e90982fcd3f563fae377f0b54b2c25a8d5fd2b833 . +_:cb27a11dfb9f0596012dfa11a2e90982fcd3f563fae377f0b54b2c25a8d5fd2b833 . +_:cb27a11dfb9f0596012dfa11a2e90982fcd3f563fae377f0b54b2c25a8d5fd2b833 . +_:cb27cb1dd111a3ff7c3f28148cef9e09a6bb7205916d18918af480e011edec108ba . +_:cb27cb1dd111a3ff7c3f28148cef9e09a6bb7205916d18918af480e011edec108ba "0"^^ . +_:cb27cb1dd111a3ff7c3f28148cef9e09a6bb7205916d18918af480e011edec108ba . +_:cb2901721e4c091193c4a20c1bd5ca511041ba22625906480032d1dded426e4c3a8 . +_:cb2901721e4c091193c4a20c1bd5ca511041ba22625906480032d1dded426e4c3a8 "0"^^ . +_:cb2901721e4c091193c4a20c1bd5ca511041ba22625906480032d1dded426e4c3a8 . +_:cb2a3227bee2c818a432f61adb96aa923b9f4dccb58093bf6fc5490945bc59586f7 . +_:cb2a3227bee2c818a432f61adb96aa923b9f4dccb58093bf6fc5490945bc59586f7 "0"^^ . +_:cb2a3227bee2c818a432f61adb96aa923b9f4dccb58093bf6fc5490945bc59586f7 . +_:cb2b84ec75bbd99b370b2c82e11803b35f16724d0aeebfae11886065df507dfb883 . +_:cb2b84ec75bbd99b370b2c82e11803b35f16724d0aeebfae11886065df507dfb883 "1"^^ . +_:cb2b84ec75bbd99b370b2c82e11803b35f16724d0aeebfae11886065df507dfb883 . +_:cb2ca4bb2d727b7c18cdb247369b15b04322625d0ebc5721da8bb8c6e344edaf78b . +_:cb2ca4bb2d727b7c18cdb247369b15b04322625d0ebc5721da8bb8c6e344edaf78b . +_:cb2ca4bb2d727b7c18cdb247369b15b04322625d0ebc5721da8bb8c6e344edaf78b . +_:cb2cb4e1fec6eb5699f09380fe4a8fc707d2e8560f57a574cd07f0ad44fd17a26a6 . +_:cb2cb4e1fec6eb5699f09380fe4a8fc707d2e8560f57a574cd07f0ad44fd17a26a6 . +_:cb2cb4e1fec6eb5699f09380fe4a8fc707d2e8560f57a574cd07f0ad44fd17a26a6 . +_:cb2cbb40c2f63ead4e90a67a6ffe2ffac89cba69dbdaad6486c6b163baa50037371 . +_:cb2cbb40c2f63ead4e90a67a6ffe2ffac89cba69dbdaad6486c6b163baa50037371 "1"^^ . +_:cb2cbb40c2f63ead4e90a67a6ffe2ffac89cba69dbdaad6486c6b163baa50037371 . +_:cb2e15362173eed22c873b7b773750951b40ad1476cd33cc17ef9665896988c321b . +_:cb2e15362173eed22c873b7b773750951b40ad1476cd33cc17ef9665896988c321b "0"^^ . +_:cb2e15362173eed22c873b7b773750951b40ad1476cd33cc17ef9665896988c321b . + diff --git a/tests/linkml/test_issues/__snapshots__/linkml_issue_384.py b/tests/linkml/test_issues/__snapshots__/linkml_issue_384.py deleted file mode 100644 index 884aea1975..0000000000 --- a/tests/linkml/test_issues/__snapshots__/linkml_issue_384.py +++ /dev/null @@ -1,242 +0,0 @@ -# Auto generated from linkml_issue_384.yaml by pythongen.py version: 0.0.1 -# Generation date: 2000-01-01T00:00:00 -# Schema: personinfo -# -# id: https://w3id.org/linkml/examples/personinfo -# description: -# license: https://creativecommons.org/publicdomain/zero/1.0/ - -import dataclasses -import re -from dataclasses import dataclass -from datetime import ( - date, - datetime, - time -) -from typing import ( - Any, - ClassVar, - Dict, - List, - Optional, - Union -) - -from jsonasobj2 import ( - JsonObj, - as_dict -) -from linkml_runtime.linkml_model.meta import ( - EnumDefinition, - PermissibleValue, - PvFormulaOptions -) -from linkml_runtime.utils.curienamespace import CurieNamespace -from linkml_runtime.utils.enumerations import EnumDefinitionImpl -from linkml_runtime.utils.formatutils import ( - camelcase, - sfx, - underscore -) -from linkml_runtime.utils.metamodelcore import ( - bnode, - empty_dict, - empty_list -) -from linkml_runtime.utils.slot import Slot -from linkml_runtime.utils.yamlutils import ( - YAMLRoot, - extended_float, - extended_int, - extended_str -) -from rdflib import ( - Namespace, - URIRef -) - -from linkml_runtime.linkml_model.types import Float, Integer, String - -metamodel_version = "1.7.0" -version = None - -# Namespaces -EX = CurieNamespace('ex', 'https://w3id.org/linkml/examples/personinfo/') -LINKML = CurieNamespace('linkml', 'https://w3id.org/linkml/') -SDO = CurieNamespace('sdo', 'http://schema.org/') -DEFAULT_ = EX - - -# Types - -# Class references - - - -@dataclass(repr=False) -class Thing(YAMLRoot): - _inherited_slots: ClassVar[list[str]] = [] - - class_class_uri: ClassVar[URIRef] = EX["Thing"] - class_class_curie: ClassVar[str] = "ex:Thing" - class_name: ClassVar[str] = "Thing" - class_model_uri: ClassVar[URIRef] = EX.Thing - - id: Optional[str] = None - full_name: Optional[str] = None - - def __post_init__(self, *_: str, **kwargs: Any): - if self.id is not None and not isinstance(self.id, str): - self.id = str(self.id) - - if self.full_name is not None and not isinstance(self.full_name, str): - self.full_name = str(self.full_name) - - super().__post_init__(**kwargs) - - -@dataclass(repr=False) -class Person(Thing): - _inherited_slots: ClassVar[list[str]] = [] - - class_class_uri: ClassVar[URIRef] = SDO["Person"] - class_class_curie: ClassVar[str] = "sdo:Person" - class_name: ClassVar[str] = "Person" - class_model_uri: ClassVar[URIRef] = EX.Person - - aliases: Optional[Union[str, list[str]]] = empty_list() - phone: Optional[str] = None - age: Optional[int] = None - parent: Optional[Union[Union[dict, "Person"], list[Union[dict, "Person"]]]] = empty_list() - - def __post_init__(self, *_: str, **kwargs: Any): - if not isinstance(self.aliases, list): - self.aliases = [self.aliases] if self.aliases is not None else [] - self.aliases = [v if isinstance(v, str) else str(v) for v in self.aliases] - - if self.phone is not None and not isinstance(self.phone, str): - self.phone = str(self.phone) - - if self.age is not None and not isinstance(self.age, int): - self.age = int(self.age) - - if not isinstance(self.parent, list): - self.parent = [self.parent] if self.parent is not None else [] - self.parent = [v if isinstance(v, Person) else Person(**as_dict(v)) for v in self.parent] - - super().__post_init__(**kwargs) - - -@dataclass(repr=False) -class Organization(Thing): - _inherited_slots: ClassVar[list[str]] = [] - - class_class_uri: ClassVar[URIRef] = EX["Organization"] - class_class_curie: ClassVar[str] = "ex:Organization" - class_name: ClassVar[str] = "Organization" - class_model_uri: ClassVar[URIRef] = EX.Organization - - full_name: Optional[str] = None - parent: Optional[Union[Union[dict, "Organization"], list[Union[dict, "Organization"]]]] = empty_list() - - def __post_init__(self, *_: str, **kwargs: Any): - if self.full_name is not None and not isinstance(self.full_name, str): - self.full_name = str(self.full_name) - - if not isinstance(self.parent, list): - self.parent = [self.parent] if self.parent is not None else [] - self.parent = [v if isinstance(v, Organization) else Organization(**as_dict(v)) for v in self.parent] - - super().__post_init__(**kwargs) - - -@dataclass(repr=False) -class GeoObject(Thing): - _inherited_slots: ClassVar[list[str]] = [] - - class_class_uri: ClassVar[URIRef] = EX["GeoObject"] - class_class_curie: ClassVar[str] = "ex:GeoObject" - class_name: ClassVar[str] = "GeoObject" - class_model_uri: ClassVar[URIRef] = EX.GeoObject - - aliases: Optional[str] = None - age: Optional[Union[dict, "GeoAge"]] = None - - def __post_init__(self, *_: str, **kwargs: Any): - if self.aliases is not None and not isinstance(self.aliases, str): - self.aliases = str(self.aliases) - - if self.age is not None and not isinstance(self.age, GeoAge): - self.age = GeoAge(**as_dict(self.age)) - - super().__post_init__(**kwargs) - - -@dataclass(repr=False) -class GeoAge(YAMLRoot): - _inherited_slots: ClassVar[list[str]] = [] - - class_class_uri: ClassVar[URIRef] = EX["GeoAge"] - class_class_curie: ClassVar[str] = "ex:GeoAge" - class_name: ClassVar[str] = "GeoAge" - class_model_uri: ClassVar[URIRef] = EX.GeoAge - - unit: Optional[str] = None - value: Optional[float] = None - - def __post_init__(self, *_: str, **kwargs: Any): - if self.unit is not None and not isinstance(self.unit, str): - self.unit = str(self.unit) - - if self.value is not None and not isinstance(self.value, float): - self.value = float(self.value) - - super().__post_init__(**kwargs) - - -# Enumerations - - -# Slots -class slots: - pass - -slots.id = Slot(uri=EX.id, name="id", curie=EX.curie('id'), - model_uri=EX.id, domain=None, range=Optional[str]) - -slots.full_name = Slot(uri=EX.full_name, name="full_name", curie=EX.curie('full_name'), - model_uri=EX.full_name, domain=None, range=Optional[str]) - -slots.parent = Slot(uri=EX.parent, name="parent", curie=EX.curie('parent'), - model_uri=EX.parent, domain=None, range=Optional[Union[Union[dict, Thing], list[Union[dict, Thing]]]]) - -slots.person__aliases = Slot(uri=EX.aliases, name="person__aliases", curie=EX.curie('aliases'), - model_uri=EX.person__aliases, domain=None, range=Optional[Union[str, list[str]]]) - -slots.person__phone = Slot(uri=EX.phone, name="person__phone", curie=EX.curie('phone'), - model_uri=EX.person__phone, domain=None, range=Optional[str]) - -slots.person__age = Slot(uri=EX.age, name="person__age", curie=EX.curie('age'), - model_uri=EX.person__age, domain=None, range=Optional[int]) - -slots.geoObject__aliases = Slot(uri=EX.aliases, name="geoObject__aliases", curie=EX.curie('aliases'), - model_uri=EX.geoObject__aliases, domain=None, range=Optional[str]) - -slots.geoObject__age = Slot(uri=EX.age, name="geoObject__age", curie=EX.curie('age'), - model_uri=EX.geoObject__age, domain=None, range=Optional[Union[dict, GeoAge]]) - -slots.geoAge__unit = Slot(uri=EX.unit, name="geoAge__unit", curie=EX.curie('unit'), - model_uri=EX.geoAge__unit, domain=None, range=Optional[str]) - -slots.geoAge__value = Slot(uri=EX.value, name="geoAge__value", curie=EX.curie('value'), - model_uri=EX.geoAge__value, domain=None, range=Optional[float]) - -slots.Person_parent = Slot(uri=EX.parent, name="Person_parent", curie=EX.curie('parent'), - model_uri=EX.Person_parent, domain=Person, range=Optional[Union[Union[dict, "Person"], list[Union[dict, "Person"]]]]) - -slots.Organization_full_name = Slot(uri=EX.full_name, name="Organization_full_name", curie=EX.curie('full_name'), - model_uri=EX.Organization_full_name, domain=Organization, range=Optional[str]) - -slots.Organization_parent = Slot(uri=EX.parent, name="Organization_parent", curie=EX.curie('parent'), - model_uri=EX.Organization_parent, domain=Organization, range=Optional[Union[Union[dict, "Organization"], list[Union[dict, "Organization"]]]]) diff --git a/tests/linkml/test_issues/__snapshots__/linkml_issue_384.ttl b/tests/linkml/test_issues/__snapshots__/linkml_issue_384.ttl deleted file mode 100644 index 6e05d8a16d..0000000000 --- a/tests/linkml/test_issues/__snapshots__/linkml_issue_384.ttl +++ /dev/null @@ -1,476 +0,0 @@ -@prefix dcterms: . -@prefix linkml: . -@prefix sh: . -@prefix skos: . -@prefix xsd: . - - a linkml:SchemaDefinition ; - dcterms:license "https://creativecommons.org/publicdomain/zero/1.0/" ; - sh:declare [ sh:namespace "https://w3id.org/linkml/examples/personinfo/"^^xsd:anyURI ; - sh:prefix "ex" ], - [ sh:namespace "http://schema.org/"^^xsd:anyURI ; - sh:prefix "sdo" ], - [ sh:namespace "https://w3id.org/linkml/"^^xsd:anyURI ; - sh:prefix "linkml" ] ; - linkml:classes , - , - , - , - ; - linkml:default_prefix "ex" ; - linkml:default_range ; - linkml:generation_date "2000-01-01T00:00:00"^^xsd:dateTime ; - linkml:id "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - linkml:imports "linkml:types"^^xsd:anyURI ; - linkml:metamodel_version "1.7.0" ; - linkml:slots , - , - , - , - , - , - , - , - , - , - , - , - ; - linkml:source_file "linkml_issue_384.yaml" ; - linkml:source_file_date "2000-01-01T00:00:00"^^xsd:dateTime ; - linkml:source_file_size 1 ; - linkml:types , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - . - - a linkml:TypeDefinition ; - skos:editorialNote "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"boolean\"." ; - skos:exactMatch "schema:Boolean"^^xsd:anyURI ; - skos:inScheme "https://w3id.org/linkml/types"^^xsd:anyURI ; - linkml:base "Bool" ; - linkml:definition_uri "https://w3id.org/linkml/Boolean"^^xsd:anyURI ; - linkml:description "A binary (true or false) value" ; - linkml:imported_from "linkml:types" ; - linkml:repr "bool" ; - linkml:uri "http://www.w3.org/2001/XMLSchema#boolean"^^xsd:anyURI . - - a linkml:TypeDefinition ; - dcterms:conformsTo "https://www.w3.org/TR/curie/" ; - skos:editorialNote "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"curie\"." ; - skos:inScheme "https://w3id.org/linkml/types"^^xsd:anyURI ; - skos:note "in RDF serializations this MUST be expanded to a URI", - "in non-RDF serializations MAY be serialized as the compact representation" ; - linkml:base "Curie" ; - linkml:definition_uri "https://w3id.org/linkml/Curie"^^xsd:anyURI ; - linkml:description "a compact URI" ; - linkml:imported_from "linkml:types" ; - linkml:repr "str" ; - linkml:uri "http://www.w3.org/2001/XMLSchema#string"^^xsd:anyURI . - - a linkml:TypeDefinition ; - skos:editorialNote "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"date\".", - "URI is dateTime because OWL reasoners don't work with straight date or time" ; - skos:exactMatch "schema:Date"^^xsd:anyURI ; - skos:inScheme "https://w3id.org/linkml/types"^^xsd:anyURI ; - linkml:base "XSDDate" ; - linkml:definition_uri "https://w3id.org/linkml/Date"^^xsd:anyURI ; - linkml:description "a date (year, month and day) in an idealized calendar" ; - linkml:imported_from "linkml:types" ; - linkml:repr "str" ; - linkml:uri "http://www.w3.org/2001/XMLSchema#date"^^xsd:anyURI . - - a linkml:TypeDefinition ; - skos:editorialNote "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"date_or_datetime\"." ; - skos:inScheme "https://w3id.org/linkml/types"^^xsd:anyURI ; - linkml:base "str" ; - linkml:definition_uri "https://w3id.org/linkml/DateOrDatetime"^^xsd:anyURI ; - linkml:description "Either a date or a datetime" ; - linkml:imported_from "linkml:types" ; - linkml:repr "str" ; - linkml:uri "https://w3id.org/linkml/DateOrDatetime"^^xsd:anyURI . - - a linkml:TypeDefinition ; - skos:editorialNote "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"datetime\"." ; - skos:exactMatch "schema:DateTime"^^xsd:anyURI ; - skos:inScheme "https://w3id.org/linkml/types"^^xsd:anyURI ; - linkml:base "XSDDateTime" ; - linkml:definition_uri "https://w3id.org/linkml/Datetime"^^xsd:anyURI ; - linkml:description "The combination of a date and time" ; - linkml:imported_from "linkml:types" ; - linkml:repr "str" ; - linkml:uri "http://www.w3.org/2001/XMLSchema#dateTime"^^xsd:anyURI . - - a linkml:TypeDefinition ; - skos:broadMatch "schema:Number"^^xsd:anyURI ; - skos:editorialNote "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"decimal\"." ; - skos:inScheme "https://w3id.org/linkml/types"^^xsd:anyURI ; - linkml:base "Decimal" ; - linkml:definition_uri "https://w3id.org/linkml/Decimal"^^xsd:anyURI ; - linkml:description "A real number with arbitrary precision that conforms to the xsd:decimal specification" ; - linkml:imported_from "linkml:types" ; - linkml:uri "http://www.w3.org/2001/XMLSchema#decimal"^^xsd:anyURI . - - a linkml:TypeDefinition ; - skos:closeMatch "schema:Float"^^xsd:anyURI ; - skos:editorialNote "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"double\"." ; - skos:inScheme "https://w3id.org/linkml/types"^^xsd:anyURI ; - linkml:base "float" ; - linkml:definition_uri "https://w3id.org/linkml/Double"^^xsd:anyURI ; - linkml:description "A real number that conforms to the xsd:double specification" ; - linkml:imported_from "linkml:types" ; - linkml:uri "http://www.w3.org/2001/XMLSchema#double"^^xsd:anyURI . - - a linkml:TypeDefinition ; - dcterms:conformsTo "https://www.ietf.org/archive/id/draft-goessner-dispatch-jsonpath-00.html" ; - skos:editorialNote "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"jsonpath\"." ; - skos:inScheme "https://w3id.org/linkml/types"^^xsd:anyURI ; - linkml:base "str" ; - linkml:definition_uri "https://w3id.org/linkml/Jsonpath"^^xsd:anyURI ; - linkml:description "A string encoding a JSON Path. The value of the string MUST conform to JSON Point syntax and SHOULD dereference to zero or more valid objects within the current instance document when encoded in tree form." ; - linkml:imported_from "linkml:types" ; - linkml:repr "str" ; - linkml:uri "http://www.w3.org/2001/XMLSchema#string"^^xsd:anyURI . - - a linkml:TypeDefinition ; - dcterms:conformsTo "https://datatracker.ietf.org/doc/html/rfc6901" ; - skos:editorialNote "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"jsonpointer\"." ; - skos:inScheme "https://w3id.org/linkml/types"^^xsd:anyURI ; - linkml:base "str" ; - linkml:definition_uri "https://w3id.org/linkml/Jsonpointer"^^xsd:anyURI ; - linkml:description "A string encoding a JSON Pointer. The value of the string MUST conform to JSON Point syntax and SHOULD dereference to a valid object within the current instance document when encoded in tree form." ; - linkml:imported_from "linkml:types" ; - linkml:repr "str" ; - linkml:uri "http://www.w3.org/2001/XMLSchema#string"^^xsd:anyURI . - - a linkml:TypeDefinition ; - skos:editorialNote "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"ncname\"." ; - skos:inScheme "https://w3id.org/linkml/types"^^xsd:anyURI ; - linkml:base "NCName" ; - linkml:definition_uri "https://w3id.org/linkml/Ncname"^^xsd:anyURI ; - linkml:description "Prefix part of CURIE" ; - linkml:imported_from "linkml:types" ; - linkml:repr "str" ; - linkml:uri "http://www.w3.org/2001/XMLSchema#string"^^xsd:anyURI . - - a linkml:TypeDefinition ; - skos:editorialNote "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"nodeidentifier\"." ; - skos:inScheme "https://w3id.org/linkml/types"^^xsd:anyURI ; - linkml:base "NodeIdentifier" ; - linkml:definition_uri "https://w3id.org/linkml/Nodeidentifier"^^xsd:anyURI ; - linkml:description "A URI, CURIE or BNODE that represents a node in a model." ; - linkml:imported_from "linkml:types" ; - linkml:repr "str" ; - linkml:uri "http://www.w3.org/ns/shex#nonLiteral"^^xsd:anyURI . - - a linkml:TypeDefinition ; - skos:editorialNote "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"objectidentifier\"." ; - skos:inScheme "https://w3id.org/linkml/types"^^xsd:anyURI ; - skos:note "Used for inheritance and type checking" ; - linkml:base "ElementIdentifier" ; - linkml:definition_uri "https://w3id.org/linkml/Objectidentifier"^^xsd:anyURI ; - linkml:description "A URI or CURIE that represents an object in the model." ; - linkml:imported_from "linkml:types" ; - linkml:repr "str" ; - linkml:uri "http://www.w3.org/ns/shex#iri"^^xsd:anyURI . - - a linkml:SlotDefinition . - - a linkml:TypeDefinition ; - dcterms:conformsTo "https://www.w3.org/TR/sparql11-query/#propertypaths" ; - skos:editorialNote "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"sparqlpath\"." ; - skos:inScheme "https://w3id.org/linkml/types"^^xsd:anyURI ; - linkml:base "str" ; - linkml:definition_uri "https://w3id.org/linkml/Sparqlpath"^^xsd:anyURI ; - linkml:description "A string encoding a SPARQL Property Path. The value of the string MUST conform to SPARQL syntax and SHOULD dereference to zero or more valid objects within the current instance document when encoded as RDF." ; - linkml:imported_from "linkml:types" ; - linkml:repr "str" ; - linkml:uri "http://www.w3.org/2001/XMLSchema#string"^^xsd:anyURI . - - a linkml:TypeDefinition ; - skos:editorialNote "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"time\".", - "URI is dateTime because OWL reasoners do not work with straight date or time" ; - skos:exactMatch "schema:Time"^^xsd:anyURI ; - skos:inScheme "https://w3id.org/linkml/types"^^xsd:anyURI ; - linkml:base "XSDTime" ; - linkml:definition_uri "https://w3id.org/linkml/Time"^^xsd:anyURI ; - linkml:description "A time object represents a (local) time of day, independent of any particular day" ; - linkml:imported_from "linkml:types" ; - linkml:repr "str" ; - linkml:uri "http://www.w3.org/2001/XMLSchema#time"^^xsd:anyURI . - - a linkml:SlotDefinition . - - a linkml:TypeDefinition ; - dcterms:conformsTo "https://www.ietf.org/rfc/rfc3987.txt" ; - skos:closeMatch "schema:URL"^^xsd:anyURI ; - skos:editorialNote "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"uri\"." ; - skos:inScheme "https://w3id.org/linkml/types"^^xsd:anyURI ; - skos:note "in RDF serializations a slot with range of uri is treated as a literal or type xsd:anyURI unless it is an identifier or a reference to an identifier, in which case it is translated directly to a node" ; - linkml:base "URI" ; - linkml:definition_uri "https://w3id.org/linkml/Uri"^^xsd:anyURI ; - linkml:description "a complete URI" ; - linkml:imported_from "linkml:types" ; - linkml:repr "str" ; - linkml:uri "http://www.w3.org/2001/XMLSchema#anyURI"^^xsd:anyURI . - - a linkml:TypeDefinition ; - skos:editorialNote "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"uriorcurie\"." ; - skos:inScheme "https://w3id.org/linkml/types"^^xsd:anyURI ; - linkml:base "URIorCURIE" ; - linkml:definition_uri "https://w3id.org/linkml/Uriorcurie"^^xsd:anyURI ; - linkml:description "a URI or a CURIE" ; - linkml:imported_from "linkml:types" ; - linkml:repr "str" ; - linkml:uri "http://www.w3.org/2001/XMLSchema#anyURI"^^xsd:anyURI . - - a linkml:SlotDefinition ; - linkml:range . - - a linkml:SlotDefinition ; - dcterms:title "full name" ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - skos:prefLabel "full_name" ; - linkml:definition_uri "https://w3id.org/linkml/examples/personinfo/full_name"^^xsd:anyURI ; - linkml:description "name of the organization, e.g. ACME inc" ; - linkml:domain ; - linkml:domain_of ; - linkml:is_a ; - linkml:is_usage_slot true ; - linkml:owner ; - linkml:range ; - linkml:slot_uri "https://w3id.org/linkml/examples/personinfo/full_name"^^xsd:anyURI ; - linkml:usage_slot_name "full_name" . - - a linkml:SlotDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - skos:prefLabel "parent" ; - linkml:definition_uri "https://w3id.org/linkml/examples/personinfo/parent"^^xsd:anyURI ; - linkml:domain ; - linkml:domain_of ; - linkml:inlined true ; - linkml:inlined_as_list true ; - linkml:is_a ; - linkml:is_usage_slot true ; - linkml:multivalued true ; - linkml:owner ; - linkml:range ; - linkml:slot_uri "https://w3id.org/linkml/examples/personinfo/parent"^^xsd:anyURI ; - linkml:usage_slot_name "parent" . - - a linkml:SlotDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - skos:prefLabel "parent" ; - linkml:definition_uri "https://w3id.org/linkml/examples/personinfo/parent"^^xsd:anyURI ; - linkml:domain ; - linkml:domain_of ; - linkml:inlined true ; - linkml:inlined_as_list true ; - linkml:is_a ; - linkml:is_usage_slot true ; - linkml:multivalued true ; - linkml:owner ; - linkml:range ; - linkml:slot_uri "https://w3id.org/linkml/examples/personinfo/parent"^^xsd:anyURI ; - linkml:usage_slot_name "parent" . - - a linkml:SlotDefinition ; - skos:note "we introduce a deliberate conflict (type vs class range) with the age attribute that is local to person" ; - linkml:description "age in years" ; - linkml:range , - . - - a linkml:SlotDefinition ; - skos:note "we introduce a deliberate conflict (single vs multivalied) with the aliases attribute that is local to person" ; - linkml:multivalued true . - - a linkml:SlotDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - skos:prefLabel "unit" ; - linkml:domain_of ; - linkml:owner ; - linkml:range ; - linkml:slot_uri "https://w3id.org/linkml/examples/personinfo/unit"^^xsd:anyURI . - - a linkml:SlotDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - skos:prefLabel "value" ; - linkml:domain_of ; - linkml:owner ; - linkml:range ; - linkml:slot_uri "https://w3id.org/linkml/examples/personinfo/value"^^xsd:anyURI . - - a linkml:SlotDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - skos:note "we introduce a deliberate conflict (type vs class range) with the age attribute that is local to person" ; - skos:prefLabel "age" ; - linkml:domain_of ; - linkml:inlined true ; - linkml:inlined_as_list true ; - linkml:owner ; - linkml:range ; - linkml:slot_uri "https://w3id.org/linkml/examples/personinfo/age"^^xsd:anyURI . - - a linkml:SlotDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - skos:note "we introduce a deliberate conflict (single vs multivalied) with the aliases attribute that is local to person" ; - skos:prefLabel "aliases" ; - linkml:domain_of ; - linkml:owner ; - linkml:range ; - linkml:slot_uri "https://w3id.org/linkml/examples/personinfo/aliases"^^xsd:anyURI . - - a linkml:SlotDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - skos:prefLabel "age" ; - linkml:description "age in years" ; - linkml:domain_of ; - linkml:owner ; - linkml:range ; - linkml:slot_uri "https://w3id.org/linkml/examples/personinfo/age"^^xsd:anyURI . - - a linkml:SlotDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - skos:prefLabel "aliases" ; - linkml:domain_of ; - linkml:multivalued true ; - linkml:owner ; - linkml:range ; - linkml:slot_uri "https://w3id.org/linkml/examples/personinfo/aliases"^^xsd:anyURI . - - a linkml:SlotDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - skos:prefLabel "phone" ; - linkml:domain_of ; - linkml:owner ; - linkml:range ; - linkml:slot_uri "https://w3id.org/linkml/examples/personinfo/phone"^^xsd:anyURI . - - a linkml:TypeDefinition ; - skos:editorialNote "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"float\"." ; - skos:exactMatch "schema:Float"^^xsd:anyURI ; - skos:inScheme "https://w3id.org/linkml/types"^^xsd:anyURI ; - linkml:base "float" ; - linkml:definition_uri "https://w3id.org/linkml/Float"^^xsd:anyURI ; - linkml:description "A real number that conforms to the xsd:float specification" ; - linkml:imported_from "linkml:types" ; - linkml:uri "http://www.w3.org/2001/XMLSchema#float"^^xsd:anyURI . - - a linkml:TypeDefinition ; - skos:editorialNote "If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"integer\"." ; - skos:exactMatch "schema:Integer"^^xsd:anyURI ; - skos:inScheme "https://w3id.org/linkml/types"^^xsd:anyURI ; - linkml:base "int" ; - linkml:definition_uri "https://w3id.org/linkml/Integer"^^xsd:anyURI ; - linkml:description "An integer" ; - linkml:imported_from "linkml:types" ; - linkml:uri "http://www.w3.org/2001/XMLSchema#integer"^^xsd:anyURI . - - a linkml:SlotDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - linkml:definition_uri "https://w3id.org/linkml/examples/personinfo/parent"^^xsd:anyURI ; - linkml:inlined true ; - linkml:inlined_as_list true ; - linkml:multivalued true ; - linkml:range ; - linkml:slot_uri "https://w3id.org/linkml/examples/personinfo/parent"^^xsd:anyURI . - - a linkml:ClassDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - linkml:attributes , - ; - linkml:class_uri "https://w3id.org/linkml/examples/personinfo/GeoObject"^^xsd:anyURI ; - linkml:definition_uri "https://w3id.org/linkml/examples/personinfo/GeoObject"^^xsd:anyURI ; - linkml:is_a ; - linkml:slot_usage [ ] ; - linkml:slots , - , - , - . - - a linkml:SlotDefinition ; - dcterms:title "full name" ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - linkml:definition_uri "https://w3id.org/linkml/examples/personinfo/full_name"^^xsd:anyURI ; - linkml:domain_of ; - linkml:owner ; - linkml:range ; - linkml:slot_uri "https://w3id.org/linkml/examples/personinfo/full_name"^^xsd:anyURI . - - a linkml:SlotDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - linkml:definition_uri "https://w3id.org/linkml/examples/personinfo/id"^^xsd:anyURI ; - linkml:domain_of ; - linkml:owner ; - linkml:range ; - linkml:slot_uri "https://w3id.org/linkml/examples/personinfo/id"^^xsd:anyURI . - - a linkml:ClassDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - linkml:attributes , - ; - linkml:class_uri "https://w3id.org/linkml/examples/personinfo/GeoAge"^^xsd:anyURI ; - linkml:definition_uri "https://w3id.org/linkml/examples/personinfo/GeoAge"^^xsd:anyURI ; - linkml:slot_usage [ ] ; - linkml:slots , - . - - a linkml:ClassDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - linkml:class_uri "https://w3id.org/linkml/examples/personinfo/Organization"^^xsd:anyURI ; - linkml:definition_uri "https://w3id.org/linkml/examples/personinfo/Organization"^^xsd:anyURI ; - linkml:is_a ; - linkml:slot_usage [ ] ; - linkml:slots , - , - . - - a linkml:ClassDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - linkml:class_uri "https://w3id.org/linkml/examples/personinfo/Thing"^^xsd:anyURI ; - linkml:definition_uri "https://w3id.org/linkml/examples/personinfo/Thing"^^xsd:anyURI ; - linkml:slot_usage [ ] ; - linkml:slots , - . - - a linkml:TypeDefinition ; - skos:editorialNote "In RDF serializations, a slot with range of string is treated as a literal or type xsd:string. If you are authoring schemas in LinkML YAML, the type is referenced with the lower case \"string\"." ; - skos:exactMatch "schema:Text"^^xsd:anyURI ; - skos:inScheme "https://w3id.org/linkml/types"^^xsd:anyURI ; - linkml:base "str" ; - linkml:definition_uri "https://w3id.org/linkml/String"^^xsd:anyURI ; - linkml:description "A character string" ; - linkml:imported_from "linkml:types" ; - linkml:uri "http://www.w3.org/2001/XMLSchema#string"^^xsd:anyURI . - - a linkml:ClassDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - skos:mappingRelation "sdo:Person"^^xsd:anyURI ; - linkml:attributes , - , - ; - linkml:class_uri "http://schema.org/Person"^^xsd:anyURI ; - linkml:definition_uri "https://w3id.org/linkml/examples/personinfo/Person"^^xsd:anyURI ; - linkml:is_a ; - linkml:slot_usage [ ] ; - linkml:slots , - , - , - , - , - . diff --git a/tests/linkml/test_issues/__snapshots__/linkml_issue_384.yaml b/tests/linkml/test_issues/__snapshots__/linkml_issue_384.yaml deleted file mode 100644 index 13effa93ce..0000000000 --- a/tests/linkml/test_issues/__snapshots__/linkml_issue_384.yaml +++ /dev/null @@ -1,537 +0,0 @@ -name: personinfo -id: https://w3id.org/linkml/examples/personinfo -imports: -- linkml:types -license: https://creativecommons.org/publicdomain/zero/1.0/ -prefixes: - linkml: - prefix_prefix: linkml - prefix_reference: https://w3id.org/linkml/ - sdo: - prefix_prefix: sdo - prefix_reference: http://schema.org/ - ex: - prefix_prefix: ex - prefix_reference: https://w3id.org/linkml/examples/personinfo/ -default_prefix: ex -default_range: string -types: - string: - name: string - definition_uri: https://w3id.org/linkml/String - description: A character string - notes: - - In RDF serializations, a slot with range of string is treated as a literal or - type xsd:string. If you are authoring schemas in LinkML YAML, the type is referenced - with the lower case "string". - from_schema: https://w3id.org/linkml/types - imported_from: linkml:types - exact_mappings: - - schema:Text - base: str - uri: xsd:string - integer: - name: integer - definition_uri: https://w3id.org/linkml/Integer - description: An integer - notes: - - If you are authoring schemas in LinkML YAML, the type is referenced with the - lower case "integer". - from_schema: https://w3id.org/linkml/types - imported_from: linkml:types - exact_mappings: - - schema:Integer - base: int - uri: xsd:integer - boolean: - name: boolean - definition_uri: https://w3id.org/linkml/Boolean - description: A binary (true or false) value - notes: - - If you are authoring schemas in LinkML YAML, the type is referenced with the - lower case "boolean". - from_schema: https://w3id.org/linkml/types - imported_from: linkml:types - exact_mappings: - - schema:Boolean - base: Bool - uri: xsd:boolean - repr: bool - float: - name: float - definition_uri: https://w3id.org/linkml/Float - description: A real number that conforms to the xsd:float specification - notes: - - If you are authoring schemas in LinkML YAML, the type is referenced with the - lower case "float". - from_schema: https://w3id.org/linkml/types - imported_from: linkml:types - exact_mappings: - - schema:Float - base: float - uri: xsd:float - double: - name: double - definition_uri: https://w3id.org/linkml/Double - description: A real number that conforms to the xsd:double specification - notes: - - If you are authoring schemas in LinkML YAML, the type is referenced with the - lower case "double". - from_schema: https://w3id.org/linkml/types - imported_from: linkml:types - close_mappings: - - schema:Float - base: float - uri: xsd:double - decimal: - name: decimal - definition_uri: https://w3id.org/linkml/Decimal - description: A real number with arbitrary precision that conforms to the xsd:decimal - specification - notes: - - If you are authoring schemas in LinkML YAML, the type is referenced with the - lower case "decimal". - from_schema: https://w3id.org/linkml/types - imported_from: linkml:types - broad_mappings: - - schema:Number - base: Decimal - uri: xsd:decimal - time: - name: time - definition_uri: https://w3id.org/linkml/Time - description: A time object represents a (local) time of day, independent of any - particular day - notes: - - URI is dateTime because OWL reasoners do not work with straight date or time - - If you are authoring schemas in LinkML YAML, the type is referenced with the - lower case "time". - from_schema: https://w3id.org/linkml/types - imported_from: linkml:types - exact_mappings: - - schema:Time - base: XSDTime - uri: xsd:time - repr: str - date: - name: date - definition_uri: https://w3id.org/linkml/Date - description: a date (year, month and day) in an idealized calendar - notes: - - URI is dateTime because OWL reasoners don't work with straight date or time - - If you are authoring schemas in LinkML YAML, the type is referenced with the - lower case "date". - from_schema: https://w3id.org/linkml/types - imported_from: linkml:types - exact_mappings: - - schema:Date - base: XSDDate - uri: xsd:date - repr: str - datetime: - name: datetime - definition_uri: https://w3id.org/linkml/Datetime - description: The combination of a date and time - notes: - - If you are authoring schemas in LinkML YAML, the type is referenced with the - lower case "datetime". - from_schema: https://w3id.org/linkml/types - imported_from: linkml:types - exact_mappings: - - schema:DateTime - base: XSDDateTime - uri: xsd:dateTime - repr: str - date_or_datetime: - name: date_or_datetime - definition_uri: https://w3id.org/linkml/DateOrDatetime - description: Either a date or a datetime - notes: - - If you are authoring schemas in LinkML YAML, the type is referenced with the - lower case "date_or_datetime". - from_schema: https://w3id.org/linkml/types - imported_from: linkml:types - base: str - uri: linkml:DateOrDatetime - repr: str - uriorcurie: - name: uriorcurie - definition_uri: https://w3id.org/linkml/Uriorcurie - description: a URI or a CURIE - notes: - - If you are authoring schemas in LinkML YAML, the type is referenced with the - lower case "uriorcurie". - from_schema: https://w3id.org/linkml/types - imported_from: linkml:types - base: URIorCURIE - uri: xsd:anyURI - repr: str - curie: - name: curie - definition_uri: https://w3id.org/linkml/Curie - conforms_to: https://www.w3.org/TR/curie/ - description: a compact URI - notes: - - If you are authoring schemas in LinkML YAML, the type is referenced with the - lower case "curie". - comments: - - in RDF serializations this MUST be expanded to a URI - - in non-RDF serializations MAY be serialized as the compact representation - from_schema: https://w3id.org/linkml/types - imported_from: linkml:types - base: Curie - uri: xsd:string - repr: str - uri: - name: uri - definition_uri: https://w3id.org/linkml/Uri - conforms_to: https://www.ietf.org/rfc/rfc3987.txt - description: a complete URI - notes: - - If you are authoring schemas in LinkML YAML, the type is referenced with the - lower case "uri". - comments: - - in RDF serializations a slot with range of uri is treated as a literal or type - xsd:anyURI unless it is an identifier or a reference to an identifier, in which - case it is translated directly to a node - from_schema: https://w3id.org/linkml/types - imported_from: linkml:types - close_mappings: - - schema:URL - base: URI - uri: xsd:anyURI - repr: str - ncname: - name: ncname - definition_uri: https://w3id.org/linkml/Ncname - description: Prefix part of CURIE - notes: - - If you are authoring schemas in LinkML YAML, the type is referenced with the - lower case "ncname". - from_schema: https://w3id.org/linkml/types - imported_from: linkml:types - base: NCName - uri: xsd:string - repr: str - objectidentifier: - name: objectidentifier - definition_uri: https://w3id.org/linkml/Objectidentifier - description: A URI or CURIE that represents an object in the model. - notes: - - If you are authoring schemas in LinkML YAML, the type is referenced with the - lower case "objectidentifier". - comments: - - Used for inheritance and type checking - from_schema: https://w3id.org/linkml/types - imported_from: linkml:types - base: ElementIdentifier - uri: shex:iri - repr: str - nodeidentifier: - name: nodeidentifier - definition_uri: https://w3id.org/linkml/Nodeidentifier - description: A URI, CURIE or BNODE that represents a node in a model. - notes: - - If you are authoring schemas in LinkML YAML, the type is referenced with the - lower case "nodeidentifier". - from_schema: https://w3id.org/linkml/types - imported_from: linkml:types - base: NodeIdentifier - uri: shex:nonLiteral - repr: str - jsonpointer: - name: jsonpointer - definition_uri: https://w3id.org/linkml/Jsonpointer - conforms_to: https://datatracker.ietf.org/doc/html/rfc6901 - description: A string encoding a JSON Pointer. The value of the string MUST conform - to JSON Point syntax and SHOULD dereference to a valid object within the current - instance document when encoded in tree form. - notes: - - If you are authoring schemas in LinkML YAML, the type is referenced with the - lower case "jsonpointer". - from_schema: https://w3id.org/linkml/types - imported_from: linkml:types - base: str - uri: xsd:string - repr: str - jsonpath: - name: jsonpath - definition_uri: https://w3id.org/linkml/Jsonpath - conforms_to: https://www.ietf.org/archive/id/draft-goessner-dispatch-jsonpath-00.html - description: A string encoding a JSON Path. The value of the string MUST conform - to JSON Point syntax and SHOULD dereference to zero or more valid objects within - the current instance document when encoded in tree form. - notes: - - If you are authoring schemas in LinkML YAML, the type is referenced with the - lower case "jsonpath". - from_schema: https://w3id.org/linkml/types - imported_from: linkml:types - base: str - uri: xsd:string - repr: str - sparqlpath: - name: sparqlpath - definition_uri: https://w3id.org/linkml/Sparqlpath - conforms_to: https://www.w3.org/TR/sparql11-query/#propertypaths - description: A string encoding a SPARQL Property Path. The value of the string - MUST conform to SPARQL syntax and SHOULD dereference to zero or more valid objects - within the current instance document when encoded as RDF. - notes: - - If you are authoring schemas in LinkML YAML, the type is referenced with the - lower case "sparqlpath". - from_schema: https://w3id.org/linkml/types - imported_from: linkml:types - base: str - uri: xsd:string - repr: str -slots: - id: - name: id - definition_uri: https://w3id.org/linkml/examples/personinfo/id - from_schema: https://w3id.org/linkml/examples/personinfo - slot_uri: ex:id - owner: Thing - domain_of: - - Thing - range: string - full_name: - name: full_name - definition_uri: https://w3id.org/linkml/examples/personinfo/full_name - title: full name - from_schema: https://w3id.org/linkml/examples/personinfo - slot_uri: ex:full_name - owner: Thing - domain_of: - - Thing - range: string - parent: - name: parent - definition_uri: https://w3id.org/linkml/examples/personinfo/parent - from_schema: https://w3id.org/linkml/examples/personinfo - slot_uri: ex:parent - range: Thing - multivalued: true - inlined: true - inlined_as_list: true - person__aliases: - name: person__aliases - from_schema: https://w3id.org/linkml/examples/personinfo - slot_uri: ex:aliases - alias: aliases - owner: Person - domain_of: - - Person - range: string - multivalued: true - person__phone: - name: person__phone - from_schema: https://w3id.org/linkml/examples/personinfo - slot_uri: ex:phone - alias: phone - owner: Person - domain_of: - - Person - range: string - person__age: - name: person__age - description: age in years - from_schema: https://w3id.org/linkml/examples/personinfo - slot_uri: ex:age - alias: age - owner: Person - domain_of: - - Person - range: integer - geoObject__aliases: - name: geoObject__aliases - comments: - - we introduce a deliberate conflict (single vs multivalied) with the aliases - attribute that is local to person - from_schema: https://w3id.org/linkml/examples/personinfo - slot_uri: ex:aliases - alias: aliases - owner: GeoObject - domain_of: - - GeoObject - range: string - multivalued: false - geoObject__age: - name: geoObject__age - comments: - - we introduce a deliberate conflict (type vs class range) with the age attribute - that is local to person - from_schema: https://w3id.org/linkml/examples/personinfo - slot_uri: ex:age - alias: age - owner: GeoObject - domain_of: - - GeoObject - range: GeoAge - inlined: true - inlined_as_list: true - geoAge__unit: - name: geoAge__unit - from_schema: https://w3id.org/linkml/examples/personinfo - slot_uri: ex:unit - alias: unit - owner: GeoAge - domain_of: - - GeoAge - range: string - geoAge__value: - name: geoAge__value - from_schema: https://w3id.org/linkml/examples/personinfo - slot_uri: ex:value - alias: value - owner: GeoAge - domain_of: - - GeoAge - range: float - Person_parent: - name: Person_parent - definition_uri: https://w3id.org/linkml/examples/personinfo/parent - from_schema: https://w3id.org/linkml/examples/personinfo - is_a: parent - domain: Person - slot_uri: ex:parent - alias: parent - owner: Person - domain_of: - - Person - is_usage_slot: true - usage_slot_name: parent - range: Person - multivalued: true - inlined: true - inlined_as_list: true - Organization_full_name: - name: Organization_full_name - definition_uri: https://w3id.org/linkml/examples/personinfo/full_name - description: name of the organization, e.g. ACME inc - title: full name - from_schema: https://w3id.org/linkml/examples/personinfo - is_a: full_name - domain: Organization - slot_uri: ex:full_name - alias: full_name - owner: Organization - domain_of: - - Organization - is_usage_slot: true - usage_slot_name: full_name - range: string - Organization_parent: - name: Organization_parent - definition_uri: https://w3id.org/linkml/examples/personinfo/parent - from_schema: https://w3id.org/linkml/examples/personinfo - is_a: parent - domain: Organization - slot_uri: ex:parent - alias: parent - owner: Organization - domain_of: - - Organization - is_usage_slot: true - usage_slot_name: parent - range: Organization - multivalued: true - inlined: true - inlined_as_list: true -classes: - Thing: - name: Thing - definition_uri: https://w3id.org/linkml/examples/personinfo/Thing - from_schema: https://w3id.org/linkml/examples/personinfo - slots: - - id - - full_name - class_uri: ex:Thing - Person: - name: Person - definition_uri: https://w3id.org/linkml/examples/personinfo/Person - from_schema: https://w3id.org/linkml/examples/personinfo - mappings: - - sdo:Person - is_a: Thing - slots: - - id - - full_name - - person__aliases - - person__phone - - person__age - - Person_parent - slot_usage: - parent: - name: parent - range: Person - attributes: - aliases: - name: aliases - multivalued: true - phone: - name: phone - age: - name: age - description: age in years - range: integer - class_uri: sdo:Person - Organization: - name: Organization - definition_uri: https://w3id.org/linkml/examples/personinfo/Organization - from_schema: https://w3id.org/linkml/examples/personinfo - is_a: Thing - slots: - - id - - Organization_full_name - - Organization_parent - slot_usage: - full_name: - name: full_name - description: name of the organization, e.g. ACME inc - parent: - name: parent - range: Organization - class_uri: ex:Organization - GeoObject: - name: GeoObject - definition_uri: https://w3id.org/linkml/examples/personinfo/GeoObject - from_schema: https://w3id.org/linkml/examples/personinfo - is_a: Thing - slots: - - id - - full_name - - geoObject__aliases - - geoObject__age - attributes: - aliases: - name: aliases - comments: - - we introduce a deliberate conflict (single vs multivalied) with the aliases - attribute that is local to person - multivalued: false - age: - name: age - comments: - - we introduce a deliberate conflict (type vs class range) with the age attribute - that is local to person - range: GeoAge - class_uri: ex:GeoObject - GeoAge: - name: GeoAge - definition_uri: https://w3id.org/linkml/examples/personinfo/GeoAge - from_schema: https://w3id.org/linkml/examples/personinfo - slots: - - geoAge__unit - - geoAge__value - attributes: - unit: - name: unit - value: - name: value - range: float - class_uri: ex:GeoAge -metamodel_version: 1.7.0 -source_file: linkml_issue_384.yaml -source_file_date: '2000-01-01T00:00:00' -source_file_size: 1 -generation_date: '2000-01-01T00:00:00' diff --git a/tests/linkml/test_issues/__snapshots__/linkml_issue_388.owl b/tests/linkml/test_issues/__snapshots__/linkml_issue_388.owl deleted file mode 100644 index 444d8a423a..0000000000 --- a/tests/linkml/test_issues/__snapshots__/linkml_issue_388.owl +++ /dev/null @@ -1,66 +0,0 @@ -@prefix linkml: . -@prefix owl: . -@prefix rdfs: . -@prefix skos: . -@prefix this: . -@prefix xsd: . - -this:C2 a owl:Class ; - rdfs:label "C2" ; - rdfs:subClassOf [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty this:a ], - [ a owl:Restriction ; - owl:allValuesFrom this:my_int ; - owl:onProperty this:a ], - [ a owl:Restriction ; - owl:maxCardinality 1 ; - owl:onProperty this:a ] ; - skos:inScheme . - -this:C3 a owl:Class ; - rdfs:label "C3" ; - rdfs:subClassOf [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty this:a ], - [ a owl:Restriction ; - owl:allValuesFrom this:C1 ; - owl:onProperty this:a ], - [ a owl:Restriction ; - owl:maxCardinality 1 ; - owl:onProperty this:a ] ; - skos:inScheme . - -this:C1 a owl:Class ; - rdfs:label "C1" ; - rdfs:subClassOf [ a owl:Restriction ; - owl:allValuesFrom this:my_str ; - owl:onProperty this:a ], - [ a owl:Restriction ; - owl:maxCardinality 1 ; - owl:onProperty this:a ], - [ a owl:Restriction ; - owl:minCardinality 0 ; - owl:onProperty this:a ] ; - skos:inScheme . - -this:my_int a owl:Class ; - rdfs:subClassOf [ a owl:Restriction ; - owl:onDataRange this:my_int ; - owl:onProperty linkml:topValue ; - owl:qualifiedCardinality 1 ] . - -this:my_str a owl:Class ; - rdfs:subClassOf [ a owl:Restriction ; - owl:onDataRange this:my_str ; - owl:onProperty linkml:topValue ; - owl:qualifiedCardinality 1 ] . - -linkml:topValue a owl:DatatypeProperty ; - rdfs:label "value" . - - a owl:Ontology ; - rdfs:label "personinfo" . - -this:a a owl:ObjectProperty . - diff --git a/tests/linkml/test_issues/__snapshots__/linkml_issue_388.schema.json b/tests/linkml/test_issues/__snapshots__/linkml_issue_388.schema.json deleted file mode 100644 index a82b1ad6d4..0000000000 --- a/tests/linkml/test_issues/__snapshots__/linkml_issue_388.schema.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "$defs": { - "C1": { - "additionalProperties": false, - "description": "", - "properties": { - "a": { - "description": "this-a in the context of C1", - "type": [ - "string", - "null" - ] - } - }, - "title": "C1", - "type": "object" - }, - "C2": { - "additionalProperties": false, - "description": "", - "properties": { - "a": { - "description": "this-a in the context of C2", - "type": [ - "integer", - "null" - ] - } - }, - "title": "C2", - "type": "object" - }, - "C3": { - "additionalProperties": false, - "description": "", - "properties": { - "a": { - "anyOf": [ - { - "$ref": "#/$defs/C1" - }, - { - "type": "null" - } - ], - "description": "other-a in the context of C3" - } - }, - "title": "C3", - "type": "object" - } - }, - "$id": "https://w3id.org/linkml/examples/personinfo", - "$schema": "https://json-schema.org/draft/2019-09/schema", - "additionalProperties": true, - "metamodel_version": "1.7.0", - "title": "personinfo", - "type": "object", - "version": null -} diff --git a/tests/linkml/test_issues/__snapshots__/linkml_issue_388.ttl b/tests/linkml/test_issues/__snapshots__/linkml_issue_388.ttl deleted file mode 100644 index a05ae8a18b..0000000000 --- a/tests/linkml/test_issues/__snapshots__/linkml_issue_388.ttl +++ /dev/null @@ -1,105 +0,0 @@ -@prefix linkml: . -@prefix sh: . -@prefix skos: . -@prefix xsd: . - - a linkml:SchemaDefinition ; - sh:declare [ sh:namespace "https://example.org/this/"^^xsd:anyURI ; - sh:prefix "this" ], - [ sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ; - sh:prefix "xsd" ], - [ sh:namespace "https://example.org/other/"^^xsd:anyURI ; - sh:prefix "other" ], - [ sh:namespace "https://w3id.org/linkml/"^^xsd:anyURI ; - sh:prefix "linkml" ] ; - linkml:classes , - , - ; - linkml:default_prefix "this" ; - linkml:default_range ; - linkml:generation_date "2000-01-01T00:00:00"^^xsd:dateTime ; - linkml:id "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - linkml:metamodel_version "1.7.0" ; - linkml:slots , - , - ; - linkml:source_file "linkml_issue_388.yaml" ; - linkml:source_file_date "2000-01-01T00:00:00"^^xsd:dateTime ; - linkml:source_file_size 1 ; - linkml:types , - . - - a linkml:SlotDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - skos:prefLabel "a" ; - linkml:description "this-a in the context of C1" ; - linkml:domain_of ; - linkml:owner ; - linkml:range ; - linkml:slot_uri "https://example.org/this/a"^^xsd:anyURI . - - a linkml:SlotDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - skos:prefLabel "a" ; - linkml:description "this-a in the context of C2" ; - linkml:domain_of ; - linkml:owner ; - linkml:range ; - linkml:slot_uri "https://example.org/this/a"^^xsd:anyURI . - - a linkml:SlotDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - skos:mappingRelation "https://example.org/other/a"^^xsd:anyURI ; - skos:prefLabel "a" ; - linkml:description "other-a in the context of C3" ; - linkml:domain_of ; - linkml:inlined true ; - linkml:inlined_as_list true ; - linkml:owner ; - linkml:range ; - linkml:slot_uri "https://example.org/other/a"^^xsd:anyURI . - - a linkml:ClassDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - linkml:attributes ; - linkml:class_uri "https://example.org/this/C2"^^xsd:anyURI ; - linkml:definition_uri "https://example.org/this/C2"^^xsd:anyURI ; - linkml:slot_usage [ ] ; - linkml:slots . - - a linkml:ClassDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - linkml:attributes ; - linkml:class_uri "https://example.org/this/C3"^^xsd:anyURI ; - linkml:definition_uri "https://example.org/this/C3"^^xsd:anyURI ; - linkml:slot_usage [ ] ; - linkml:slots . - - a linkml:SlotDefinition ; - linkml:description "other-a in the context of C3", - "this-a in the context of C1", - "this-a in the context of C2" ; - linkml:range , - , - ; - linkml:slot_uri "other:a"^^xsd:anyURI . - - a linkml:TypeDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - linkml:base "integer" ; - linkml:definition_uri "https://example.org/this/MyInt"^^xsd:anyURI ; - linkml:uri "http://www.w3.org/2001/XMLSchema#integer"^^xsd:anyURI . - - a linkml:TypeDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - linkml:base "str" ; - linkml:definition_uri "https://example.org/this/MyStr"^^xsd:anyURI ; - linkml:uri "http://www.w3.org/2001/XMLSchema#string"^^xsd:anyURI . - - a linkml:ClassDefinition ; - skos:inScheme "https://w3id.org/linkml/examples/personinfo"^^xsd:anyURI ; - linkml:attributes ; - linkml:class_uri "https://example.org/this/C1"^^xsd:anyURI ; - linkml:definition_uri "https://example.org/this/C1"^^xsd:anyURI ; - linkml:slot_usage [ ] ; - linkml:slots . diff --git a/tests/linkml/test_issues/__snapshots__/linkml_issue_388.yaml b/tests/linkml/test_issues/__snapshots__/linkml_issue_388.yaml deleted file mode 100644 index efc7ef9bb1..0000000000 --- a/tests/linkml/test_issues/__snapshots__/linkml_issue_388.yaml +++ /dev/null @@ -1,108 +0,0 @@ -name: personinfo -id: https://w3id.org/linkml/examples/personinfo -prefixes: - linkml: - prefix_prefix: linkml - prefix_reference: https://w3id.org/linkml/ - this: - prefix_prefix: this - prefix_reference: https://example.org/this/ - other: - prefix_prefix: other - prefix_reference: https://example.org/other/ - xsd: - prefix_prefix: xsd - prefix_reference: http://www.w3.org/2001/XMLSchema# -default_prefix: this -default_range: my_str -types: - my_str: - name: my_str - definition_uri: https://example.org/this/MyStr - from_schema: https://w3id.org/linkml/examples/personinfo - base: str - uri: xsd:string - my_int: - name: my_int - definition_uri: https://example.org/this/MyInt - from_schema: https://w3id.org/linkml/examples/personinfo - base: integer - uri: xsd:integer -slots: - c1__a: - name: c1__a - description: this-a in the context of C1 - from_schema: https://w3id.org/linkml/examples/personinfo - slot_uri: this:a - alias: a - owner: C1 - domain_of: - - C1 - range: my_str - c2__a: - name: c2__a - description: this-a in the context of C2 - from_schema: https://w3id.org/linkml/examples/personinfo - slot_uri: this:a - alias: a - owner: C2 - domain_of: - - C2 - range: my_int - c3__a: - name: c3__a - description: other-a in the context of C3 - from_schema: https://w3id.org/linkml/examples/personinfo - mappings: - - other:a - slot_uri: other:a - alias: a - owner: C3 - domain_of: - - C3 - range: C1 - inlined: true - inlined_as_list: true -classes: - C1: - name: C1 - definition_uri: https://example.org/this/C1 - from_schema: https://w3id.org/linkml/examples/personinfo - slots: - - c1__a - attributes: - a: - name: a - description: this-a in the context of C1 - range: my_str - class_uri: this:C1 - C2: - name: C2 - definition_uri: https://example.org/this/C2 - from_schema: https://w3id.org/linkml/examples/personinfo - slots: - - c2__a - attributes: - a: - name: a - description: this-a in the context of C2 - range: my_int - class_uri: this:C2 - C3: - name: C3 - definition_uri: https://example.org/this/C3 - from_schema: https://w3id.org/linkml/examples/personinfo - slots: - - c3__a - attributes: - a: - name: a - description: other-a in the context of C3 - slot_uri: other:a - range: C1 - class_uri: this:C3 -metamodel_version: 1.7.0 -source_file: linkml_issue_388.yaml -source_file_date: '2000-01-01T00:00:00' -source_file_size: 1 -generation_date: '2000-01-01T00:00:00' diff --git a/tests/linkml/test_issues/conftest.py b/tests/linkml/test_issues/conftest.py index 9b17d36627..88d07118d9 100644 --- a/tests/linkml/test_issues/conftest.py +++ b/tests/linkml/test_issues/conftest.py @@ -1,4 +1,9 @@ +import json +from collections.abc import Callable, Mapping + import pytest +import rdflib +from rdflib.compare import to_canonical_graph @pytest.fixture @@ -138,3 +143,32 @@ def data_str(): ] } """ + + +def _normalize_snapshot_bundle_output(name: str, output: str) -> str: + """Normalize bundle sections that would otherwise vary in serialization order.""" + if name.endswith((".ttl", ".owl")): + graph = rdflib.Graph() + graph.parse(data=output, format="turtle") + normalized = to_canonical_graph(graph).serialize(format="nt") + return "\n".join(sorted(line for line in normalized.splitlines() if line)) + "\n" + if name.endswith((".json", ".schema.json", ".context.jsonld")): + return json.dumps(json.loads(output), indent=2, sort_keys=True, ensure_ascii=False) + "\n" + return output if output.endswith("\n") else f"{output}\n" + + +def render_snapshot_bundle(outputs: Mapping[str, str]) -> str: + """Render multiple named outputs into one deterministic snapshot string.""" + parts: list[str] = [] + for name, output in outputs.items(): + parts.append(f"# --- {name} ---\n") + parts.append(_normalize_snapshot_bundle_output(name, output)) + parts.append("\n") + return "".join(parts) + + +@pytest.fixture +def bundled_snapshot_text() -> Callable[[Mapping[str, str]], str]: + """Build a labeled single-file snapshot from multiple related outputs.""" + + return render_snapshot_bundle diff --git a/tests/linkml/test_issues/test_curie_prefix_matching.py b/tests/linkml/test_issues/test_curie_prefix_matching.py deleted file mode 100644 index a12d0328c6..0000000000 --- a/tests/linkml/test_issues/test_curie_prefix_matching.py +++ /dev/null @@ -1,22 +0,0 @@ -from linkml.generators.jsonldcontextgen import ContextGenerator -from linkml.generators.yamlgen import YAMLGenerator - - -def test_multi_curies(input_path, snapshot): - schema = input_path("curie_prefix_matching.yaml") - - output = YAMLGenerator(schema).serialize() - assert output == snapshot("curie_prefix_matching.yaml") - - output = ContextGenerator(schema).serialize() - assert output == snapshot("curie_prefix_matching.context.jsonld") - - -def test_curie_case(input_path, snapshot): - schema = input_path("curie_case.yaml") - - output = YAMLGenerator(schema).serialize() - assert output == snapshot("curie_case.yaml") - - output = ContextGenerator(schema).serialize() - assert output == snapshot("curie_case.context.jsonld") diff --git a/tests/linkml/test_issues/test_issue_121.py b/tests/linkml/test_issues/test_issue_121.py deleted file mode 100644 index c67a474e65..0000000000 --- a/tests/linkml/test_issues/test_issue_121.py +++ /dev/null @@ -1,32 +0,0 @@ -import pytest -from jsonasobj2 import as_json - -from linkml.generators.pythongen import PythonGenerator -from linkml_runtime.utils.compile_python import compile_python - - -@pytest.mark.pythongen -def test_issue_121(input_path, snapshot): - """Make sure that types are generated as part of the output""" - python = PythonGenerator(input_path("issue_121.yaml")).serialize() - assert python == snapshot("issue_121.py") - - has_includes = False - for line in python.split("\n"): - if line.startswith("from linkml_runtime.linkml_model.types "): - assert line == "from linkml_runtime.linkml_model.types import String" - has_includes = True - assert has_includes - module = compile_python(python) - - example = module.Biosample(depth="test") - assert hasattr(example, "depth") - assert example.depth == "test" - - example2 = module.ImportedClass() - - example_json = as_json(example) - assert example_json == snapshot("issue_121_1.json") - - example_json_2 = as_json(example2) - assert example_json_2 == snapshot("issue_121_2.json") diff --git a/tests/linkml/test_issues/test_issue_80.py b/tests/linkml/test_issues/test_issue_80.py deleted file mode 100644 index ade69b2bac..0000000000 --- a/tests/linkml/test_issues/test_issue_80.py +++ /dev/null @@ -1,27 +0,0 @@ -import pytest -from jsonasobj2 import as_json - -from linkml.generators.jsonldcontextgen import ContextGenerator -from linkml.generators.pythongen import PythonGenerator -from linkml_runtime.utils.compile_python import compile_python -from linkml_runtime.utils.yamlutils import as_rdf - - -@pytest.mark.jsonldcontextgen -@pytest.mark.pythongen -def test_issue_80(input_path, snapshot): - """Make sure that types are generated as part of the output""" - output = PythonGenerator(input_path("issue_80.yaml")).serialize() - assert output == snapshot("issue_80.py") - - module = compile_python(output) - example = module.Person("http://example.org/person/17", "Fred Jones", 43) - - json_output = as_json(example) - assert json_output == snapshot("issue_80.json") - - jsonld_context_output = ContextGenerator(input_path("issue_80.yaml")).serialize() - assert jsonld_context_output == snapshot("issue_80.context.jsonld") - - rdf_output = as_rdf(example, contexts=jsonld_context_output).serialize(format="turtle") - assert rdf_output == snapshot("issue_80.ttl") diff --git a/tests/linkml/test_issues/test_linkml_issue_384.py b/tests/linkml/test_issues/test_linkml_issue_384.py index b874855b52..2c4b1fae98 100644 --- a/tests/linkml/test_issues/test_linkml_issue_384.py +++ b/tests/linkml/test_issues/test_linkml_issue_384.py @@ -17,23 +17,26 @@ """ -def _test_other(name: str, input_path, snapshot) -> None: +def _other_outputs(name: str, input_path) -> dict[str, str]: infile = input_path(f"{name}.yaml") + outputs: dict[str, str] = {} output = YAMLGenerator(infile).serialize() - assert output == snapshot(f"{name}.yaml") + outputs[f"{name}.yaml"] = output output = ContextGenerator(infile).serialize() - assert output == snapshot(f"{name}.context.jsonld") + outputs[f"{name}.context.jsonld"] = output output = RDFGenerator(infile).serialize(context=[METAMODEL_CONTEXT_URI]) - assert output == snapshot(f"{name}.ttl") + outputs[f"{name}.ttl"] = output output = PythonGenerator(infile).serialize() - assert output == snapshot(f"{name}.py") + outputs[f"{name}.py"] = output + return outputs -def _test_owl(name: str, input_path, snapshot, metaclasses=False, type_objects=False) -> Graph: + +def _test_owl(name: str, input_path, metaclasses=False, type_objects=False) -> tuple[Graph, str, str]: infile = input_path(f"{name}.yaml") outpath = f"{name}-{metaclasses}-{type_objects}.owl" @@ -44,11 +47,10 @@ def _test_owl(name: str, input_path, snapshot, metaclasses=False, type_objects=F metaclasses=metaclasses, type_objects=type_objects, ).serialize(mergeimports=False) - assert output == snapshot(outpath) g = Graph() g.parse(data=output, format="turtle") - return g + return g, outpath, output def _contains_restriction(g: Graph, c: URIRef, prop: URIRef, pred: URIRef, filler: URIRef) -> bool: @@ -60,15 +62,17 @@ def _contains_restriction(g: Graph, c: URIRef, prop: URIRef, pred: URIRef, fille @pytest.mark.owlgen -def test_issue_owl_properties(input_path, snapshot): +def test_issue_owl_properties(input_path, snapshot, bundled_snapshot_text): def uri(s) -> URIRef: return URIRef(f"https://w3id.org/linkml/examples/personinfo/{s}") + outputs: dict[str, str] = {} for conf in [ dict(metaclasses=False, type_objects=False), dict(metaclasses=True, type_objects=True), ]: - g = _test_owl(TESTFILE, input_path, snapshot, **conf) + g, outpath, output = _test_owl(TESTFILE, input_path, **conf) + outputs[outpath] = output Thing = uri("Thing") Person = uri("Person") Organization = uri("Organization") @@ -95,6 +99,8 @@ def uri(s) -> URIRef: # TODO: also validate cardinality restrictions # assert self._contains_restriction(g, Thing, full_name, OWL.allValuesFrom, string_rep) + assert bundled_snapshot_text(outputs) == snapshot(f"{TESTFILE}.owl.txt") + # self.assertIn((A, RDF.type, OWL.Class), g) # NAME = URIRef('http://example.org/name') # self.assertIn((NAME, RDF.type, OWL.ObjectProperty), g) @@ -105,5 +111,5 @@ def uri(s) -> URIRef: @pytest.mark.yamlgen @pytest.mark.pythongen @pytest.mark.network -def test_other_formats(input_path, snapshot): - _test_other(TESTFILE, input_path, snapshot) +def test_other_formats(input_path, snapshot, bundled_snapshot_text): + assert bundled_snapshot_text(_other_outputs(TESTFILE, input_path)) == snapshot(f"{TESTFILE}.other.txt") diff --git a/tests/linkml/test_issues/test_linkml_issue_388.py b/tests/linkml/test_issues/test_linkml_issue_388.py deleted file mode 100644 index fe689837ef..0000000000 --- a/tests/linkml/test_issues/test_linkml_issue_388.py +++ /dev/null @@ -1,43 +0,0 @@ -import pytest -from rdflib import Graph, URIRef - -from linkml import METAMODEL_CONTEXT_URI -from linkml.generators.jsonschemagen import JsonSchemaGenerator -from linkml.generators.owlgen import OwlSchemaGenerator -from linkml.generators.rdfgen import RDFGenerator -from linkml.generators.yamlgen import YAMLGenerator - - -@pytest.mark.owlgen -@pytest.mark.jsonschemagen -@pytest.mark.network -def test_attribute_behavior(input_path, snapshot, snapshot_path): - """ - Tests: https://github.com/linkml/linkml/issues/388 - - Note: this test is currently for exploration, it does not yet do tests beyond ensuring conversion - generates no errors - """ - - name = "linkml_issue_388" - schema = input_path(f"{name}.yaml") - - output = YAMLGenerator(schema).serialize() - assert output == snapshot(f"{name}.yaml") - - output = JsonSchemaGenerator(schema).serialize() - assert output == snapshot(f"{name}.schema.json") - - output = RDFGenerator(schema).serialize(context=[METAMODEL_CONTEXT_URI]) - assert output == snapshot(f"{name}.ttl") - - output = OwlSchemaGenerator(schema, metaclasses=False).serialize(context=[METAMODEL_CONTEXT_URI]) - assert output == snapshot(f"{name}.owl") - - g = Graph() - g.parse(snapshot_path(f"{name}.owl"), format="turtle") - this_a = URIRef("https://example.org/this/a") - URIRef("https://example.org/other/a") - # slot_uri refers to two attributes, ambiguous, so minimal metadata; - assert len(list(g.triples((this_a, None, None)))) == 1 - # assert len(list(g.triples((other_a, None, None)))) > 1 diff --git a/tests/linkml/test_issues/input/curie_case.yaml b/tests/linkml/test_prefixes/input/curie_case.yaml similarity index 100% rename from tests/linkml/test_issues/input/curie_case.yaml rename to tests/linkml/test_prefixes/input/curie_case.yaml diff --git a/tests/linkml/test_issues/input/curie_prefix_matching.yaml b/tests/linkml/test_prefixes/input/curie_prefix_matching.yaml similarity index 100% rename from tests/linkml/test_issues/input/curie_prefix_matching.yaml rename to tests/linkml/test_prefixes/input/curie_prefix_matching.yaml diff --git a/tests/linkml/test_prefixes/test_prefixes.py b/tests/linkml/test_prefixes/test_prefixes.py index e2b07af190..76a56ac82c 100644 --- a/tests/linkml/test_prefixes/test_prefixes.py +++ b/tests/linkml/test_prefixes/test_prefixes.py @@ -4,12 +4,14 @@ import re import pytest +import yaml from rdflib import Graph, URIRef from linkml.generators.jsonldcontextgen import ContextGenerator from linkml.generators.owlgen import OwlSchemaGenerator from linkml.generators.prefixmapgen import PrefixGenerator from linkml.generators.rdfgen import RDFGenerator +from linkml.generators.yamlgen import YAMLGenerator from tests.linkml.test_prefixes.environment import env pytestmark = pytest.mark.xdist_group("prefixes") @@ -189,6 +191,35 @@ def test_jsonldcontext(schema, context_output): assert "ENVO" not in obj +def test_issue_curie_prefix_matching_longest_prefix_wins_for_context_terms(): + schema = env.input_path("curie_prefix_matching.yaml") + + generated_yaml = yaml.safe_load(YAMLGenerator(schema).serialize()) + assert generated_yaml["types"]["t1"]["uri"] == "p1:c/suffix1" + assert generated_yaml["types"]["t2"]["uri"] == "p2:suffix2" + assert generated_yaml["types"]["t3"]["uri"] == "http://example.org/prefixes/a/b/c/suffix3" + + generated_context = json.loads(ContextGenerator(schema).serialize())["@context"] + assert generated_context["C1"]["@id"] == "p1:/c/c1" + assert generated_context["C2"]["@id"] == "p2:/c2" + assert generated_context["C3"]["@id"] == "p2:c3" + + +def test_issue_curie_prefix_matching_prefix_case_is_canonicalized_in_context(): + schema = env.input_path("curie_case.yaml") + + generated_yaml = yaml.safe_load(YAMLGenerator(schema).serialize()) + assert generated_yaml["classes"]["c1"]["class_uri"] == "abc:c1" + assert generated_yaml["classes"]["c2"]["class_uri"] == "ABC:t2" + assert generated_yaml["classes"]["c3"]["class_uri"] == "aBc:t3" + + generated_context = json.loads(ContextGenerator(schema).serialize())["@context"] + assert {generated_context[prefix] for prefix in ("ABC", "aBC", "aBc", "abc")} == {"http://example.org/abc#"} + assert generated_context["C1"]["@id"] == "aBC:c1" + assert generated_context["C2"]["@id"] == "aBC:t2" + assert generated_context["C3"]["@id"] == "aBC:t3" + + def check_triples(g, exceptions=None): """ currently testing is fairly weak: simply checks if the expected expanded URIs are diff --git a/tests/linkml/test_issues/input/issue_80.yaml b/tests/linkml/test_utils/input/issue_80.yaml similarity index 100% rename from tests/linkml/test_issues/input/issue_80.yaml rename to tests/linkml/test_utils/input/issue_80.yaml diff --git a/tests/linkml/test_utils/test_uri_and_curie.py b/tests/linkml/test_utils/test_uri_and_curie.py index acdd4adc92..3dba819c79 100644 --- a/tests/linkml/test_utils/test_uri_and_curie.py +++ b/tests/linkml/test_utils/test_uri_and_curie.py @@ -1,7 +1,10 @@ +import json from pathlib import PurePath import pytest -from jsonasobj2 import loads +from jsonasobj2 import as_json, loads +from rdflib import Graph, Literal, URIRef +from rdflib.namespace import RDF from linkml.generators.jsonldcontextgen import ContextGenerator from linkml.generators.jsonldgen import JSONLDGenerator @@ -48,3 +51,32 @@ def test_uri_and_curie(input_path, snapshot, snapshot_path): ], ) assert g.serialize(format="ttl") == snapshot(f"{model_name}.ttl") + + +def test_issue_80_objectidentifier_roundtrip(input_path): + """Objectidentifier classes should round-trip cleanly through Python, context, and RDF.""" + schema = input_path("issue_80.yaml") + + generated_python = PythonGenerator(schema).serialize() + assert "class Person" in generated_python + + module = compile_python(generated_python) + example = module.Person("http://example.org/person/17", "Fred Jones", 43) + assert json.loads(as_json(example)) == { + "id": "http://example.org/person/17", + "name": "Fred Jones", + "age": 43, + } + + generated_context = json.loads(ContextGenerator(schema).serialize())["@context"] + assert generated_context["Person"]["@id"] == "ex:PERSON" + assert generated_context["age"]["@type"] == "xsd:integer" + + rdf_output = as_rdf(example, contexts=json.dumps({"@context": generated_context})).serialize(format="turtle") + graph = Graph() + graph.parse(data=rdf_output, format="turtle") + + person = URIRef("http://example.org/person/17") + assert (person, RDF.type, URIRef("http://example.org/PERSON")) in graph + assert (person, URIRef("https://w3id.org/biolink/vocab/name"), Literal("Fred Jones")) in graph + assert (person, URIRef("https://w3id.org/biolink/vocab/age"), Literal(43)) in graph From 02ab75184afa3c3171fa60537282e8d9e855096d Mon Sep 17 00:00:00 2001 From: Sveinung Gundersen Date: Thu, 9 Apr 2026 12:59:36 +0200 Subject: [PATCH 15/19] fix(schemaview): preserve insertion order in slot_range_as_union --- packages/linkml_runtime/src/linkml_runtime/utils/schemaview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/linkml_runtime/src/linkml_runtime/utils/schemaview.py b/packages/linkml_runtime/src/linkml_runtime/utils/schemaview.py index e5dc331ae5..7bd85409b6 100644 --- a/packages/linkml_runtime/src/linkml_runtime/utils/schemaview.py +++ b/packages/linkml_runtime/src/linkml_runtime/utils/schemaview.py @@ -1942,7 +1942,7 @@ def slot_range_as_union(self, slot: SlotDefinition) -> list[ElementName]: err_msg = "A SlotDefinition must be provided to generate the slot range as union." raise ValueError(err_msg) - return list({y.range for y in [slot, *[x for x in [*slot.exactly_one_of, *slot.any_of] if x.range]]}) + return list({y.range: None for y in [slot, *[x for x in [*slot.exactly_one_of, *slot.any_of] if x.range]]}.keys()) def induced_slot_range(self, slot: SlotDefinition, strict: bool = False) -> set[str | ElementName]: # noqa: FBT001, FBT002 """Retrieve all applicable ranges for a slot, falling back to the default if necessary. From 77139755cf683d5413336c2843df22e37c1fbd8a Mon Sep 17 00:00:00 2001 From: Sveinung Gundersen Date: Thu, 9 Apr 2026 13:03:44 +0200 Subject: [PATCH 16/19] fix(schemaview): preserve insertion order in get_classes_by_slot --- .../linkml_runtime/src/linkml_runtime/utils/schemaview.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/linkml_runtime/src/linkml_runtime/utils/schemaview.py b/packages/linkml_runtime/src/linkml_runtime/utils/schemaview.py index 7bd85409b6..d1178645aa 100644 --- a/packages/linkml_runtime/src/linkml_runtime/utils/schemaview.py +++ b/packages/linkml_runtime/src/linkml_runtime/utils/schemaview.py @@ -2008,20 +2008,20 @@ def get_classes_by_slot(self, slot: SlotDefinition, include_induced: bool = Fals :param include_induced: supplement all direct slots with induced slots, defaults to False :return: list of slots, either direct, or both direct and induced """ - classes_set = set() # use set to avoid duplicates + classes_ordered_set = {} # use dict with None as value to mimic ordered set all_classes = self.all_classes() for c_name, c in all_classes.items(): if slot.name in c.slots: - classes_set.add(c_name) + classes_ordered_set[c_name] = None if include_induced: for c_name in all_classes: induced_slot_names = [ind_slot.name for ind_slot in self.class_induced_slots(c_name)] if slot.name in induced_slot_names: - classes_set.add(c_name) + classes_ordered_set[c_name] = None - return list(classes_set) + return list(classes_ordered_set.keys()) @lru_cache(None) def get_slots_by_enum(self, enum_name: ENUM_NAME = None) -> list[SlotDefinition]: From a60430641e353e688078d435791690e2e90a23fb Mon Sep 17 00:00:00 2001 From: Sveinung Gundersen Date: Thu, 9 Apr 2026 13:18:14 +0200 Subject: [PATCH 17/19] Linting and improved comments --- .../linkml_runtime/src/linkml_runtime/utils/schemaview.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/linkml_runtime/src/linkml_runtime/utils/schemaview.py b/packages/linkml_runtime/src/linkml_runtime/utils/schemaview.py index d1178645aa..eb8c277b46 100644 --- a/packages/linkml_runtime/src/linkml_runtime/utils/schemaview.py +++ b/packages/linkml_runtime/src/linkml_runtime/utils/schemaview.py @@ -1942,7 +1942,10 @@ def slot_range_as_union(self, slot: SlotDefinition) -> list[ElementName]: err_msg = "A SlotDefinition must be provided to generate the slot range as union." raise ValueError(err_msg) - return list({y.range: None for y in [slot, *[x for x in [*slot.exactly_one_of, *slot.any_of] if x.range]]}.keys()) + # Using dict with None as value to mimic ordered set behavior + return list( + {y.range: None for y in [slot, *[x for x in [*slot.exactly_one_of, *slot.any_of] if x.range]]}.keys() + ) def induced_slot_range(self, slot: SlotDefinition, strict: bool = False) -> set[str | ElementName]: # noqa: FBT001, FBT002 """Retrieve all applicable ranges for a slot, falling back to the default if necessary. @@ -2008,7 +2011,7 @@ def get_classes_by_slot(self, slot: SlotDefinition, include_induced: bool = Fals :param include_induced: supplement all direct slots with induced slots, defaults to False :return: list of slots, either direct, or both direct and induced """ - classes_ordered_set = {} # use dict with None as value to mimic ordered set + classes_ordered_set = {} # To avoid duplicates. Using dict with None as value to mimic ordered set all_classes = self.all_classes() for c_name, c in all_classes.items(): From 39c255df7992e1f0f3442bf4e2c2aed8d3859712 Mon Sep 17 00:00:00 2001 From: Sarah Gehrke <99770056+sagehrke@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:06:57 -0600 Subject: [PATCH 18/19] Update Community-Meetings.md Updating the Community Meeting schedule to have the most up to date information for the April 19 and May 21, 2026 community call presenters. Also took the opportunity to remove the disabled Slack join link from the page. --- docs/get-involved/Community-Meetings.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/get-involved/Community-Meetings.md b/docs/get-involved/Community-Meetings.md index 3d597e8335..c0e115bd36 100644 --- a/docs/get-involved/Community-Meetings.md +++ b/docs/get-involved/Community-Meetings.md @@ -10,7 +10,7 @@ Join the LinkML community for regular sessions featuring presentations on LinkML [Sarah Gehrke](https://tislab.org/members/sarah-gehrke.html), University of North Carolina at Chapel Hill
## What to present at a Community meeting? -Contact Sarah Gehrke on [Slack](https://join.slack.com/t/obo-communitygroup/shared_invite/zt-1oq48ttk7-kKo0i6TwntYtAq~Jcjjg4g) and they can help get you on the schedule! +Contact Sarah Gehrke on Slack and they can help get you on the schedule! ## Helpful links - [Community Meeting Agenda](https://docs.google.com/document/d/1MStpDyh9LOZYJTjLtnpOsNYc3HaeU-bz0CHAI9xOjfQ/edit?tab=t.0#heading=h.6sqkx1xhumse): In this document you can find the zoom link to join the monthly call as well as recordings of previous sessions. @@ -20,8 +20,8 @@ Contact Sarah Gehrke on [Slack](https://join.slack.com/t/obo-communitygroup/shar | Date | Presenter 1 | Topic 1 | Presenter 2 | Topic 2 | | :---: | :---: | :---: | :----: | :---: | -| May 21, 2026 | | Open slot! Contact Sarah if you'd like to present.| |Open slot! Contact Sarah if you'd like to present.| | -| April 19, 2026 | Daniel Kapitan| [LinkML in Hospital ETL pipelines](https://github.com/linkml/linkml/pull/3257#issuecomment-4055029669)| | Open slot! Contact Sarah if you'd like to present.| | +| May 21, 2026 | Inge Vejsbjerg | [LinkML for AI Governance at IBM](https://ibm.github.io/ai-atlas-nexus/) |Sierra Moxon|LinkML Microschemas| | +| April 19, 2026 | Daniel Kapitan| [LinkML in Hospital ETL pipelines](https://github.com/linkml/linkml/pull/3257#issuecomment-4055029669)| Community Discussion Topics| | | | [March 19, 2026](https://docs.google.com/presentation/d/13gQtrlTJcF3V8XzW4ruva9_D1tzyp8ZzDLATp2TuuMY/edit?slide=id.g36e69bd970c_1_50#slide=id.g36e69bd970c_1_50)| Damien Goutte-Gattat | [linkml-java](https://github.com/gouttegd/linkml-java): a LinkML runtime library for the Java language ([Slides](https://drive.google.com/file/d/1-hXJo9FFmIvpERrKNJjb_-tTO_6354cM/view?usp=sharing))| Chris Mungall| Validating dynamic value sets with [linkml-term-validator](https://github.com/linkml/linkml-term-validator) ([Slides](https://docs.google.com/presentation/d/1lhdDImxBQDaj8yFgpRYF_pnCTNDVn3Ilkb_pHfoR7Po/edit?slide=id.p#slide=id.p))| | [February 19, 2026](https://docs.google.com/presentation/d/1Wa_vX8JhLeA_5b7dsRDDoWAA7uTG9fsiH0nMYjxjSz0/edit?slide=id.g36e69bd970c_1_50#slide=id.g36e69bd970c_1_50) | David Linke | [The new LinkML project template based on copier and why we built it](https://zenodo.org/records/18704589)
Repo: [linkml-project-copier](https://github.com/linkml/linkml-project-copier) | Nico Matentzoglu | [The LinkML Community Governance & Community Discussion](https://drive.google.com/file/d/1s2Fslc2pN6ig2-TDkdtzjtBKa5z1oFqC/view?usp=sharing) | | [January 15, 2026](https://docs.google.com/presentation/d/1ed5P1BsAylCiifEw6ATHmnNY24j91mViK62oZKcVYyo/edit?slide=id.g36e69bd970c_1_50#slide=id.g36e69bd970c_1_50) | Adam Graefe | [LinkML in Rare Diseases: Ontology-Based Interoperability for Clinical Registries and Analysis](https://docs.google.com/presentation/d/1Zeme_WzQOMyyMjHkx5UIkvZMyTIzDVHy/edit?usp=sharing&ouid=113016219988110049925&rtpof=true&sd=true) | Community Discussion Topics | Markdown-data-dictionary generator with Vlad Korolev | From d2dd1458ba833222b89535ace126997a0f07b4ef Mon Sep 17 00:00:00 2001 From: Nico Matentzoglu Date: Tue, 14 Apr 2026 23:01:41 +0300 Subject: [PATCH 19/19] Add regression tests for _normalize_inlined with comma-containing values --- .../test_utils/test_yaml_utils.py | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/tests/linkml_runtime/test_utils/test_yaml_utils.py b/tests/linkml_runtime/test_utils/test_yaml_utils.py index 709d6a1d74..044e9ccc2d 100644 --- a/tests/linkml_runtime/test_utils/test_yaml_utils.py +++ b/tests/linkml_runtime/test_utils/test_yaml_utils.py @@ -372,3 +372,150 @@ def test_normalize_inlined_list_of_lists_with_inheritance(): """List-of-lists: first element must map to key, not first MRO field.""" c = _ContainerList(items=[["n1", "t1"]]) assert c.items == [_ChildClass(notation="n1", title="t1")] + + +# --------------------------------------------------------------------------- +# _normalize_inlined with values containing commas (issue #3367) +# +# Classes below model the reproducer from the issue: +# +# id: https://example.org/comma-bug +# name: comma_bug +# imports: +# - linkml:types +# prefixes: +# linkml: https://w3id.org/linkml/ +# ex: https://example.org/ +# default_prefix: ex +# default_range: string +# classes: +# CommaContainer: +# tree_root: true +# attributes: +# items: +# range: CommaItem +# multivalued: true +# inlined_as_list: true +# CommaItem: +# attributes: +# item_id: +# identifier: true +# range: string +# synonyms: +# range: CommaSynonym +# multivalued: true +# inlined_as_list: true +# CommaSynonym: +# attributes: +# synonym_text: +# required: true +# synonym_type: +# range: string +# +# CommaSynonym has no identifier — synonym_text is just required. +# Values like "48,XXYY syndrome" must not be split or rejected. +# --------------------------------------------------------------------------- + + +class _CommaItemId(extended_str): + pass + + +@dataclass(repr=False) +class _CommaSynonym(YAMLRoot): + _inherited_slots: ClassVar[list[str]] = [] + + synonym_text: str = None + synonym_type: Optional[str] = None + + def __post_init__(self, *_: str, **kwargs: Any): + if self._is_empty(self.synonym_text): + self.MissingRequiredField("synonym_text") + if not isinstance(self.synonym_text, str): + self.synonym_text = str(self.synonym_text) + + if self.synonym_type is not None and not isinstance(self.synonym_type, str): + self.synonym_type = str(self.synonym_type) + + super().__post_init__(**kwargs) + + +@dataclass(repr=False) +class _CommaItem(YAMLRoot): + _inherited_slots: ClassVar[list[str]] = [] + + item_id: Union[str, _CommaItemId] = None + synonyms: Optional[Union[Union[dict, _CommaSynonym], list[Union[dict, _CommaSynonym]]]] = empty_list() + + def __post_init__(self, *_: str, **kwargs: Any): + if self._is_empty(self.item_id): + self.MissingRequiredField("item_id") + if not isinstance(self.item_id, _CommaItemId): + self.item_id = _CommaItemId(self.item_id) + + self._normalize_inlined_as_list( + slot_name="synonyms", slot_type=_CommaSynonym, key_name="synonym_text", keyed=False + ) + + super().__post_init__(**kwargs) + + +@dataclass(repr=False) +class _CommaContainer(YAMLRoot): + _inherited_slots: ClassVar[list[str]] = [] + + items: Optional[Union[dict[Union[str, _CommaItemId], Union[dict, _CommaItem]], list[Union[dict, _CommaItem]]]] = ( + empty_dict() + ) + + def __post_init__(self, *_: str, **kwargs: Any): + self._normalize_inlined_as_list(slot_name="items", slot_type=_CommaItem, key_name="item_id", keyed=True) + + super().__post_init__(**kwargs) + + +@pytest.mark.parametrize( + "synonym_text", + [ + "48,XXYY syndrome", + "a,b,c", + ",leading comma", + "trailing comma,", + "1,000", + "Smith, John", + ], +) +def test_normalize_inlined_comma_in_value(synonym_text: str): + """Values containing commas must survive _normalize_inlined (issue #3367).""" + c = _CommaContainer( + items=[{"item_id": "ex:001", "synonyms": [{"synonym_text": synonym_text}]}], + ) + assert len(c.items) == 1 + assert len(c.items[0].synonyms) == 1 + assert c.items[0].synonyms[0].synonym_text == synonym_text + + +def test_normalize_inlined_comma_in_value_from_yaml(): + """Values with commas must round-trip through YAML loading (issue #3367).""" + yaml_str = """ +items: +- item_id: ex:001 + synonyms: + - synonym_text: '48,XXYY syndrome' + - synonym_text: simple synonym +""" + result = from_yaml(yaml_str, _CommaContainer) + assert len(result.items) == 1 + synonyms = result.items[0].synonyms + assert len(synonyms) == 2 + assert synonyms[0].synonym_text == "48,XXYY syndrome" + assert synonyms[1].synonym_text == "simple synonym" + + +def test_normalize_inlined_comma_in_identifier(): + """Commas in identifier values must also be handled (issue #3367).""" + c = _CommaContainer( + items=[{"item_id": "48,XXYY", "synonyms": [{"synonym_text": "syn1"}]}], + ) + assert len(c.items) == 1 + assert str(c.items[0].item_id) == "48,XXYY"