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
28 changes: 27 additions & 1 deletion packages/linkml/src/linkml/generators/owlgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,22 @@ class OwlSchemaGenerator(Generator):
"""Suffix to add to the schema name to create the ontology URI, e.g. .owl.ttl"""

# ObjectVars
diff_stable: bool = False
"""Label blank nodes so that unrelated edits leave them untouched.

Output is already deterministic: RDFC-1.0 guarantees that isomorphic
graphs serialize identically. It does not guarantee that *similar*
graphs serialize *similarly* — blank nodes are numbered ``c14nN`` in a
global order, so adding one class can renumber every blank node after
it and rewrite most of the file.

When ``True``, blank-node labels are instead derived from each node's
own neighbourhood via Weisfeiler-Lehman refinement, so an edit relabels
only the blank nodes it actually touches. The output stays
deterministic and isomorphic either way; only the choice of label
changes. Off by default because enabling it relabels existing output.
"""

metadata_profile: MetadataProfile | None = None
"""Deprecated - use metadata_profiles."""

Expand Down Expand Up @@ -353,7 +369,7 @@ def serialize(self, **kwargs: Any) -> str:
"""
self.as_graph()
fmt = "turtle" if self.format in ["owl", "ttl"] else self.format
return canonicalize_rdf_graph(self.graph, output_format=fmt)
return canonicalize_rdf_graph(self.graph, output_format=fmt, diff_stable=self.diff_stable)

def add_metadata(self, e: Definition | PermissibleValue, uri: URIRef) -> None:
"""
Expand Down Expand Up @@ -1844,6 +1860,16 @@ def slot_owl_type(self, slot: SlotDefinition) -> URIRef:
"specified language tag. Element-level in_language overrides this."
),
)
@click.option(
"--diff-stable/--no-diff-stable",
default=False,
show_default=True,
help=(
"Derive blank-node labels from each node's own neighbourhood so that "
"unrelated edits leave them unchanged. Output is deterministic either "
"way; this makes successive versions of a file diff cleanly."
),
)
@click.version_option(__version__, "-V", "--version")
def cli(yamlfile: str, metadata_profile: str, **kwargs: Any) -> None:
"""Generate an OWL representation of a LinkML model
Expand Down
28 changes: 27 additions & 1 deletion packages/linkml/src/linkml/generators/rdfgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@ class RDFGenerator(Generator):
uses_schemaloader = True

# ObjectVars
diff_stable: bool = False
"""Label blank nodes so that unrelated edits leave them untouched.

Output is already deterministic: RDFC-1.0 guarantees that isomorphic
graphs serialize identically. It does not guarantee that *similar*
graphs serialize *similarly* — blank nodes are numbered ``c14nN`` in a
global order, so adding one class can renumber every blank node after
it and rewrite most of the file.

When ``True``, blank-node labels are instead derived from each node's
own neighbourhood via Weisfeiler-Lehman refinement, so an edit relabels
only the blank nodes it actually touches. The output stays
deterministic and isomorphic either way; only the choice of label
changes. Off by default because enabling it relabels existing output.
"""

emit_metadata: bool = False
context: list[str] = None
original_schema: SchemaDefinition = None
Expand All @@ -89,7 +105,7 @@ def __post_init__(self):

def _data(self, g: Graph) -> str:
fmt = "turtle" if self.format == "ttl" else self.format
return canonicalize_rdf_graph(g, output_format=fmt)
return canonicalize_rdf_graph(g, output_format=fmt, diff_stable=self.diff_stable)

def end_schema(self, output: str | None = None, context: str = None, **_) -> str:
gen = JSONLDGenerator(
Expand Down Expand Up @@ -137,6 +153,16 @@ def end_schema(self, output: str | None = None, context: str = None, **_) -> str
multiple=True,
help="JSONLD context file (default: vendored meta.context.jsonld)",
)
@click.option(
"--diff-stable/--no-diff-stable",
default=False,
show_default=True,
help=(
"Derive blank-node labels from each node's own neighbourhood so that "
"unrelated edits leave them unchanged. Output is deterministic either "
"way; this makes successive versions of a file diff cleanly."
),
)
@click.version_option(__version__, "-V", "--version")
def cli(yamlfile, **kwargs):
"""Generate an RDF representation of a LinkML model"""
Expand Down
28 changes: 27 additions & 1 deletion packages/linkml/src/linkml/generators/shaclgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,22 @@ class ShaclGenerator(Generator):
ignores any per-slot ``in_language``.
"""

diff_stable: bool = False
"""Label blank nodes so that unrelated edits leave them untouched.

Output is already deterministic: RDFC-1.0 guarantees that isomorphic
graphs serialize identically. It does not guarantee that *similar*
graphs serialize *similarly* — blank nodes are numbered ``c14nN`` in a
global order, so adding one class can renumber every blank node after
it and rewrite most of the file.

When ``True``, blank-node labels are instead derived from each node's
own neighbourhood via Weisfeiler-Lehman refinement, so an edit relabels
only the blank nodes it actually touches. The output stays
deterministic and isomorphic either way; only the choice of label
changes. Off by default because enabling it relabels existing output.
"""

emit_rules: bool = True
"""Emit ``sh:sparql`` constraints from LinkML ``rules:`` blocks.

Expand Down Expand Up @@ -196,7 +212,7 @@ def generate_header(self) -> str:
def serialize(self, **args) -> str:
g = self.as_graph()
fmt = "turtle" if self.format in ["owl", "ttl"] else self.format
return canonicalize_rdf_graph(g, output_format=fmt)
return canonicalize_rdf_graph(g, output_format=fmt, diff_stable=self.diff_stable)

def as_graph(self) -> Graph:
sv = self.schemaview
Expand Down Expand Up @@ -929,6 +945,16 @@ def add_simple_data_type(func: Callable, r: ElementName) -> None:
"sh:NodeShape. Use --no-emit-rules to suppress rule generation."
),
)
@click.option(
"--diff-stable/--no-diff-stable",
default=False,
show_default=True,
help=(
"Derive blank-node labels from each node's own neighbourhood so that "
"unrelated edits leave them unchanged. Output is deterministic either "
"way; this makes successive versions of a file diff cleanly."
),
)
@click.version_option(__version__, "-V", "--version")
def cli(yamlfile, **args):
"""Generate SHACL turtle from a LinkML model"""
Expand Down
28 changes: 27 additions & 1 deletion packages/linkml/src/linkml/generators/shexgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@ class ShExGenerator(Generator):
uses_schemaloader = True

# ObjectVars
diff_stable: bool = False
"""Label blank nodes so that unrelated edits leave them untouched.

Output is already deterministic: RDFC-1.0 guarantees that isomorphic
graphs serialize identically. It does not guarantee that *similar*
graphs serialize *similarly* — blank nodes are numbered ``c14nN`` in a
global order, so adding one class can renumber every blank node after
it and rewrite most of the file.

When ``True``, blank-node labels are instead derived from each node's
own neighbourhood via Weisfeiler-Lehman refinement, so an edit relabels
only the blank nodes it actually touches. The output stays
deterministic and isomorphic either way; only the choice of label
changes. Off by default because enabling it relabels existing output.
"""

shex: Schema = field(default_factory=lambda: Schema()) # ShEx Schema being generated
shapes: list = field(default_factory=lambda: [])
shape: Shape | None = None # Current shape being defined
Expand Down Expand Up @@ -177,7 +193,7 @@ def end_schema(self, output: str | None = None, **_) -> str:
g = Graph()
g.parse(data=shex, format="json-ld", version="1.1")
g.bind("owl", OWL)
shex = canonicalize_rdf_graph(g, output_format="turtle")
shex = canonicalize_rdf_graph(g, output_format="turtle", diff_stable=self.diff_stable)
elif self.format == "shex":
g = Graph()
self.namespaces.load_graph(g)
Expand Down Expand Up @@ -258,6 +274,16 @@ def _get_subproperty_values(self, slot: SlotDefinition) -> list:
help="If --expand-subproperty-of (default), slots with subproperty_of will generate NodeConstraint "
"values containing all slot descendants. Use --no-expand-subproperty-of to disable this behavior.",
)
@click.option(
"--diff-stable/--no-diff-stable",
default=False,
show_default=True,
help=(
"Derive blank-node labels from each node's own neighbourhood so that "
"unrelated edits leave them unchanged. Output is deterministic either "
"way; this makes successive versions of a file diff cleanly."
),
)
@click.version_option(__version__, "-V", "--version")
def cli(yamlfile, **args):
"""Generate a ShEx Schema for a LinkML model"""
Expand Down
1 change: 1 addition & 0 deletions packages/linkml_runtime/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ dependencies = [
"prefixmaps >=0.1.4",
"curies>=0.14.6",
"pyoxigraph>=0.5.11",
"diffable-rdf>=0.4.0",
"pydantic>=2.13.5,<3.0.0",
"isodate >=0.7.2, <1.0.0; python_version < '3.11'",
]
Expand Down
Loading
Loading