Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
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
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