Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
dc97223
fix(excel): move workbook.save outside loop
noelmcloughlin Apr 18, 2026
62343c0
chore(excelgen): add test coverage with updated kitchen_sink
noelmcloughlin Apr 18, 2026
5c6e1eb
test: regenerate snapshots affected by changed kitchen-sink model
noelmcloughlin Apr 29, 2026
7d27a19
Use `linkml.validator` in `linkml.utils.converter`
sneakers-the-rat May 4, 2026
781a968
terminusdb: updates to the modern schema format
philocalyst May 4, 2026
636cbed
fix(generators): add --xsd-anyuri-as-iri flag for cross-generator IRI…
jdsika May 4, 2026
6285fa3
fix(jsonldcontextgen): emit scoped contexts for class-level range ove…
amc-corey-cox May 4, 2026
21cec18
Merge branch 'main' into excel
sujaypatil96 May 4, 2026
373cba1
Merge pull request #3420 from noelmcloughlin/excel
sujaypatil96 May 4, 2026
5e33f58
Update Metamodel files for 1.11.0-rc2
matentzn May 7, 2026
79b2382
Update fixtures / tests
matentzn May 7, 2026
2a10bb5
Update snapshots
matentzn May 7, 2026
b6fdeca
Source linkml model from raw github rather than PURL
matentzn May 7, 2026
64f6a21
Adding test for any_of at the class level
cmungall May 7, 2026
6340cb0
Merge branch 'main' into linkml-model-1.11.0rc2
amc-corey-cox May 7, 2026
a7ed3e4
Merge pull request #3484 from linkml/linkml-model-1.11.0rc2
matentzn May 7, 2026
44515d5
feat(contextgen): add @type: @vocab coercion for eligible enums
jdsika May 7, 2026
2b8a024
Fix 2687: Make `class_uri` be `skos:exactMatch` of the element URI on…
Silvanoc May 8, 2026
60f0f7f
docs: update feature dashboard from compliance tests
amc-corey-cox May 8, 2026
5b7e967
Merge pull request #3486 from linkml/auto/update-feature-dashboard
matentzn May 8, 2026
1a3fe72
Fix empty multivalued cells crashing loader with --list-wrapper none
turbomam May 8, 2026
369bcd4
first working version openapi generator
Silvanoc May 8, 2026
3712b38
Emit `examples` in JSON Schema output (#3378)
sveinugu May 8, 2026
7fad7d2
Add --include flag to linkml validate for schema rules overlay compos…
amc-corey-cox May 8, 2026
ed8ea99
Merge branch 'main' into feat/contextgen-enum-vocab-coercion
kevinschaper May 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions docs/generators/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,18 @@ Percentage of tests where the generator fully implements the feature (excluding

| Generator | Implements | Partial | Ignores | N/A | Total | Score |
|-----------|:----------:|:-------:|:-------:|:---:|:-----:|:-----:|
| 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% |
| Pydantic | 29 | 34 | 1 | 0 | 64 | 45% |
| Python DC | 16 | 38 | 10 | 0 | 64 | 25% |
| JSON Schema | 39 | 23 | 2 | 0 | 64 | 61% |
| Java | 0 | 24 | 40 | 0 | 64 | 0% |
| SHACL | 19 | 25 | 20 | 0 | 64 | 30% |
| ShEx | 0 | 24 | 40 | 0 | 64 | 0% |
| OWL | 0 | 26 | 38 | 0 | 64 | 0% |
| JSON-LD Ctx | 34 | 24 | 6 | 0 | 64 | 53% |
| SQLite DDL | 12 | 40 | 12 | 0 | 64 | 19% |
| Postgres DDL | 0 | 23 | 41 | 0 | 64 | 0% |
| Pandera | 14 | 28 | 22 | 0 | 64 | 22% |
| Polars Schema | 28 | 23 | 13 | 0 | 64 | 44% |

## Details by Category

Expand All @@ -72,6 +72,7 @@ Percentage of tests where the generator fully implements the feature (excluding
|------| :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
| Cardinality in exactly_one_of | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ |
| Class any_of | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ❓ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ |
| Class boolean constraints with required | ⚠️ | ⚠️ | ✅ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ |
| Class boolean with expressions | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ❓ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ❓ | ⚠️ |
| Slot all_of | ⚠️ | ⚠️ | ✅ | ⚠️ | ⚠️ | ⚠️ | ❓ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ |
| Slot any_of | ✅ | ⚠️ | ⚠️ | ❓ | ❓ | ❓ | ❓ | ✅ | ⚠️ | ❓ | ❓ | ❓ |
Expand Down
5 changes: 5 additions & 0 deletions docs/maintainers/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ New tests in any directory should be written using pytest.
If you make a change that intentionally causes some output to not match the saved snapshot file(s), you should update the snapshots by running `pytest` with the `--generate-snapshots` flag. You should try to run only a single or small group of tests with this flag (as opposed to the entire test suite). An exception to this rule is when preparing a new minor version of linkml after the metamodel changes, changes to the metamodel can have many (inconsequential) changes to multiple snapshots.
The updated snapshot files should be checked in to Git alongside your other code changes.

Examples:

`uv run pytest tests/linkml/test_scripts/test_gen_owl.py --with-slow --generate-snapshots`
`uv run pytest tests/linkml/test_scripts/test_gen_shex.py --generate-snapshots --with-network`

Debugging tip: sometimes a snapshot-based test may fail on GitHub actions, but may appear to pass locally. This can happen if the test is marked as a slow test,
in which case you may need to use `--generate-snapshots` in combination with `--with-slow` (see below).

Expand Down
2 changes: 1 addition & 1 deletion examples/tutorial/tutorial01/personinfo.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"$id": "https://w3id.org/linkml/examples/personinfo",
"$schema": "https://json-schema.org/draft/2019-09/schema",
"additionalProperties": true,
"metamodel_version": "1.7.0",
"metamodel_version": "1.11.0",
"title": "personinfo",
"type": "object",
"version": null
Expand Down
2 changes: 1 addition & 1 deletion examples/tutorial/tutorial04/personinfo-semantic.shex
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# metamodel_version: 1.7.0
# metamodel_version: 1.11.0
BASE <https://w3id.org/linkml/examples/personinfo/>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
Expand Down
2 changes: 1 addition & 1 deletion examples/tutorial/tutorial05/personinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from linkml_runtime.utils.curienamespace import CurieNamespace
from linkml_runtime.linkml_model.types import Integer, String

metamodel_version = "1.7.0"
metamodel_version = "1.11.0"

# Namespaces
ORCID = CurieNamespace('ORCID', 'https://orcid.org/')
Expand Down
1 change: 1 addition & 0 deletions packages/linkml/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ tests-extra = [
"mock >= 5.1.0",
"testcontainers == 3.7.1",
"jsonpatch >= 1.33",
"openapi-spec-validator >= 0.8.4",
]
docs = [
"furo >= 2023.03.27",
Expand Down
2 changes: 2 additions & 0 deletions packages/linkml/src/linkml/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from linkml.generators.jsonschemagen import cli as gen_json_schema
from linkml.generators.linkmlgen import cli as gen_linkml
from linkml.generators.namespacegen import cli as gen_namespaces
from linkml.generators.openapigen import cli as gen_openapi
from linkml.generators.owlgen import cli as gen_owl
from linkml.generators.panderagen import cli as gen_pandera
from linkml.generators.plantumlgen import cli as gen_plantuml
Expand Down Expand Up @@ -106,6 +107,7 @@ def dev():
generate.add_command(gen_json_schema, name="json-schema")
generate.add_command(gen_doc, name="doc")
generate.add_command(gen_namespaces, name="namespaces")
generate.add_command(gen_openapi, name="openapi")
generate.add_command(gen_owl, name="owl")
generate.add_command(gen_plantuml, name="plantuml")
generate.add_command(gen_proto, name="proto")
Expand Down
10 changes: 6 additions & 4 deletions packages/linkml/src/linkml/converter/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import pathlib
import sys
from typing import TYPE_CHECKING

import click
import yaml
Expand All @@ -27,6 +28,9 @@
from linkml_runtime.utils.inference_utils import infer_all_slot_values
from linkml_runtime.utils.schemaview import SchemaView

if TYPE_CHECKING:
from linkml_runtime.utils.yamlutils import YAMLRoot

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -197,7 +201,7 @@ def cli(
target_class = infer_root_class(sv)
if target_class is None:
raise Exception("target class not specified and could not be inferred")
py_target_class = python_module.__dict__[target_class]
py_target_class: YAMLRoot = python_module.__dict__[target_class]
input_format = _get_format(input, input_format)
loader = get_loader(input_format)

Expand Down Expand Up @@ -237,9 +241,7 @@ def cli(
raise Exception("--schema must be passed in order to validate. Suppress with --no-validate")
obj_dict = json_dumper.to_dict(obj)
report = run_validation(obj_dict, schema, target_class)
if report.results:
errors = "\n".join(r.message for r in report.results)
raise Exception(f"Validation failed:\n{errors}")
report.raise_for_results()

output_format = _get_format(output, output_format, default="json")
if output_format == "json-ld":
Expand Down
3 changes: 3 additions & 0 deletions packages/linkml/src/linkml/generators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from linkml.generators.jsonldcontextgen import ContextGenerator
from linkml.generators.jsonldgen import JSONLDGenerator
from linkml.generators.jsonschemagen import JsonSchemaGenerator
from linkml.generators.openapigen import OpenApiGenerator
from linkml.generators.owlgen import OwlSchemaGenerator
from linkml.generators.panderagen import PanderaDataframeGenerator, PolarsSchemaDataframeGenerator
from linkml.generators.pydanticgen import PydanticGenerator
Expand All @@ -31,6 +32,7 @@
"jsonldgen",
"jsonschemagen",
"namespacegen",
"openapigen",
"owlgen",
"plantumlgen",
"protogen",
Expand All @@ -43,6 +45,7 @@
"summarygen",
"typedbgen",
"yamlgen",
"OpenApiGenerator",
"OwlSchemaGenerator",
"PydanticGenerator",
"PanderaDataframeGenerator",
Expand Down
33 changes: 33 additions & 0 deletions packages/linkml/src/linkml/generators/common/subproperty.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
CURIE_TYPES: frozenset[str] = frozenset({"uriorcurie", "curie"})
URI_TYPES: frozenset[str] = frozenset({"uri"})

# Types whose XSD mapping is xsd:anyURI (not xsd:string).
# ``curie`` maps to xsd:string and is deliberately excluded.
_ANYURI_TYPES: frozenset[str] = frozenset({"uri", "uriorcurie"})


def is_uri_range(sv: SchemaView, range_type: str | None) -> bool:
"""
Expand Down Expand Up @@ -63,6 +67,35 @@ def is_curie_range(sv: SchemaView, range_type: str | None) -> bool:
return False


def is_xsd_anyuri_range(sv: SchemaView, range_type: str | None) -> bool:
"""Check if range type resolves to ``xsd:anyURI``.

Returns True for ``uri``, ``uriorcurie``, and types that inherit from them.
Returns False for ``curie`` (which maps to ``xsd:string``).

This is the correct predicate for the ``--xsd-anyuri-as-iri`` flag: only
types whose XSD representation is ``xsd:anyURI`` should be promoted from
literal to IRI semantics. ``curie`` is a compact string representation
that resolves to ``xsd:string`` and must not be affected.

:param sv: SchemaView for type ancestry lookup
:param range_type: The range type to check
:return: True if range type maps to xsd:anyURI
"""
if range_type is None:
return False

if range_type in _ANYURI_TYPES:
return True

if range_type in sv.all_types():
type_ancestors = set(sv.type_ancestors(range_type))
if type_ancestors & _ANYURI_TYPES:
return True

return False


def format_slot_value_for_range(sv: SchemaView, slot_name: str, range_type: str | None) -> str:
"""
Format slot value according to the declared range type.
Expand Down
16 changes: 9 additions & 7 deletions packages/linkml/src/linkml/generators/excelgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,21 @@ def _create_workbook_and_worksheets(self, output_path: Path, classes: list[str])
workbook.remove(workbook.active)
sv = self.schemaview

# Compute enum lookup before next inner loop
enum_set = set(sv.all_enums(imports=self.mergeimports).keys())

for cls_name in classes:
workbook.create_sheet(cls_name)

# Add columns to the worksheet for the current class
slots = [s.name for s in sv.class_induced_slots(cls_name, self.mergeimports)]
self.add_columns_to_worksheet(workbook, cls_name, slots)
workbook.save(output_path)
# (call class_induced_slots, reuse for heading/enum validation)
induced_slots = list(sv.class_induced_slots(cls_name, self.mergeimports))
slot_names = [s.name for s in induced_slots]
self.add_columns_to_worksheet(workbook, cls_name, slot_names)

# Add enum validation for columns with enum types
enum_list = list(sv.all_enums(imports=self.mergeimports).keys())
for s in sv.class_induced_slots(cls_name, self.mergeimports):
if s.range in enum_list:
for s in induced_slots:
if s.range in enum_set:
pv_list = list(sv.get_enum(s.range).permissible_values.keys())

# Data Validation formula to be applied to the column and
Expand All @@ -81,7 +84,6 @@ def _create_workbook_and_worksheets(self, output_path: Path, classes: list[str])
"length > 255 characters. Dropdowns may not work properly "
f"in {output_path}"
)
workbook.save(output_path)

workbook.save(output_path)
if self.split_workbook_by_class:
Expand Down
Loading
Loading