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
9 changes: 8 additions & 1 deletion packages/linkml/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ dependencies = [ # Specifier syntax: https://peps.python.org/pep-0631/
"openpyxl",
"parse",
"prefixcommons >= 0.1.7",
"prefixmaps >= 0.2.2",
# TODO(prefixmaps-0.2.8): Replace git pin with "prefixmaps >= 0.2.8" once released,
# then remove [tool.hatch.metadata] allow-direct-references and regenerate uv.lock.
# Tracked in: https://github.com/linkml/prefixmaps/issues/82
"prefixmaps @ git+https://github.com/linkml/prefixmaps@75435150a1b31760b9780af2b64a265943a9b263",
"pydantic >= 2.0.0, < 3.0.0",
"pyjsg >= 0.11.6",
"pyshex >= 0.7.20",
Expand Down Expand Up @@ -196,6 +199,10 @@ vcs = "git"
style = "pep440"
fallback-version = "0.0.0"

[tool.hatch.metadata]
# TODO(prefixmaps-0.2.8): Remove this section once the git pin is replaced with >= 0.2.8
allow-direct-references = true

[tool.hatch.version]
source = "uv-dynamic-versioning"

Expand Down
82 changes: 80 additions & 2 deletions packages/linkml/src/linkml/generators/jsonldcontextgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from linkml._version import __version__
from linkml.utils.deprecation import deprecated_fields
from linkml.utils.generator import Generator, shared_arguments
from linkml.utils.generator import Generator, shared_arguments, well_known_prefix_map
from linkml_runtime.linkml_model.meta import ClassDefinition, EnumDefinition, SlotDefinition
from linkml_runtime.linkml_model.types import SHEX
from linkml_runtime.utils.formatutils import camelcase, underscore
Expand Down Expand Up @@ -90,6 +90,9 @@ class ContextGenerator(Generator):
frame_root: str | None = None

def __post_init__(self) -> None:
# Must be set before super().__post_init__() because the parent triggers
# the visitor pattern (visit_schema), which accesses _prefix_remap.
self._prefix_remap: dict[str, str] = {}
super().__post_init__()
if self.namespaces is None:
raise TypeError("Schema text must be supplied to context generator. Preparsed schema will not work")
Expand Down Expand Up @@ -127,22 +130,92 @@ def _collect_external_elements(sv: SchemaView) -> tuple[set[str], set[str]]:
external_slots.update(schema_def.slots.keys())
return external_classes, external_slots

def add_prefix(self, ncname: str) -> None:
"""Add a prefix, applying well-known prefix normalisation when enabled."""
super().add_prefix(self._prefix_remap.get(ncname, ncname))

def visit_schema(self, base: str | Namespace | None = None, output: str | None = None, **_):
# Add any explicitly declared prefixes
# Add any explicitly declared prefixes.
# Direct .add() is safe here: the normalisation block below explicitly
# rewrites emit_prefixes entries for any renamed prefixes (Cases 1-3).
for prefix in self.schema.prefixes.values():
self.emit_prefixes.add(prefix.prefix_prefix)

# Add any prefixes explicitly declared
for pfx in self.schema.emit_prefixes:
self.add_prefix(pfx)

# Normalise well-known prefix names when --normalize-prefixes is set.
# If the schema declares a non-standard alias for a namespace that has
# a well-known standard name (e.g. ``sdo`` for
# ``https://schema.org/``), replace the alias with the standard name
# so that generated JSON-LD contexts use the conventional prefix.
#
# Three cases are handled:
# 1. Standard prefix is not yet bound → just rebind from old to new.
# 2. Standard prefix is bound to a *different* URI:
# a. User-declared (in schema.prefixes) → collision, skip with warning.
# b. Runtime default (e.g. linkml-runtime's ``schema: http://…``)
# → remove stale binding, then rebind.
# 3. Standard prefix is already bound to the *same* URI (duplicate)
# → just drop the non-standard alias.
#
# A remap dict is stored for ``_build_element_id`` because
# ``prefix_suffix()`` splits CURIEs on ``:`` without looking up the
# namespace dict.
self._prefix_remap.clear()
if self.normalize_prefixes:
wk = well_known_prefix_map()
for old_pfx in list(self.namespaces):
url = str(self.namespaces[old_pfx])
std_pfx = wk.get(url)
if not std_pfx or std_pfx == old_pfx:
continue
if std_pfx in self.namespaces:
if str(self.namespaces[std_pfx]) != url:
# Case 2: std_pfx is bound to a different URI.
# If the user explicitly declared std_pfx in the schema,
# it is intentional — skip to avoid data loss.
if std_pfx in self.schema.prefixes:
self.logger.warning(
"Prefix collision: cannot rename '%s' to '%s' because '%s' is "
"already declared for <%s>; skipping normalisation for <%s>",
old_pfx,
std_pfx,
std_pfx,
str(self.namespaces[std_pfx]),
url,
)
continue
# Not user-declared (e.g. linkml-runtime default) — safe to remove
self.emit_prefixes.discard(std_pfx)
del self.namespaces[std_pfx]
else:
# Case 3: standard prefix already bound to same URI
# — just drop the non-standard alias
del self.namespaces[old_pfx]
if old_pfx in self.emit_prefixes:
self.emit_prefixes.discard(old_pfx)
self.emit_prefixes.add(std_pfx)
self._prefix_remap[old_pfx] = std_pfx
continue
# Case 1 (or Case 2 after stale removal): bind standard name
self.namespaces[std_pfx] = self.namespaces[old_pfx]
del self.namespaces[old_pfx]
if old_pfx in self.emit_prefixes:
self.emit_prefixes.discard(old_pfx)
self.emit_prefixes.add(std_pfx)
self._prefix_remap[old_pfx] = std_pfx

# Add the default prefix
if self.schema.default_prefix:
dflt = self.namespaces.prefix_for(self.schema.default_prefix)
if dflt:
self.default_ns = dflt
if self.default_ns:
default_uri = self.namespaces[self.default_ns]
# Direct .add() is safe: default_ns is already resolved from
# the (possibly normalised) namespace bindings above.
self.emit_prefixes.add(self.default_ns)
else:
default_uri = self.schema.default_prefix
Expand Down Expand Up @@ -486,6 +559,11 @@ def _build_element_id(self, definition: Any, uri: str) -> None:
@return: None
"""
uri_prefix, uri_suffix = self.namespaces.prefix_suffix(uri)
# Apply well-known prefix normalisation (e.g. sdo → schema).
# prefix_suffix() splits CURIEs on ':' without checking the
# namespace dict, so it may return a stale alias.
if uri_prefix and uri_prefix in self._prefix_remap:
uri_prefix = self._prefix_remap[uri_prefix]
is_default_namespace = uri_prefix == self.context_body["@vocab"] or uri_prefix == self.namespaces.prefix_for(
self.context_body["@vocab"]
)
Expand Down
2 changes: 2 additions & 0 deletions packages/linkml/src/linkml/generators/jsonldgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ def end_schema(self, context: str | Sequence[str] | None = None, context_kwargs:
# TODO: The _visit function above alters the schema in situ
# force some context_kwargs
context_kwargs["metadata"] = False
# Forward prefix normalisation into the inline @context.
context_kwargs.setdefault("normalize_prefixes", self.normalize_prefixes)
add_prefixes = ContextGenerator(self.original_schema, **context_kwargs).serialize()
add_prefixes_json = loads(add_prefixes)
metamodel_ctx = self.metamodel_context or METAMODEL_CONTEXT_URI
Expand Down
6 changes: 5 additions & 1 deletion packages/linkml/src/linkml/generators/owlgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from linkml._version import __version__
from linkml.generators.common.subproperty import is_xsd_anyuri_range
from linkml.utils.deprecation import deprecation_warning
from linkml.utils.generator import Generator, shared_arguments
from linkml.utils.generator import Generator, normalize_graph_prefixes, shared_arguments
from linkml_runtime import SchemaView
from linkml_runtime.linkml_model.meta import (
AnonymousClassExpression,
Expand Down Expand Up @@ -264,6 +264,10 @@ def as_graph(self) -> Graph:
self.graph.bind(prefix, self.metamodel.namespaces[prefix])
for pfx in schema.prefixes.values():
self.graph.namespace_manager.bind(pfx.prefix_prefix, URIRef(pfx.prefix_reference))
if self.normalize_prefixes:
normalize_graph_prefixes(
graph, {str(v.prefix_prefix): str(v.prefix_reference) for v in schema.prefixes.values()}
)
graph.add((base, RDF.type, OWL.Ontology))

# Add main schema elements
Expand Down
6 changes: 5 additions & 1 deletion packages/linkml/src/linkml/generators/shaclgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from linkml.generators.common.subproperty import get_subproperty_values, is_uri_range
from linkml.generators.shacl.shacl_data_type import ShaclDataType
from linkml.generators.shacl.shacl_ifabsent_processor import ShaclIfAbsentProcessor
from linkml.utils.generator import Generator, shared_arguments
from linkml.utils.generator import Generator, normalize_graph_prefixes, shared_arguments
from linkml_runtime.linkml_model.meta import ClassDefinition, ElementName
from linkml_runtime.utils.formatutils import underscore
from linkml_runtime.utils.yamlutils import TypedNode, extended_float, extended_int, extended_str
Expand Down Expand Up @@ -105,6 +105,10 @@ def as_graph(self) -> Graph:

for pfx in self.schema.prefixes.values():
g.bind(str(pfx.prefix_prefix), pfx.prefix_reference)
if self.normalize_prefixes:
normalize_graph_prefixes(
g, {str(v.prefix_prefix): str(v.prefix_reference) for v in self.schema.prefixes.values()}
)

for c in sv.all_classes(imports=not self.exclude_imports).values():

Expand Down
Loading
Loading