Skip to content
Draft
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
59 changes: 51 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,21 +269,64 @@ type: `http://spdx.invalid./AbstractClass`, but this is not preferred.

### Pre-Release models

`shacl2code` has a custom annotation that can be used to mark an entire
ontology as "pre-release" meaning it is still subject to break changes. In the
python bindings, this will cause a `FutureWarning` to be emitted. In order to
do this, you must declare your ontology, and then use the custom annotation to
mark it as pre-release:
`shacl2code` can detect if an ontology is a "pre-release" version (still
subject to breaking changes). For language bindings that support it (such as
Python), importing a pre-release ontology binding will emit a warning
(e.g. `FutureWarning`).

```ttl
Pre-release status can be specified explicitly via command-line options,
or inferred automatically from various ontology annotations. For example,

```ttl
<http://example.org/my-ontology> a ow:Ontology ;
sh-to-code:isPreRelease true
.
sh-to-code:isPreRelease true
.
```

In the event of conflicting annotations, `shacl2code` evaluates pre-release
status using the following order of precedence (1 = the highest priority):

1. **`--pre-release` or `--no-pre-release` command-line options**:
Explicitly marks the generated bindings as pre-release or stable,
overriding any annotations in the input ontology.
2. **`sh-to-code:isPreRelease`**:
The `shacl2code` custom boolean annotation
(`sh-to-code:isPreRelease true` or `false`).
3. **`adms:status` (EU SEMIC Vocabulary)**:
If the status is
`<http://publications.europa.eu/resource/authority/dataset-status/DEVELOP>`,
it is considered a pre-release.
Other values in the dataset-status vocabulary space (e.g. `COMPLETED`)
indicate a stable release.
4. **`adms:status` (Original ADMS Vocabulary)**:
If the status is `<http://purl.org/adms/status/UnderDevelopment>`,
it is considered a pre-release.
Other values in the ADMS status namespace (e.g. `Completed`)
indicate a stable release.
5. **`bibo:status` (Bibliographic Ontology)**:
If set to `<http://purl.org/ontology/bibo/status/draft>`, it is
considered a pre-release.
Other values in the BIBO status namespace (e.g. `published`, `legal`)
indicate a stable release.
6. **`schema:creativeWorkStatus`**:
If set to `"Draft"` or `"Incomplete"`, it is considered a pre-release.
Other values (e.g. `"Published"`) indicate a stable release.
7. **`vs:term_status`**:
If set to `"unstable"` or `"testing"`, it is considered a pre-release.
Other values (e.g. `"stable"`) indicate a stable release.
8. **`owl:versionInfo` (pre-release extension)**:
If the version string contains a pre-release extension suffix
(e.g., `-alpha`, `-beta`, `-dev`, `-rc`, `-SNAPSHOT`, `.alpha`, etc.).
9. **`owl:versionInfo` (major version zero)**:
If the version string corresponds to a major version zero in
[Semantic Versioning][semver] (e.g., `0.7.1`).
10. **Default Fallback**:
If none of the above are present, the ontology is assumed to be a stable
release (`false`).

Note that the IRI of the ontology must be the prefix of all IRIs that belong to
that ontology.

[pytest]: https://www.pytest.org
[pytest-cov]: https://pytest-cov.readthedocs.io/en/latest/
[semver]: https://semver.org/
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ dev = [
"mypy >= 1.19.1", # latest version for Python 3.9 is 1.19.x
"pyrefly >= 0.55.0",
"pyright >= 1.1.403",
"pyshacl >= 0.25.0",
"pyshacl >= 0.25.0, < 0.40.0", # 0.40.0 uses Python 3.10+ type union
"pytest >= 7.4",
"pytest-cov >= 4.1",
"pytest-xdist >= 3.5",
Expand Down
7 changes: 6 additions & 1 deletion src/shacl2code/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def handle_generate(parser, args):
data = json.load(f)
contexts.append(ContextData(data, url))

m = Model(graph, UrlContext(contexts))
m = Model(graph, UrlContext(contexts), is_prerelease=args.pre_release)

render = args.lang(args)
render.output(m)
Expand Down Expand Up @@ -120,6 +120,11 @@ def handle_version(parser, args):
help="SPDX License Identifier to use for generated source code. Default is %(default)s",
default="0BSD",
)
generate_parser.add_argument(
"--pre-release",
action=argparse.BooleanOptionalAction,
help="Mark the generated binding as pre-release. Overrides any ontology annotations",
)
generate_parser.set_defaults(func=handle_generate)

lang_subparser = generate_parser.add_subparsers(
Expand Down
112 changes: 108 additions & 4 deletions src/shacl2code/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# SPDX-License-Identifier: MIT
"""SHACL model parsing and data class definitions"""

import re
from dataclasses import dataclass, field
from typing import List, Optional

Expand All @@ -19,6 +20,8 @@
XSD,
)

from .util import convert_version_string

PATTERN_DATATYPES = [
str(XSD.string),
str(XSD.dateTime),
Expand Down Expand Up @@ -114,7 +117,7 @@ class Class:


class Model(object):
def __init__(self, graph, context=None):
def __init__(self, graph, context=None, is_prerelease=None):
self.model = graph
self.context = context
self.compact_ids = {}
Expand Down Expand Up @@ -197,6 +200,109 @@ def is_abstract(s):

return False

def is_semver_prerelease(version_str):
# Only treat a hyphen as a semver pre-release marker when it
# directly follows a dotted numeric core (e.g. "1.2.3-beta"),
# not e.g. a date-like version such as "2024-01-15".
if re.match(r"^\d+\.\d+(?:\.\d+)?-[0-9A-Za-z]", version_str):
return True
if re.search(
r"\b(alpha|beta|dev|pre|rc|snapshot|test)\b", version_str, re.IGNORECASE
):
return True
return False

def get_is_prerelease(onto_iri):
# 1) --pre-release command line option
if is_prerelease is not None:
return is_prerelease

# 2) sh-to-code:isPreRelease
val = self.model.value(onto_iri, SHACL2CODE.isPreRelease)
if val is not None:
return bool(val)

adms_statuses = list(
self.model.objects(onto_iri, URIRef("http://www.w3.org/ns/adms#status"))
)
if adms_statuses:
semic = [
str(s)
for s in adms_statuses
if str(s).startswith(
"http://publications.europa.eu/resource/authority/dataset-status/"
)
]
# 3) adms:status (EU SEMIC vocab)
if semic:
return any(
s
== "http://publications.europa.eu/resource/authority/dataset-status/DEVELOP"
for s in semic
)
original = [
str(s)
for s in adms_statuses
if str(s).startswith("http://purl.org/adms/status/")
]
# 4) adms:status (Original ADMS vocab)
if original:
return any(
s == "http://purl.org/adms/status/UnderDevelopment"
for s in original
)

# 5) bibo:status (Bibliographic Ontology)
bibo_statuses = list(
self.model.objects(
onto_iri, URIRef("http://purl.org/ontology/bibo/status")
)
)
if bibo_statuses:
return any(
str(s) == "http://purl.org/ontology/bibo/status/draft"
for s in bibo_statuses
)

# 6) schema:creativeWorkStatus
schema_statuses = list(
self.model.objects(
onto_iri, URIRef("http://schema.org/creativeWorkStatus")
)
) or list(
self.model.objects(
onto_iri, URIRef("https://schema.org/creativeWorkStatus")
)
)
if schema_statuses:
return any(str(s) in ("Draft", "Incomplete") for s in schema_statuses)

# 7) vs:term_status
vs_statuses = list(
self.model.objects(
onto_iri,
URIRef("http://www.w3.org/2003/06/sw-vocab-status/ns#term_status"),
)
)
if vs_statuses:
return any(str(s) in ("unstable", "testing") for s in vs_statuses)

# 8) & 9) owl:versionInfo
versions = list(self.model.objects(onto_iri, OWL.versionInfo))
if versions:
for version in versions:
version_str = str(version)
# 8) owl:versionInfo (pre-release extension e.g., "-beta", "-alpha", "-rc" etc)
if is_semver_prerelease(version_str):
return True
# 9) owl:versionInfo (major version zero)
parts = convert_version_string(version_str)
if parts and parts[0] == 0:
return True
return False

return False

for onto_iri in self.model.subjects(RDF.type, OWL.Ontology):
label = str(self.model.value(onto_iri, RDFS.label, default=""))
o = Ontology(
Expand All @@ -205,9 +311,7 @@ def is_abstract(s):
label=label,
comment=str(self.model.value(onto_iri, RDFS.comment, default="")),
version=str(self.model.value(onto_iri, OWL.versionInfo, default="")),
is_prerelease=bool(
self.model.value(onto_iri, SHACL2CODE.isPreRelease, default=False)
),
is_prerelease=get_is_prerelease(onto_iri),
)
self.ontologies.append(o)

Expand Down
Loading
Loading