Skip to content

Commit

Permalink
Added tests
Browse files Browse the repository at this point in the history
  • Loading branch information
MFSY committed Apr 2, 2024
1 parent 44bee26 commit 6143ead
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 126 deletions.
111 changes: 55 additions & 56 deletions kgforge/specializations/models/rdf/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,9 @@ def validate(self, resource: Resource, type_: str):
raise TypeError(
f"A single type should be provided for validation: {str(exc)}"
) from exc
shape_iri = self.get_shape_iri_from_class_fragment(type_to_validate)
shape_iri = self.get_shape_uriref_from_class_fragment(type_to_validate)
data_graph = as_graph(resource, False, self.context, None, None)
# link_property_shapes_from_ancestors=True to address pySHacl current
# limitation of the length of sh:node transitive path (https://github.com/RDFLib/pySHACL/blob/master/pyshacl/shape.py#L468).
shape, shacl_graph = self.get_shape_graph(
shape_iri, link_property_shapes_from_ancestors=True
)
shape, shacl_graph = self.get_shape_graph(shape_iri)
return self._validate(shape_iri, data_graph, shape, shacl_graph)

@abstractmethod
Expand Down Expand Up @@ -241,7 +237,7 @@ def _build_shapes_map(self) -> Tuple[Dict, Dict, Dict]:

@abstractmethod
def load_shape_graph_from_source(self, graph_id: str, schema_id: str) -> Graph:
"""Loads into graph_id the node shapes defined in shema_id
"""Loads into graph_id the node shapes defined in shema_id from the source
Args:
graph_id: A named graph uri from which the shapes are accessible
Expand Down Expand Up @@ -359,10 +355,14 @@ def _transitive_load_shape_graph(self, graph_uriref: URIRef, schema_uriref: URIR
Loaded schemas are added to self._imported to avoid loading them a second time.
Args:
node_shape_uriref: the URI of a node shape
graph_uriref: A named graph URIRef containing schema_uriref
schema_uriref: A URIRef of the schema
import_transitive_closure: Whether (True) to add node_shape_uriref's owl:import transitive closure in node_shape_uriref's graph or not (False)
link_property_shapes_from_ancestors: Whether (True) to directly link to node_shape_uriref recursively collected property shapes of its ancestors or not (False)
"""
schema_graph = self.load_shape_graph_from_source(graph_uriref, schema_uriref)
# if import_transitive_closure:
for imported in schema_graph.objects(schema_uriref, OWL.imports):
imported_schema_uriref = URIRef(self.context.expand(imported))
try:
Expand All @@ -380,7 +380,7 @@ def _transitive_load_shape_graph(self, graph_uriref: URIRef, schema_uriref: URIR
schema_graph += imported_schema_graph
except KeyError as ke:
raise ValueError(
f"Imported schema {imported_schema_uriref} is not loaded ad indexed: {str(ke)}"
f"Imported schema {imported_schema_uriref} is not loaded and indexed: {str(ke)}"
) from ke
except ParserError as pe:
raise ValueError(
Expand All @@ -389,7 +389,7 @@ def _transitive_load_shape_graph(self, graph_uriref: URIRef, schema_uriref: URIR
self._imported.append(schema_uriref)
return schema_graph

def _get_property_shapes_from_nodeshape(
def _get_transitive_property_shapes_from_nodeshape(
self, node_shape_uriref: URIRef, schema_graph: Graph
) -> Tuple[List, List, List, List]:
"""
Expand All @@ -412,32 +412,19 @@ def _get_property_shapes_from_nodeshape(
* the triples to remove from schema_graph
* RDF Collections and index of items to remove from them
"""
property_shapes_to_add = []
node_transitive_property_shapes = []
triples_to_add = []
triples_to_remove = []
rdfcollection_items_to_remove = []
schema_defines_shape = [
SH["and"] / (RDF.first | RDF.rest / RDF.first) * ZeroOrMore,
SH["or"] / (RDF.first | RDF.rest / RDF.first) * ZeroOrMore,
SH["xone"] / (RDF.first | RDF.rest / RDF.first) * ZeroOrMore,
]
sh_nodes = []
sh_properties = []
sh_properties.extend(
list(self._dataset_graph.objects(node_shape_uriref, SH.property))
sh_nodes, sh_properties = self.get_nodeshape_parent_propertyshape(
self._dataset_graph, node_shape_uriref
)
sh_nodes.extend(list(self._dataset_graph.objects(node_shape_uriref, SH.node)))
for s in schema_defines_shape:
sh_nodes.extend(
list(self._dataset_graph.objects(node_shape_uriref, s / SH.node))
)
sh_properties.extend(
list(self._dataset_graph.objects(node_shape_uriref, s / SH.property))
)
for sh_node in set(sh_nodes):
if str(sh_node) != str(node_shape_uriref) and str(sh_node) != str(RDF.nil):
t_a, p_a, t_r, c_r = self._get_property_shapes_from_nodeshape(
sh_node, schema_graph
t_a, p_a, t_r, c_r = (
self._get_transitive_property_shapes_from_nodeshape(
sh_node, schema_graph
)
)
if p_a:
triples_to_add.extend(t_a)
Expand All @@ -446,7 +433,7 @@ def _get_property_shapes_from_nodeshape(
triples_to_add.append(
(node_shape_uriref, propertyShape[0], propertyShape[1])
)
property_shapes_to_add.append(propertyShape)
node_transitive_property_shapes.append(propertyShape)
triples_to_remove.extend(t_r)
triples_to_remove.append((node_shape_uriref, SH.node, sh_node))
for t in schema_graph.subjects(SH.node, sh_node):
Expand All @@ -471,19 +458,30 @@ def _get_property_shapes_from_nodeshape(
if str(sh_node_property) != str(node_shape_uriref) and str(
sh_node_property
) != str(RDF.nil):
property_shapes_to_add.append((SH.property, sh_node_property))
node_transitive_property_shapes.append((SH.property, sh_node_property))
return (
triples_to_add,
property_shapes_to_add,
node_transitive_property_shapes,
triples_to_remove,
rdfcollection_items_to_remove,
)

def get_shape_graph(
self,
node_shape_uriref: URIRef,
link_property_shapes_from_ancestors: bool = False,
) -> Tuple[Shape, Graph]:
def get_nodeshape_parent_propertyshape(self, graph, node_shape_uriref):
schema_defines_shape = [
SH["and"] / (RDF.first | RDF.rest / RDF.first) * ZeroOrMore,
SH["or"] / (RDF.first | RDF.rest / RDF.first) * ZeroOrMore,
SH["xone"] / (RDF.first | RDF.rest / RDF.first) * ZeroOrMore,
]
sh_properties = list(graph.objects(node_shape_uriref, SH.property))
sh_nodes = list(graph.objects(node_shape_uriref, SH.node))
for s in schema_defines_shape:
sh_nodes.extend(list(graph.objects(node_shape_uriref, s / SH.node)))
sh_properties.extend(
list(graph.objects(node_shape_uriref, s / SH.property))
)
return sh_nodes, sh_properties

def get_shape_graph(self, node_shape_uriref: URIRef) -> Tuple[Shape, Graph]:
try:
shape = self.get_shape_graph_wrapper().lookup_shape_from_node(
node_shape_uriref
Expand All @@ -503,24 +501,25 @@ def get_shape_graph(
self._get_named_graph_from_shape(node_shape_uriref),
self.shape_to_defining_resource[node_shape_uriref],
)
if link_property_shapes_from_ancestors:
(
triples_to_add,
_,
triples_to_remove,
rdfcollection_items_to_remove,
) = self._get_property_shapes_from_nodeshape(
node_shape_uriref, shape_graph
)
for triple_to_add in triples_to_add:
shape_graph.add(triple_to_add)
for triple_to_remove in triples_to_remove:
shape_graph.remove(triple_to_remove)
for (
rdfcollection,
rdfcollection_item_index,
) in rdfcollection_items_to_remove:
del rdfcollection[rdfcollection_item_index]
# Address (though not for all node shape inheritance cases) limitation of the length
# of sh:node transitive path (https://github.com/RDFLib/pySHACL/blob/master/pyshacl/shape.py#L468).
(
triples_to_add,
_,
triples_to_remove,
rdfcollection_items_to_remove,
) = self._get_transitive_property_shapes_from_nodeshape(
node_shape_uriref, shape_graph
)
for triple_to_add in triples_to_add:
shape_graph.add(triple_to_add)
for triple_to_remove in triples_to_remove:
shape_graph.remove(triple_to_remove)
for (
rdfcollection,
rdfcollection_item_index,
) in rdfcollection_items_to_remove:
del rdfcollection[rdfcollection_item_index]
# reloads the shapes graph
self._init_shape_graph_wrapper()
shape = self.get_shape_graph_wrapper().lookup_shape_from_node(
Expand All @@ -532,7 +531,7 @@ def get_shape_graph(
) from e
return shape, shape_graph

def get_shape_iri_from_class_fragment(self, fragment):
def get_shape_uriref_from_class_fragment(self, fragment):
try:
type_expanded_cls = self.context.expand(fragment)
return self.class_to_shape[URIRef(type_expanded_cls)]
Expand Down
2 changes: 1 addition & 1 deletion kgforge/specializations/models/rdf/store_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from kgforge.core.conversions.rdf import as_jsonld
from kgforge.core.archetypes.store import Store
from kgforge.specializations.models.rdf.node_properties import NodeProperties
from kgforge.specializations.models.rdf.service import RdfService, ShapesGraphWrapper
from kgforge.specializations.models.rdf.service import RdfService
from kgforge.specializations.stores.nexus import Service


Expand Down
5 changes: 3 additions & 2 deletions kgforge/specializations/models/rdf_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,9 @@ def _generate_context(self) -> Context:

def _template(self, type: str, only_required: bool) -> Dict:
try:
shape_iri = self.service.get_shape_iri_from_class_fragment(type)
shape_iri = self.service.get_shape_uriref_from_class_fragment(type)
node_properties = self.service.materialize(shape_iri)
print(node_properties)
dictionary = parse_attributes(node_properties, only_required, None)
return dictionary
except Exception as exc:
Expand All @@ -104,7 +105,7 @@ def _template(self, type: str, only_required: bool) -> Dict:

def schema_id(self, type: str) -> str:
try:
shape_iri = self.service.get_shape_iri_from_class_fragment(type)
shape_iri = self.service.get_shape_uriref_from_class_fragment(type)
return self.service.schema_id(shape_iri)
except Exception as exc:
raise ValueError(f"Unable to get the schema id:{str(exc)}") from exc
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ def context_iri_file(context_file_path):
return f"file://{context_file_path}"


@pytest.fixture(scope="session")
@pytest.fixture(scope="function")
def rdf_model_from_dir(context_iri_file, shacl_schemas_file_path):
return RdfModel(
shacl_schemas_file_path, context={"iri": context_iri_file}, origin="directory"
Expand Down
44 changes: 0 additions & 44 deletions tests/data/shacl-model/commons/shapes-1.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,50 +159,6 @@
}
]
},
{
"@id": "this:EmployeeShape",
"@type": "sh:NodeShape",
"nodeKind": "sh:IRI",
"targetClass": "this:Employee",
"and": [
{
"node": "this:PersonShape"
},
{
"path": "schema:startDate",
"datatype": "xsd:date",
"minCount": 1,
"maxCount": 1
},
{
"path": "schema:colleague",
"class": "schema:Person"
},
{
"path": "schema:worksFor",
"or": [
{
"class": "schema:Person"
},
{
"class": "schema:Organization"
}
]
},
{
"or": [
{
"path": "schema:contractor",
"class": "schema:Organization"
},
{
"path": "schema:department",
"class": "schema:Organization"
}
]
}
]
},
{
"@id": "this:OrganizationShape",
"@type": "sh:NodeShape",
Expand Down
41 changes: 40 additions & 1 deletion tests/specializations/models/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
# along with Blue Brain Nexus Forge. If not, see <https://choosealicense.com/licenses/lgpl-3.0/>.
from copy import deepcopy

from rdflib import URIRef

from tests.conftest import shacl_schemas_file_path


ORGANIZATION = {
"id": "",
Expand Down Expand Up @@ -122,29 +126,64 @@
"Activity": {
"shape": "http://www.example.com/ActivityShape",
"schema": "http://shapes.ex/activity",
"named_graph": f"{shacl_schemas_file_path}/shapes-2.json",
"imports": [],
"parent_node_shapes": [],
"number_of_direct_property_shapes": 9,
"number_of_inherited_property_shapes": 0,
},
"Association": {
"shape": "http://www.example.com/AssociationShape",
"schema": "http://shapes.ex/person",
"named_graph": f"{shacl_schemas_file_path}/shapes-1.json",
"imports": [],
"parent_node_shapes": [],
"number_of_direct_property_shapes": 1,
"number_of_inherited_property_shapes": 0,
},
"Building": {
"shape": "http://www.example.com/BuildingShape",
"schema": "http://shapes.ex/building",
"named_graph": f"{shacl_schemas_file_path}/shapes-3.json",
"imports": [],
"parent_node_shapes": [],
"number_of_direct_property_shapes": 4,
"number_of_inherited_property_shapes": 0,
},
"Employee": {
"shape": "http://www.example.com/EmployeeShape",
"schema": "http://shapes.ex/person",
"schema": "http://shapes.ex/employee",
"named_graph": f"{shacl_schemas_file_path}/shapes-4.json",
"imports": ["http://shapes.ex/person"],
"parent_node_shapes": [URIRef("http://www.example.com/PersonShape")],
"number_of_direct_property_shapes": 0,
"number_of_inherited_property_shapes": 6,
},
"Organization": {
"shape": "http://www.example.com/OrganizationShape",
"schema": "http://shapes.ex/person",
"named_graph": f"{shacl_schemas_file_path}/shapes-1.json",
"imports": [],
"parent_node_shapes": [],
"number_of_direct_property_shapes": 2,
"number_of_inherited_property_shapes": 0,
},
"Person": {
"shape": "http://www.example.com/PersonShape",
"schema": "http://shapes.ex/person",
"named_graph": f"{shacl_schemas_file_path}/shapes-1.json",
"imports": [],
"parent_node_shapes": [],
"number_of_direct_property_shapes": 6,
"number_of_inherited_property_shapes": 0,
},
"PostalAddress": {
"shape": "http://schema.org/PostalAddress",
"schema": "http://shapes.ex/person",
"named_graph": f"{shacl_schemas_file_path}/shapes-1.json",
"imports": [],
"parent_node_shapes": [],
"number_of_direct_property_shapes": 2,
"number_of_inherited_property_shapes": 0,
},
}
2 changes: 1 addition & 1 deletion tests/specializations/models/test_rdf_directory_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def test_load_rdf_files_as_graph(shacl_schemas_file_path):
graph_dataset = _load_rdf_files_as_graph(Path(shacl_schemas_file_path))
assert isinstance(graph_dataset, rdflib.Dataset)
shape_graphs = [str(g.identifier) for g in graph_dataset.graphs()]
assert len(shape_graphs) == 4
assert len(shape_graphs) == 5
expected_file_paths = [
str(f.resolve())
for f in Path(shacl_schemas_file_path).rglob(os.path.join("*.*"))
Expand Down
Loading

0 comments on commit 6143ead

Please sign in to comment.