From 9b5878f723be510c807da7db705c84fa3d93fac9 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sun, 19 Jul 2026 15:48:40 +0100 Subject: [PATCH 01/12] Add --pre-release to force pre-release status --pre-release / --no-pre-release will take precedence any annotation in the ontology Signed-off-by: Arthit Suriyawongkul --- src/shacl2code/main.py | 7 ++- src/shacl2code/model.py | 12 +++-- tests/test_python.py | 108 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 119 insertions(+), 8 deletions(-) diff --git a/src/shacl2code/main.py b/src/shacl2code/main.py index bc71a140..9ce7604b 100644 --- a/src/shacl2code/main.py +++ b/src/shacl2code/main.py @@ -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) @@ -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( diff --git a/src/shacl2code/model.py b/src/shacl2code/model.py index 7a2d860e..ee5f48b3 100644 --- a/src/shacl2code/model.py +++ b/src/shacl2code/model.py @@ -114,7 +114,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 = {} @@ -205,8 +205,14 @@ 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=( + is_prerelease + if is_prerelease is not None + else bool( + self.model.value( + onto_iri, SHACL2CODE.isPreRelease, default=False + ) + ) ), ) self.ontologies.append(o) diff --git a/tests/test_python.py b/tests/test_python.py index 18aed753..87ced4ff 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -106,7 +106,9 @@ def model_script(tmp_path_factory, python_model): module_path, module_name = python_model script = tmp_directory / "script.py" - script.write_text(textwrap.dedent(f"""\ + script.write_text( + textwrap.dedent( + f"""\ #! /usr/bin/env python3 import sys sys.path.append("{module_path}") @@ -114,7 +116,9 @@ def model_script(tmp_path_factory, python_model): import {module_name} sys.exit({module_name}.main()) - """)) + """ + ) + ) script.chmod(0o755) yield script @@ -262,7 +266,9 @@ def python_usage_script(python_model_env, tmp_path): env, module_name = python_model_env script_path = tmp_path / "script.py" - script_path.write_text(textwrap.dedent(f""" + script_path.write_text( + textwrap.dedent( + f""" #! /usr/bin/env python3 from typing import ClassVar, Iterable, List, Union import {module_name} @@ -292,7 +298,9 @@ def test3(a: {module_name}.link_class, b: {module_name}.link_class) -> bool: def test4(lst: Iterable[{module_name}.SHACLObject]) -> List[{module_name}.SHACLObject]: return sorted(lst) - """)) + """ + ) + ) # Validate the script runs subprocess.run([sys.executable, script_path], env=env, check=True) @@ -2306,3 +2314,95 @@ def test_prerelease_warning(model): with pytest.warns(FutureWarning): model.test_class() + + +def test_pre_release_cli_option(tmp_path_factory, model_context_url): + tmp_directory = tmp_path_factory.mktemp("prerelease_test") + module_name = "pymodel_prerelease" + output_dir = tmp_directory / module_name + shacl2code_generate( + [ + "--input", + str(TEST_MODEL), + "--context", + model_context_url, + "--pre-release", + ], + [ + "--version", + MODEL_VERSION, + ], + output_dir, + ) + + sys.path.append(str(tmp_directory)) + try: + m = importlib.import_module(module_name) + assert m.SHACL2CODE_TEST.is_prerelease is True + finally: + sys.path.remove(str(tmp_directory)) + + +def test_no_pre_release_cli_option(tmp_path, model_context_url): + ttl_content = """ +@base . +@prefix rdf: . +@prefix rdfs: . +@prefix owl: . +@prefix sh-to-code: . + + a owl:Ontology ; + rdfs:comment "A test ontology" ; + rdfs:label "shacl2code-test" ; + owl:versionInfo "1.0.0" ; + sh-to-code:isPreRelease true . +""" + ttl_file = tmp_path / "prerelease.ttl" + ttl_file.write_text(ttl_content) + + module_name = "pymodel_default_true" + output_dir = tmp_path / module_name + shacl2code_generate( + [ + "--input", + str(ttl_file), + "--context", + model_context_url, + ], + [ + "--version", + MODEL_VERSION, + ], + output_dir, + ) + + sys.path.append(str(tmp_path)) + try: + m = importlib.import_module(module_name) + assert m.SHACL2CODE_TEST.is_prerelease is True + finally: + sys.path.remove(str(tmp_path)) + + module_name_no = "pymodel_no_prerelease" + output_dir_no = tmp_path / module_name_no + shacl2code_generate( + [ + "--input", + str(ttl_file), + "--context", + model_context_url, + "--no-pre-release", + ], + [ + "--version", + MODEL_VERSION, + ], + output_dir_no, + ) + + sys.path.append(str(tmp_path)) + try: + m = importlib.import_module(module_name_no) + assert m.SHACL2CODE_TEST.is_prerelease is False + finally: + sys.path.remove(str(tmp_path)) From be725f151e2ca8b6f399963e93cb3c75115e1988 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sun, 19 Jul 2026 16:18:40 +0100 Subject: [PATCH 02/12] Fix format Signed-off-by: Arthit Suriyawongkul --- tests/test_python.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/tests/test_python.py b/tests/test_python.py index 87ced4ff..bc6e0b83 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -106,9 +106,7 @@ def model_script(tmp_path_factory, python_model): module_path, module_name = python_model script = tmp_directory / "script.py" - script.write_text( - textwrap.dedent( - f"""\ + script.write_text(textwrap.dedent(f"""\ #! /usr/bin/env python3 import sys sys.path.append("{module_path}") @@ -116,9 +114,7 @@ def model_script(tmp_path_factory, python_model): import {module_name} sys.exit({module_name}.main()) - """ - ) - ) + """)) script.chmod(0o755) yield script @@ -266,9 +262,7 @@ def python_usage_script(python_model_env, tmp_path): env, module_name = python_model_env script_path = tmp_path / "script.py" - script_path.write_text( - textwrap.dedent( - f""" + script_path.write_text(textwrap.dedent(f""" #! /usr/bin/env python3 from typing import ClassVar, Iterable, List, Union import {module_name} @@ -298,9 +292,7 @@ def test3(a: {module_name}.link_class, b: {module_name}.link_class) -> bool: def test4(lst: Iterable[{module_name}.SHACLObject]) -> List[{module_name}.SHACLObject]: return sorted(lst) - """ - ) - ) + """)) # Validate the script runs subprocess.run([sys.executable, script_path], env=env, check=True) From 507e31e41ef876d40967d5384e14287a83c63041 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sun, 19 Jul 2026 16:46:15 +0100 Subject: [PATCH 03/12] Support additional ontology pre-release annotations Signed-off-by: Arthit Suriyawongkul --- src/shacl2code/model.py | 85 +++++++++++++-- tests/test_python.py | 227 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 299 insertions(+), 13 deletions(-) diff --git a/src/shacl2code/model.py b/src/shacl2code/model.py index ee5f48b3..e36c0be3 100644 --- a/src/shacl2code/model.py +++ b/src/shacl2code/model.py @@ -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 @@ -19,6 +20,8 @@ XSD, ) +from .util import convert_version_string + PATTERN_DATATYPES = [ str(XSD.string), str(XSD.dateTime), @@ -197,6 +200,78 @@ def is_abstract(s): return False + def is_semver_prerelease(version_str): + if re.search(r"-[0-9a-zA-Z]", version_str): + return True + if re.search( + r"\b(alpha|beta|rc|snapshot|dev)\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) + + # 3) & 4) adms:status + adms_status = self.model.value( + onto_iri, URIRef("http://www.w3.org/ns/adms#status") + ) + if adms_status is not None: + adms_status_str = str(adms_status) + # 3) adms:status (EU SEMIC vocab) + if adms_status_str.startswith( + "http://publications.europa.eu/resource/authority/dataset-status/" + ): + return ( + adms_status_str + == "http://publications.europa.eu/resource/authority/dataset-status/DEVELOP" + ) + # 4) adms:status (Original ADMS vocab) + if adms_status_str.startswith("http://purl.org/adms/status/"): + return ( + adms_status_str + == "http://purl.org/adms/status/UnderDevelopment" + ) + + # 5) schema:creativeWorkStatus + schema_status = self.model.value( + onto_iri, URIRef("http://schema.org/creativeWorkStatus") + ) or self.model.value( + onto_iri, URIRef("https://schema.org/creativeWorkStatus") + ) + if schema_status is not None: + return str(schema_status) in ("Draft", "Incomplete") + + # 6) vs:term_status + vs_status = self.model.value( + onto_iri, + URIRef("http://www.w3.org/2003/06/sw-vocab-status/ns#term_status"), + ) + if vs_status is not None: + return str(vs_status) in ("unstable", "testing") + + # 7) & 8) owl:versionInfo + version = self.model.value(onto_iri, OWL.versionInfo) + if version is not None: + version_str = str(version) + # 7) owl:versionInfo (pre-release extension e.g., "-beta", "-alpha", "-rc" etc) + if is_semver_prerelease(version_str): + return True + # 8) 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( @@ -205,15 +280,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=( - is_prerelease - if is_prerelease is not None - else bool( - self.model.value( - onto_iri, SHACL2CODE.isPreRelease, default=False - ) - ) - ), + is_prerelease=get_is_prerelease(onto_iri), ) self.ontologies.append(o) diff --git a/tests/test_python.py b/tests/test_python.py index bc6e0b83..64dc8cc7 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -106,7 +106,9 @@ def model_script(tmp_path_factory, python_model): module_path, module_name = python_model script = tmp_directory / "script.py" - script.write_text(textwrap.dedent(f"""\ + script.write_text( + textwrap.dedent( + f"""\ #! /usr/bin/env python3 import sys sys.path.append("{module_path}") @@ -114,7 +116,9 @@ def model_script(tmp_path_factory, python_model): import {module_name} sys.exit({module_name}.main()) - """)) + """ + ) + ) script.chmod(0o755) yield script @@ -262,7 +266,9 @@ def python_usage_script(python_model_env, tmp_path): env, module_name = python_model_env script_path = tmp_path / "script.py" - script_path.write_text(textwrap.dedent(f""" + script_path.write_text( + textwrap.dedent( + f""" #! /usr/bin/env python3 from typing import ClassVar, Iterable, List, Union import {module_name} @@ -292,7 +298,9 @@ def test3(a: {module_name}.link_class, b: {module_name}.link_class) -> bool: def test4(lst: Iterable[{module_name}.SHACLObject]) -> List[{module_name}.SHACLObject]: return sorted(lst) - """)) + """ + ) + ) # Validate the script runs subprocess.run([sys.executable, script_path], env=env, check=True) @@ -2398,3 +2406,214 @@ def test_no_pre_release_cli_option(tmp_path, model_context_url): assert m.SHACL2CODE_TEST.is_prerelease is False finally: sys.path.remove(str(tmp_path)) + + +def test_pre_release_annotations_cases(tmp_path, model_context_url): + cases = [ + # 2) sh-to-code:isPreRelease + ("sh-to-code:isPreRelease true .", True), + ("sh-to-code:isPreRelease false .", False), + # 3) adms:status (EU SEMIC vocab) + ( + "adms:status .", + True, + ), + ( + "adms:status .", + False, + ), + # 4) adms:status (Original ADMS vocab) + ("adms:status .", True), + ("adms:status .", False), + # 5) schema:creativeWorkStatus + ('schema:creativeWorkStatus "Draft" .', True), + ('schema:creativeWorkStatus "Incomplete" .', True), + ('schema:creativeWorkStatus "Published" .', False), + # 6) vs:term_status + ('vs:term_status "testing" .', True), + ('vs:term_status "unstable" .', True), + ('vs:term_status "stable" .', False), + # 7) owl:versionInfo (pre-release extension) + ('owl:versionInfo "3.1.0-rc2" .', True), + ('owl:versionInfo "1.2.1-SNAPSHOT" .', True), + ('owl:versionInfo "1.0.0.alpha" .', True), + ('owl:versionInfo "1.0.0" .', False), + # 8) owl:versionInfo (major version zero) + ('owl:versionInfo "0.7.1" .', True), + ('owl:versionInfo "0.0.1" .', True), + ] + + for idx, (annotations, expected) in enumerate(cases): + ttl_content = f""" +@base . +@prefix rdf: . +@prefix rdfs: . +@prefix owl: . +@prefix sh-to-code: . +@prefix adms: . +@prefix schema: . +@prefix vs: . + + a owl:Ontology ; + rdfs:comment "A test ontology" ; + rdfs:label "shacl2code-test" ; + {annotations} +""" + ttl_file = tmp_path / f"case_{idx}.ttl" + ttl_file.write_text(ttl_content) + + module_name = f"pymodel_case_{idx}" + output_dir = tmp_path / module_name + shacl2code_generate( + [ + "--input", + str(ttl_file), + "--context", + model_context_url, + ], + [ + "--version", + MODEL_VERSION, + ], + output_dir, + ) + + sys.path.append(str(tmp_path)) + try: + m = importlib.import_module(module_name) + assert ( + m.SHACL2CODE_TEST.is_prerelease is expected + ), f"Failed for case {idx}: {annotations}" + finally: + sys.path.remove(str(tmp_path)) + + +def test_pre_release_precedence(tmp_path, model_context_url): + # Example 1: + # 2) sh-to-code:isPreRelease false (False) + # 3) adms:status EU SEMIC DEVELOP (True) + # Expected: False (sh-to-code has higher precedence) + ttl_content_1 = """ +@base . +@prefix rdf: . +@prefix rdfs: . +@prefix owl: . +@prefix sh-to-code: . +@prefix adms: . + + a owl:Ontology ; + rdfs:label "shacl2code-test" ; + sh-to-code:isPreRelease false ; + adms:status . +""" + ttl_file_1 = tmp_path / "prec_1.ttl" + ttl_file_1.write_text(ttl_content_1) + + module_name_1 = "pymodel_prec_1" + output_dir_1 = tmp_path / module_name_1 + shacl2code_generate( + [ + "--input", + str(ttl_file_1), + "--context", + model_context_url, + ], + [ + "--version", + MODEL_VERSION, + ], + output_dir_1, + ) + + sys.path.append(str(tmp_path)) + try: + m = importlib.import_module(module_name_1) + assert m.SHACL2CODE_TEST.is_prerelease is False + finally: + sys.path.remove(str(tmp_path)) + + # Example 2: + # 5) schema:creativeWorkStatus "Published" (False) + # 6) vs:term_status "testing" (True) + # Expected: False (creativeWorkStatus has higher precedence) + ttl_content_2 = """ +@base . +@prefix rdf: . +@prefix rdfs: . +@prefix owl: . +@prefix schema: . +@prefix vs: . + + a owl:Ontology ; + rdfs:label "shacl2code-test" ; + schema:creativeWorkStatus "Published" ; + vs:term_status "testing" . +""" + ttl_file_2 = tmp_path / "prec_2.ttl" + ttl_file_2.write_text(ttl_content_2) + + module_name_2 = "pymodel_prec_2" + output_dir_2 = tmp_path / module_name_2 + shacl2code_generate( + [ + "--input", + str(ttl_file_2), + "--context", + model_context_url, + ], + [ + "--version", + MODEL_VERSION, + ], + output_dir_2, + ) + + sys.path.append(str(tmp_path)) + try: + m = importlib.import_module(module_name_2) + assert m.SHACL2CODE_TEST.is_prerelease is False + finally: + sys.path.remove(str(tmp_path)) + + # Example 3: + # 3) adms:status EU SEMIC DEVELOP (True) + # 5) schema:creativeWorkStatus "Published" (False) + # Expected: True (adms:status has higher precedence) + ttl_content_3 = """ +@base . +@prefix rdf: . +@prefix rdfs: . +@prefix owl: . +@prefix adms: . +@prefix schema: . + + a owl:Ontology ; + rdfs:label "shacl2code-test" ; + adms:status ; + schema:creativeWorkStatus "Published" . +""" + ttl_file_3 = tmp_path / "prec_3.ttl" + ttl_file_3.write_text(ttl_content_3) + + module_name_3 = "pymodel_prec_3" + output_dir_3 = tmp_path / module_name_3 + shacl2code_generate( + [ + "--input", + str(ttl_file_3), + "--context", + model_context_url, + ], + [ + "--version", + MODEL_VERSION, + ], + output_dir_3, + ) + + sys.path.append(str(tmp_path)) + try: + m = importlib.import_module(module_name_3) + assert m.SHACL2CODE_TEST.is_prerelease is True + finally: + sys.path.remove(str(tmp_path)) From 9fc8346b84ec12b0d637be19c377d74d5558a430 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sun, 19 Jul 2026 17:05:07 +0100 Subject: [PATCH 04/12] Update README.md with pre-release annotations and precedence documentation Signed-off-by: Arthit Suriyawongkul --- README.md | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 7fabdd34..4dc03bcd 100644 --- a/README.md +++ b/README.md @@ -269,18 +269,32 @@ 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: - -```ttl - - a ow:Ontology ; - sh-to-code:isPreRelease true - . -``` +`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`). + +Pre-release status can be specified explicitly via command-line options, or inferred automatically from various ontology annotations. + +#### Order of Precedence + +In the event of conflicting annotations, `shacl2code` evaluates pre-release status using the following order of precedence (highest priority first): + +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 custom boolean annotation (`sh-to-code:isPreRelease true` or `false`). +3. **`adms:status` (EU SEMIC Vocabulary)**: + If the status is ``, 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 ``, it is considered a pre-release. Other values in the ADMS status namespace (e.g. `Completed`) indicate a stable release. +5. **`schema:creativeWorkStatus`**: + If set to `"Draft"` or `"Incomplete"`, it is considered a pre-release. Other values (e.g. `"Published"`) indicate a stable release. +6. **`vs:term_status`**: + If set to `"unstable"` or `"testing"`, it is considered a pre-release. Other values (e.g. `"stable"`) indicate a stable release. +7. **`owl:versionInfo` (pre-release extension)**: + If the version string contains a pre-release extension suffix (e.g., `-beta`, `-alpha`, `-rc`, `-SNAPSHOT`, `.alpha`, etc.). +8. **`owl:versionInfo` (major version zero)**: + If the version string corresponds to a major version zero in semantic versioning (e.g., `0.7.1`). +9. **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. From 035c328b83324bc96ef6ed0dd4f110e44f644534 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sun, 19 Jul 2026 17:22:53 +0100 Subject: [PATCH 05/12] Update README example Signed-off-by: Arthit Suriyawongkul --- README.md | 50 ++++++++++++++++++++++++++++++----------- src/shacl2code/model.py | 2 +- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 4dc03bcd..2c2351df 100644 --- a/README.md +++ b/README.md @@ -269,35 +269,59 @@ type: `http://spdx.invalid./AbstractClass`, but this is not preferred. ### Pre-Release models -`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`). +`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`). -Pre-release status can be specified explicitly via command-line options, or inferred automatically from various ontology annotations. +Pre-release status can be specified explicitly via command-line options, +or inferred automatically from various ontology annotations. For example, -#### Order of Precedence +```ttl + a ow:Ontology ; + sh-to-code:isPreRelease true + . +``` -In the event of conflicting annotations, `shacl2code` evaluates pre-release status using the following order of precedence (highest priority first): +In the event of conflicting annotations, `shacl2code` evaluates pre-release +status using the following order of precedence (highest priority first): 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. + Explicitly marks the generated bindings as pre-release or stable, + overriding any annotations in the input ontology. 2. **`sh-to-code:isPreRelease`**: - The custom boolean annotation (`sh-to-code:isPreRelease true` or `false`). + The `shacl2code` custom boolean annotation + (`sh-to-code:isPreRelease true` or `false`). 3. **`adms:status` (EU SEMIC Vocabulary)**: - If the status is ``, it is considered a pre-release. Other values in the dataset-status vocabulary space (e.g. `COMPLETED`) indicate a stable release. + If the status is + ``, + 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 ``, it is considered a pre-release. Other values in the ADMS status namespace (e.g. `Completed`) indicate a stable release. + If the status is ``, + it is considered a pre-release. + Other values in the ADMS status namespace (e.g. `Completed`) + indicate a stable release. 5. **`schema:creativeWorkStatus`**: - If set to `"Draft"` or `"Incomplete"`, it is considered a pre-release. Other values (e.g. `"Published"`) indicate a stable release. + If set to `"Draft"` or `"Incomplete"`, it is considered a pre-release. + Other values (e.g. `"Published"`) indicate a stable release. 6. **`vs:term_status`**: - If set to `"unstable"` or `"testing"`, it is considered a pre-release. Other values (e.g. `"stable"`) indicate a stable release. + If set to `"unstable"` or `"testing"`, it is considered a pre-release. + Other values (e.g. `"stable"`) indicate a stable release. 7. **`owl:versionInfo` (pre-release extension)**: - If the version string contains a pre-release extension suffix (e.g., `-beta`, `-alpha`, `-rc`, `-SNAPSHOT`, `.alpha`, etc.). + If the version string contains a pre-release extension suffix + (e.g., `-alpha`, `-beta`, `-dev`, `-rc`, `-SNAPSHOT`, `.alpha`, etc.). 8. **`owl:versionInfo` (major version zero)**: - If the version string corresponds to a major version zero in semantic versioning (e.g., `0.7.1`). + If the version string corresponds to a major version zero in + [Semantic Versioning][semver] (e.g., `0.7.1`). 9. **Default Fallback**: - If none of the above are present, the ontology is assumed to be a stable release (`false`). + 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/ diff --git a/src/shacl2code/model.py b/src/shacl2code/model.py index e36c0be3..697f98b1 100644 --- a/src/shacl2code/model.py +++ b/src/shacl2code/model.py @@ -204,7 +204,7 @@ def is_semver_prerelease(version_str): if re.search(r"-[0-9a-zA-Z]", version_str): return True if re.search( - r"\b(alpha|beta|rc|snapshot|dev)\b", version_str, re.IGNORECASE + r"\b(alpha|beta|dev|pre|rc|snapshot|test)\b", version_str, re.IGNORECASE ): return True return False From 2e082774d048263d349360721acd585255e3bea7 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sun, 19 Jul 2026 17:26:05 +0100 Subject: [PATCH 06/12] Fix format Signed-off-by: Arthit Suriyawongkul --- tests/test_python.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/tests/test_python.py b/tests/test_python.py index 64dc8cc7..83607700 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -106,9 +106,7 @@ def model_script(tmp_path_factory, python_model): module_path, module_name = python_model script = tmp_directory / "script.py" - script.write_text( - textwrap.dedent( - f"""\ + script.write_text(textwrap.dedent(f"""\ #! /usr/bin/env python3 import sys sys.path.append("{module_path}") @@ -116,9 +114,7 @@ def model_script(tmp_path_factory, python_model): import {module_name} sys.exit({module_name}.main()) - """ - ) - ) + """)) script.chmod(0o755) yield script @@ -266,9 +262,7 @@ def python_usage_script(python_model_env, tmp_path): env, module_name = python_model_env script_path = tmp_path / "script.py" - script_path.write_text( - textwrap.dedent( - f""" + script_path.write_text(textwrap.dedent(f""" #! /usr/bin/env python3 from typing import ClassVar, Iterable, List, Union import {module_name} @@ -298,9 +292,7 @@ def test3(a: {module_name}.link_class, b: {module_name}.link_class) -> bool: def test4(lst: Iterable[{module_name}.SHACLObject]) -> List[{module_name}.SHACLObject]: return sorted(lst) - """ - ) - ) + """)) # Validate the script runs subprocess.run([sys.executable, script_path], env=env, check=True) From 58e8951496112352ab3526c492fd5d756e39cf87 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sun, 19 Jul 2026 17:32:36 +0100 Subject: [PATCH 07/12] Limit pyshacl to 0.40.0 for Python 3.9 support Signed-off-by: Arthit Suriyawongkul --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e58cfe30..d73a3601 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", "pytest >= 7.4", "pytest-cov >= 4.1", "pytest-xdist >= 3.5", From 1c2fb180e4fcfa2943851152f856a45ea86faf61 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sun, 19 Jul 2026 17:40:30 +0100 Subject: [PATCH 08/12] Add pyshacl version comment Signed-off-by: Arthit Suriyawongkul --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d73a3601..339a0722 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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, < 0.40.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", From 11ae04e3b41039710d893e470c5f9be9ae1b99ca Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Mon, 20 Jul 2026 02:59:53 +0100 Subject: [PATCH 09/12] Add no annotations fallback test Signed-off-by: Arthit Suriyawongkul --- tests/test_python.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_python.py b/tests/test_python.py index 83607700..91b71d1c 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -2433,6 +2433,8 @@ def test_pre_release_annotations_cases(tmp_path, model_context_url): # 8) owl:versionInfo (major version zero) ('owl:versionInfo "0.7.1" .', True), ('owl:versionInfo "0.0.1" .', True), + # Fallback (no annotations or versionInfo at all) + ('rdfs:comment "A test ontology" .', False), ] for idx, (annotations, expected) in enumerate(cases): From ccfb46a307234e3cb74a84330e9f4da9c182f91f Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Mon, 20 Jul 2026 03:13:12 +0100 Subject: [PATCH 10/12] Handle multiple values Signed-off-by: Arthit Suriyawongkul --- src/shacl2code/model.py | 90 +++++++++++++++++++++++++---------------- tests/test_python.py | 89 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 35 deletions(-) diff --git a/src/shacl2code/model.py b/src/shacl2code/model.py index 697f98b1..5dbbe2b2 100644 --- a/src/shacl2code/model.py +++ b/src/shacl2code/model.py @@ -201,7 +201,10 @@ def is_abstract(s): return False def is_semver_prerelease(version_str): - if re.search(r"-[0-9a-zA-Z]", 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 @@ -220,54 +223,71 @@ def get_is_prerelease(onto_iri): return bool(val) # 3) & 4) adms:status - adms_status = self.model.value( - onto_iri, URIRef("http://www.w3.org/ns/adms#status") + adms_statuses = list( + self.model.objects(onto_iri, URIRef("http://www.w3.org/ns/adms#status")) ) - if adms_status is not None: - adms_status_str = str(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 adms_status_str.startswith( - "http://publications.europa.eu/resource/authority/dataset-status/" - ): - return ( - adms_status_str + 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 adms_status_str.startswith("http://purl.org/adms/status/"): - return ( - adms_status_str - == "http://purl.org/adms/status/UnderDevelopment" + if original: + return any( + s == "http://purl.org/adms/status/UnderDevelopment" + for s in original ) # 5) schema:creativeWorkStatus - schema_status = self.model.value( - onto_iri, URIRef("http://schema.org/creativeWorkStatus") - ) or self.model.value( - onto_iri, URIRef("https://schema.org/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_status is not None: - return str(schema_status) in ("Draft", "Incomplete") + if schema_statuses: + return any(str(s) in ("Draft", "Incomplete") for s in schema_statuses) # 6) vs:term_status - vs_status = self.model.value( - onto_iri, - URIRef("http://www.w3.org/2003/06/sw-vocab-status/ns#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_status is not None: - return str(vs_status) in ("unstable", "testing") + if vs_statuses: + return any(str(s) in ("unstable", "testing") for s in vs_statuses) # 7) & 8) owl:versionInfo - version = self.model.value(onto_iri, OWL.versionInfo) - if version is not None: - version_str = str(version) - # 7) owl:versionInfo (pre-release extension e.g., "-beta", "-alpha", "-rc" etc) - if is_semver_prerelease(version_str): - return True - # 8) owl:versionInfo (major version zero) - parts = convert_version_string(version_str) - if parts and parts[0] == 0: - return True + versions = list(self.model.objects(onto_iri, OWL.versionInfo)) + if versions: + for version in versions: + version_str = str(version) + # 7) owl:versionInfo (pre-release extension e.g., "-beta", "-alpha", "-rc" etc) + if is_semver_prerelease(version_str): + return True + # 8) owl:versionInfo (major version zero) + parts = convert_version_string(version_str) + if parts and parts[0] == 0: + return True return False return False diff --git a/tests/test_python.py b/tests/test_python.py index 91b71d1c..c5e84e41 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -2401,6 +2401,8 @@ def test_no_pre_release_cli_option(tmp_path, model_context_url): def test_pre_release_annotations_cases(tmp_path, model_context_url): + # The number in the comment indicates the precedence of the annotation. + # 1 is the highest precedence (force by command line option) cases = [ # 2) sh-to-code:isPreRelease ("sh-to-code:isPreRelease true .", True), @@ -2433,6 +2435,9 @@ def test_pre_release_annotations_cases(tmp_path, model_context_url): # 8) owl:versionInfo (major version zero) ('owl:versionInfo "0.7.1" .', True), ('owl:versionInfo "0.0.1" .', True), + # date-like version must not be mistaken for a semver pre-release + # suffix (no dotted numeric core precedes the hyphen) + ('owl:versionInfo "2024-01-15" .', False), # Fallback (no annotations or versionInfo at all) ('rdfs:comment "A test ontology" .', False), ] @@ -2611,3 +2616,87 @@ def test_pre_release_precedence(tmp_path, model_context_url): assert m.SHACL2CODE_TEST.is_prerelease is True finally: sys.path.remove(str(tmp_path)) + + +def test_pre_release_multi_valued_annotations(tmp_path, model_context_url): + # Each of these predicates can legally repeat. + # A stable-looking value listed first must not hide + # a pre-release-indicating value listed after it. + cases = [ + # owl:versionInfo: first value stable, second is a semver + # pre-release extension. + ( + """ +owl:versionInfo "1.0.0" ; +owl:versionInfo "2.0.0-beta" . +""", + True, + ), + # adms:status (EU SEMIC vocab): first value stable, second under + # development. + ( + """ +adms:status ; +adms:status . +""", + True, + ), + # schema:creativeWorkStatus: first value stable, second draft. + ( + """ +schema:creativeWorkStatus "Published" ; +schema:creativeWorkStatus "Draft" . +""", + True, + ), + # vs:term_status: first value stable, second testing. + ( + """ +vs:term_status "stable" ; +vs:term_status "testing" . +""", + True, + ), + ] + + for idx, (annotations, expected) in enumerate(cases): + ttl_content = f""" +@base . +@prefix rdf: . +@prefix rdfs: . +@prefix owl: . +@prefix adms: . +@prefix schema: . +@prefix vs: . + + a owl:Ontology ; + rdfs:label "shacl2code-test" ; + {annotations} +""" + ttl_file = tmp_path / f"multi_{idx}.ttl" + ttl_file.write_text(ttl_content) + + module_name = f"pymodel_multi_{idx}" + output_dir = tmp_path / module_name + shacl2code_generate( + [ + "--input", + str(ttl_file), + "--context", + model_context_url, + ], + [ + "--version", + MODEL_VERSION, + ], + output_dir, + ) + + sys.path.append(str(tmp_path)) + try: + m = importlib.import_module(module_name) + assert ( + m.SHACL2CODE_TEST.is_prerelease is expected + ), f"Failed for case {idx}: {annotations}" + finally: + sys.path.remove(str(tmp_path)) From 8ce78a862f7299c7c059f7176eaf16b830e46f14 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Mon, 20 Jul 2026 16:52:20 +0100 Subject: [PATCH 11/12] Add BIBO ontology draft Signed-off-by: Arthit Suriyawongkul --- README.md | 21 +++++--- src/shacl2code/model.py | 23 +++++++-- tests/test_python.py | 106 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 136 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 2c2351df..ce8a1b98 100644 --- a/README.md +++ b/README.md @@ -284,7 +284,7 @@ or inferred automatically from various ontology annotations. For example, ``` In the event of conflicting annotations, `shacl2code` evaluates pre-release -status using the following order of precedence (highest priority first): +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, @@ -303,21 +303,26 @@ status using the following order of precedence (highest priority first): it is considered a pre-release. Other values in the ADMS status namespace (e.g. `Completed`) indicate a stable release. -5. **`schema:creativeWorkStatus`**: +5. **`bibo:status` (Bibliographic Ontology)**: + If set to ``, 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. -6. **`vs:term_status`**: +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. -7. **`owl:versionInfo` (pre-release extension)**: +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.). -8. **`owl:versionInfo` (major version zero)**: +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`). -9. **Default Fallback**: - If none of the above are present, the ontology is assumed to be a stable - release (`false`). +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. diff --git a/src/shacl2code/model.py b/src/shacl2code/model.py index 5dbbe2b2..9838cbde 100644 --- a/src/shacl2code/model.py +++ b/src/shacl2code/model.py @@ -222,7 +222,7 @@ def get_is_prerelease(onto_iri): if val is not None: return bool(val) - # 3) & 4) adms:status + # 3) adms:status adms_statuses = list( self.model.objects(onto_iri, URIRef("http://www.w3.org/ns/adms#status")) ) @@ -234,7 +234,7 @@ def get_is_prerelease(onto_iri): "http://publications.europa.eu/resource/authority/dataset-status/" ) ] - # 3) adms:status (EU SEMIC vocab) + # 3.1) adms:status (EU SEMIC vocab) if semic: return any( s @@ -246,13 +246,30 @@ def get_is_prerelease(onto_iri): for s in adms_statuses if str(s).startswith("http://purl.org/adms/status/") ] - # 4) adms:status (Original ADMS vocab) + # 3.2) adms:status (Original ADMS vocab) if original: return any( s == "http://purl.org/adms/status/UnderDevelopment" for s in original ) + # 4) bibo:status (Bibliographic Ontology) + bibo_statuses = list( + self.model.objects( + onto_iri, URIRef("http://purl.org/ontology/bibo/status") + ) + ) + if bibo_statuses: + bibo = [ + str(s) + for s in bibo_statuses + if str(s).startswith("http://purl.org/ontology/bibo/status/") + ] + if bibo: + return any( + s == "http://purl.org/ontology/bibo/status/draft" for s in bibo + ) + # 5) schema:creativeWorkStatus schema_statuses = list( self.model.objects( diff --git a/tests/test_python.py b/tests/test_python.py index c5e84e41..b0e513a1 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -2407,7 +2407,7 @@ def test_pre_release_annotations_cases(tmp_path, model_context_url): # 2) sh-to-code:isPreRelease ("sh-to-code:isPreRelease true .", True), ("sh-to-code:isPreRelease false .", False), - # 3) adms:status (EU SEMIC vocab) + # 3.1) adms:status (EU SEMIC vocab) ( "adms:status .", True, @@ -2416,9 +2416,13 @@ def test_pre_release_annotations_cases(tmp_path, model_context_url): "adms:status .", False, ), - # 4) adms:status (Original ADMS vocab) + # 3.2) adms:status (Original ADMS vocab) ("adms:status .", True), ("adms:status .", False), + # 4) bibo:status (Bibliographic Ontology) + ("bibo:status .", True), + ("bibo:status .", False), + ("bibo:status .", False), # 5) schema:creativeWorkStatus ('schema:creativeWorkStatus "Draft" .', True), ('schema:creativeWorkStatus "Incomplete" .', True), @@ -2452,6 +2456,7 @@ def test_pre_release_annotations_cases(tmp_path, model_context_url): @prefix adms: . @prefix schema: . @prefix vs: . +@prefix bibo: . a owl:Ontology ; rdfs:comment "A test ontology" ; @@ -2575,7 +2580,7 @@ def test_pre_release_precedence(tmp_path, model_context_url): sys.path.remove(str(tmp_path)) # Example 3: - # 3) adms:status EU SEMIC DEVELOP (True) + # 3.1) adms:status EU SEMIC DEVELOP (True) # 5) schema:creativeWorkStatus "Published" (False) # Expected: True (adms:status has higher precedence) ttl_content_3 = """ @@ -2617,6 +2622,92 @@ def test_pre_release_precedence(tmp_path, model_context_url): finally: sys.path.remove(str(tmp_path)) + # Example 4: + # 3.2) adms:status Original ADMS status Completed (False) + # 4) bibo:status draft (True) + # Expected: False (adms:status has higher precedence) + ttl_content_4 = """ +@base . +@prefix rdf: . +@prefix rdfs: . +@prefix owl: . +@prefix adms: . +@prefix bibo: . + + a owl:Ontology ; + rdfs:label "shacl2code-test" ; + adms:status ; + bibo:status . +""" + ttl_file_4 = tmp_path / "prec_4.ttl" + ttl_file_4.write_text(ttl_content_4) + + module_name_4 = "pymodel_prec_4" + output_dir_4 = tmp_path / module_name_4 + shacl2code_generate( + [ + "--input", + str(ttl_file_4), + "--context", + model_context_url, + ], + [ + "--version", + MODEL_VERSION, + ], + output_dir_4, + ) + + sys.path.append(str(tmp_path)) + try: + m = importlib.import_module(module_name_4) + assert m.SHACL2CODE_TEST.is_prerelease is False + finally: + sys.path.remove(str(tmp_path)) + + # Example 5: + # 4) bibo:status published (False) + # 5) schema:creativeWorkStatus Draft (True) + # Expected: False (bibo:status has higher precedence) + ttl_content_5 = """ +@base . +@prefix rdf: . +@prefix rdfs: . +@prefix owl: . +@prefix schema: . +@prefix bibo: . + + a owl:Ontology ; + rdfs:label "shacl2code-test" ; + bibo:status ; + schema:creativeWorkStatus "Draft" . +""" + ttl_file_5 = tmp_path / "prec_5.ttl" + ttl_file_5.write_text(ttl_content_5) + + module_name_5 = "pymodel_prec_5" + output_dir_5 = tmp_path / module_name_5 + shacl2code_generate( + [ + "--input", + str(ttl_file_5), + "--context", + model_context_url, + ], + [ + "--version", + MODEL_VERSION, + ], + output_dir_5, + ) + + sys.path.append(str(tmp_path)) + try: + m = importlib.import_module(module_name_5) + assert m.SHACL2CODE_TEST.is_prerelease is False + finally: + sys.path.remove(str(tmp_path)) + def test_pre_release_multi_valued_annotations(tmp_path, model_context_url): # Each of these predicates can legally repeat. @@ -2654,6 +2745,14 @@ def test_pre_release_multi_valued_annotations(tmp_path, model_context_url): """ vs:term_status "stable" ; vs:term_status "testing" . +""", + True, + ), + # bibo:status: first value published, second draft. + ( + """ +bibo:status ; +bibo:status . """, True, ), @@ -2668,6 +2767,7 @@ def test_pre_release_multi_valued_annotations(tmp_path, model_context_url): @prefix adms: . @prefix schema: . @prefix vs: . +@prefix bibo: . a owl:Ontology ; rdfs:label "shacl2code-test" ; From c0f966eac6126ad4e2626c355309d600d61b6566 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Mon, 20 Jul 2026 18:28:59 +0100 Subject: [PATCH 12/12] Renumberred Signed-off-by: Arthit Suriyawongkul --- src/shacl2code/model.py | 28 +++++++++++----------------- tests/test_python.py | 30 +++++++++++++++--------------- 2 files changed, 26 insertions(+), 32 deletions(-) diff --git a/src/shacl2code/model.py b/src/shacl2code/model.py index 9838cbde..d327af95 100644 --- a/src/shacl2code/model.py +++ b/src/shacl2code/model.py @@ -222,7 +222,6 @@ def get_is_prerelease(onto_iri): if val is not None: return bool(val) - # 3) adms:status adms_statuses = list( self.model.objects(onto_iri, URIRef("http://www.w3.org/ns/adms#status")) ) @@ -234,7 +233,7 @@ def get_is_prerelease(onto_iri): "http://publications.europa.eu/resource/authority/dataset-status/" ) ] - # 3.1) adms:status (EU SEMIC vocab) + # 3) adms:status (EU SEMIC vocab) if semic: return any( s @@ -246,31 +245,26 @@ def get_is_prerelease(onto_iri): for s in adms_statuses if str(s).startswith("http://purl.org/adms/status/") ] - # 3.2) adms:status (Original ADMS vocab) + # 4) adms:status (Original ADMS vocab) if original: return any( s == "http://purl.org/adms/status/UnderDevelopment" for s in original ) - # 4) bibo:status (Bibliographic Ontology) + # 5) bibo:status (Bibliographic Ontology) bibo_statuses = list( self.model.objects( onto_iri, URIRef("http://purl.org/ontology/bibo/status") ) ) if bibo_statuses: - bibo = [ - str(s) + return any( + str(s) == "http://purl.org/ontology/bibo/status/draft" for s in bibo_statuses - if str(s).startswith("http://purl.org/ontology/bibo/status/") - ] - if bibo: - return any( - s == "http://purl.org/ontology/bibo/status/draft" for s in bibo - ) + ) - # 5) schema:creativeWorkStatus + # 6) schema:creativeWorkStatus schema_statuses = list( self.model.objects( onto_iri, URIRef("http://schema.org/creativeWorkStatus") @@ -283,7 +277,7 @@ def get_is_prerelease(onto_iri): if schema_statuses: return any(str(s) in ("Draft", "Incomplete") for s in schema_statuses) - # 6) vs:term_status + # 7) vs:term_status vs_statuses = list( self.model.objects( onto_iri, @@ -293,15 +287,15 @@ def get_is_prerelease(onto_iri): if vs_statuses: return any(str(s) in ("unstable", "testing") for s in vs_statuses) - # 7) & 8) owl:versionInfo + # 8) & 9) owl:versionInfo versions = list(self.model.objects(onto_iri, OWL.versionInfo)) if versions: for version in versions: version_str = str(version) - # 7) owl:versionInfo (pre-release extension e.g., "-beta", "-alpha", "-rc" etc) + # 8) owl:versionInfo (pre-release extension e.g., "-beta", "-alpha", "-rc" etc) if is_semver_prerelease(version_str): return True - # 8) owl:versionInfo (major version zero) + # 9) owl:versionInfo (major version zero) parts = convert_version_string(version_str) if parts and parts[0] == 0: return True diff --git a/tests/test_python.py b/tests/test_python.py index b0e513a1..c77fe94c 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -2407,7 +2407,7 @@ def test_pre_release_annotations_cases(tmp_path, model_context_url): # 2) sh-to-code:isPreRelease ("sh-to-code:isPreRelease true .", True), ("sh-to-code:isPreRelease false .", False), - # 3.1) adms:status (EU SEMIC vocab) + # 3) adms:status (EU SEMIC vocab) ( "adms:status .", True, @@ -2416,27 +2416,27 @@ def test_pre_release_annotations_cases(tmp_path, model_context_url): "adms:status .", False, ), - # 3.2) adms:status (Original ADMS vocab) + # 4) adms:status (Original ADMS vocab) ("adms:status .", True), ("adms:status .", False), - # 4) bibo:status (Bibliographic Ontology) + # 5) bibo:status (Bibliographic Ontology) ("bibo:status .", True), ("bibo:status .", False), ("bibo:status .", False), - # 5) schema:creativeWorkStatus + # 6) schema:creativeWorkStatus ('schema:creativeWorkStatus "Draft" .', True), ('schema:creativeWorkStatus "Incomplete" .', True), ('schema:creativeWorkStatus "Published" .', False), - # 6) vs:term_status + # 7) vs:term_status ('vs:term_status "testing" .', True), ('vs:term_status "unstable" .', True), ('vs:term_status "stable" .', False), - # 7) owl:versionInfo (pre-release extension) + # 8) owl:versionInfo (pre-release extension) ('owl:versionInfo "3.1.0-rc2" .', True), ('owl:versionInfo "1.2.1-SNAPSHOT" .', True), ('owl:versionInfo "1.0.0.alpha" .', True), ('owl:versionInfo "1.0.0" .', False), - # 8) owl:versionInfo (major version zero) + # 9) owl:versionInfo (major version zero) ('owl:versionInfo "0.7.1" .', True), ('owl:versionInfo "0.0.1" .', True), # date-like version must not be mistaken for a semver pre-release @@ -2537,8 +2537,8 @@ def test_pre_release_precedence(tmp_path, model_context_url): sys.path.remove(str(tmp_path)) # Example 2: - # 5) schema:creativeWorkStatus "Published" (False) - # 6) vs:term_status "testing" (True) + # 6) schema:creativeWorkStatus "Published" (False) + # 7) vs:term_status "testing" (True) # Expected: False (creativeWorkStatus has higher precedence) ttl_content_2 = """ @base . @@ -2580,8 +2580,8 @@ def test_pre_release_precedence(tmp_path, model_context_url): sys.path.remove(str(tmp_path)) # Example 3: - # 3.1) adms:status EU SEMIC DEVELOP (True) - # 5) schema:creativeWorkStatus "Published" (False) + # 3) adms:status EU SEMIC DEVELOP (True) + # 6) schema:creativeWorkStatus "Published" (False) # Expected: True (adms:status has higher precedence) ttl_content_3 = """ @base . @@ -2623,8 +2623,8 @@ def test_pre_release_precedence(tmp_path, model_context_url): sys.path.remove(str(tmp_path)) # Example 4: - # 3.2) adms:status Original ADMS status Completed (False) - # 4) bibo:status draft (True) + # 4) adms:status Original ADMS status Completed (False) + # 5) bibo:status draft (True) # Expected: False (adms:status has higher precedence) ttl_content_4 = """ @base . @@ -2666,8 +2666,8 @@ def test_pre_release_precedence(tmp_path, model_context_url): sys.path.remove(str(tmp_path)) # Example 5: - # 4) bibo:status published (False) - # 5) schema:creativeWorkStatus Draft (True) + # 5) bibo:status published (False) + # 6) schema:creativeWorkStatus Draft (True) # Expected: False (bibo:status has higher precedence) ttl_content_5 = """ @base .