Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Changelog
All notable changes to this project will be documented in this file.

## [1.3.6] - 2026-06-22
### Added
- Improved namespaced property handling and additional unit testing.

## [1.3.5] - 2026-06-22
### Added
- Improved document loading, namespaced property handling and additional unit testing.
Expand Down
6 changes: 2 additions & 4 deletions source/semanticpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@
__version__ = file.read().strip()


logger.debug("%s library (%s) imported from: %s", __name__, __version__, __file__)


class Model(Node):
"""SemanticPy Base Model Class"""

Expand Down Expand Up @@ -580,7 +577,8 @@ def extend(
props = cls._validate_properties(props, prop)

if isinstance(canonical := props.get("canonical"), str):
subclass._canonical[canonical] = prop
if not canonical in subclass._canonical:
subclass._canonical[canonical] = prop

if isinstance(hidden := props.get("hidden"), bool):
if hidden is True:
Expand Down
11 changes: 7 additions & 4 deletions source/semanticpy/types/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,11 +321,14 @@ def _canonicalize(self, name: str) -> str:
elif not len(name := name.strip()) > 0:
raise ValueError("The 'name' argument must have a non-empty string value!")

if name in self._canonical:
if name in self._namespace:
return self._namespace[name] + ":" + self._canonical[name]
if isinstance(canonical := self._canonical.get(name), str):
if isinstance(namespace := self._namespace.get(name), str):
if not canonical.startswith(namespace + ":"):
return namespace + ":" + canonical
else:
return canonical
else:
return self._canonical[name]
return canonical
else:
return name

Expand Down
2 changes: 1 addition & 1 deletion source/semanticpy/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.3.5
1.3.6
25 changes: 25 additions & 0 deletions tests/data/examples/saved-extended.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"@context": "https://linked.art/ns/v1/linked-art.json",
"id": "https://data.example.org/object/123",
"type": "HumanMadeObject",
"_label": "Example Object 123",
"classified_as": [
{
"id": "http://vocab.getty.edu/aat/300133025",
"type": "Type",
"_label": "Works of Art"
}
],
"test:related": [
{
"id": "https://data.example.org/object/456",
"type": "HumanMadeObject",
"_label": "Related Object 456"
},
{
"id": "https://data.example.org/object/789",
"type": "HumanMadeObject",
"_label": "Related Object 789"
}
]
}
66 changes: 65 additions & 1 deletion tests/test_record_save.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
logger = logging.getLogger(__name__)


def test_record_load(factory: callable, data: callable, path: callable):
def test_record_save_not_extended(factory: callable, data: callable, path: callable):
# Initialise the Model using a named profile; in this case specify the "linked-art"
# profile which is provided with the library so only needs specifying by its name.
# Other profiles may be used to create models of other types; see the *Profiles*
Expand Down Expand Up @@ -42,3 +42,67 @@ def test_record_load(factory: callable, data: callable, path: callable):

# Ensure that the contents of the saved file match the pre-saved example file
assert contents == data("examples/saved.json")


def test_record_save_with_extended(factory: callable, data: callable, path: callable):
# Initialise the Model using a named profile; in this case specify the "linked-art"
# profile which is provided with the library so only needs specifying by its name.
# Other profiles may be used to create models of other types; see the *Profiles*
# section in the README for more information. The factory method dynamically creates
# the required model class types and adds them to the object returned from the call:
model = factory(profile="linked-art")

# Extend the model by registering custom schema properties
# Register the 'test:related' custom property on the HumanMadeObject entity type
Model.extend(
model.HumanMadeObject,
properties=dict(
related=dict(
canonical="related",
namespace="test",
individual=False,
range=model.HumanMadeObject,
sorting=10000,
),
),
)

# Create a new Model instance
artefact = model.HumanMadeObject(
ident="https://data.example.org/object/123",
label="Example Object 123",
)

# Ensure that the created JSON-LD record is of the type expected
assert isinstance(artefact, model.HumanMadeObject)
assert isinstance(artefact, Model)
assert isinstance(artefact, Node)

# Add an example property value to the Model instance
artefact.classified_as = model.Type(
ident="http://vocab.getty.edu/aat/300133025",
label="Works of Art",
)

artefact.related = model.HumanMadeObject(
ident="https://data.example.org/object/456",
label="Related Object 456",
)

artefact.related = model.HumanMadeObject(
ident="https://data.example.org/object/789",
label="Related Object 789",
)

# Save the Model instance object to storage
filepath: str = artefact.save("/tmp/saved-extended.json", indent=2, overwrite=True)

# Ensure that the call to the .save() method returned the expected file path
assert isinstance(filepath, str)

# Load the contents of the saved file
contents: str = data(filepath)
assert isinstance(contents, str)

# Ensure that the contents of the saved file match the pre-saved example file
assert contents == data("examples/saved-extended.json")
Loading