Skip to content

Commit

Permalink
Merge pull request #278 from cthoyt/pass-converters
Browse files Browse the repository at this point in the history
Enable passing a converter to RDFLib dumper
  • Loading branch information
pkalita-lbl authored Sep 26, 2023
2 parents a0f393c + b16cf09 commit 122ddb9
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 30 deletions.
33 changes: 26 additions & 7 deletions linkml_runtime/dumpers/rdflib_dumper.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Optional, Any, Dict, Union
from pydantic import BaseModel

from curies import Converter
from rdflib import Graph, URIRef, XSD
from rdflib.term import Node, BNode, Literal
from rdflib.namespace import RDF
Expand All @@ -24,7 +25,12 @@ class RDFLibDumper(Dumper):
This requires a SchemaView object
"""
def as_rdf_graph(self, element: Union[BaseModel, YAMLRoot], schemaview: SchemaView, prefix_map: Dict[str, str] = None) -> Graph:
def as_rdf_graph(
self,
element: Union[BaseModel, YAMLRoot],
schemaview: SchemaView,
prefix_map: Union[Dict[str, str], Converter, None] = None,
) -> Graph:
"""
Dumps from element to an rdflib Graph,
following a schema
Expand All @@ -35,6 +41,9 @@ def as_rdf_graph(self, element: Union[BaseModel, YAMLRoot], schemaview: SchemaVi
:return:
"""
g = Graph()
if isinstance(prefix_map, Converter):
# TODO replace with `prefix_map = prefix_map.bimap` after making minimum requirement on python 3.8
prefix_map = {record.prefix: record.uri_prefix for record in prefix_map.records}
logging.debug(f'PREFIXMAP={prefix_map}')
if prefix_map:
for k, v in prefix_map.items():
Expand Down Expand Up @@ -137,10 +146,15 @@ def inject_triples(self, element: Any, schemaview: SchemaView, graph: Graph, tar
graph.add((element_uri, RDF.type, URIRef(schemaview.get_uri(cn, expand=True))))
return element_uri

def dump(self, element: Union[BaseModel, YAMLRoot],
to_file: str,
schemaview: SchemaView = None,
fmt: str = 'turtle', prefix_map: Dict[str, str] = None, **args) -> None:
def dump(
self,
element: Union[BaseModel, YAMLRoot],
to_file: str,
schemaview: SchemaView = None,
fmt: str = 'turtle',
prefix_map: Union[Dict[str, str], Converter, None] = None,
**args,
) -> None:
"""
Write element as rdf to to_file
Expand All @@ -153,8 +167,13 @@ def dump(self, element: Union[BaseModel, YAMLRoot],
"""
super().dump(element, to_file, schemaview=schemaview, fmt=fmt, prefix_map=prefix_map)

def dumps(self, element: Union[BaseModel, YAMLRoot], schemaview: SchemaView = None,
fmt: Optional[str] = 'turtle', prefix_map: Dict[str, str] = None) -> str:
def dumps(
self,
element: Union[BaseModel, YAMLRoot],
schemaview: SchemaView = None,
fmt: Optional[str] = 'turtle',
prefix_map: Union[Dict[str, str], Converter, None] = None,
) -> str:
"""
Convert element into an RDF graph guided by the schema
Expand Down
34 changes: 23 additions & 11 deletions linkml_runtime/loaders/rdflib_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from dataclasses import dataclass
from typing import Optional, Any, Dict, Type, Union, TextIO, List, Tuple, Set

from curies import Converter
from hbreader import FileInfo
from rdflib import Graph, URIRef
from rdflib.term import Node, BNode, Literal
Expand All @@ -30,11 +31,15 @@ class RDFLibLoader(Loader):
Note: this is a more complete replacement for rdf_loader
"""
def from_rdf_graph(self, graph: Graph, schemaview: SchemaView, target_class: Type[Union[BaseModel, YAMLRoot]],
prefix_map: Dict[str, str] = None,
cast_literals: bool = True,
allow_unprocessed_triples: bool = True,
ignore_unmapped_predicates: bool = False) -> List[Union[BaseModel, YAMLRoot]]:
def from_rdf_graph(
self, graph: Graph,
schemaview: SchemaView,
target_class: Type[Union[BaseModel, YAMLRoot]],
prefix_map: Union[Dict[str, str], Converter, None] = None,
cast_literals: bool = True,
allow_unprocessed_triples: bool = True,
ignore_unmapped_predicates: bool = False,
) -> List[Union[BaseModel, YAMLRoot]]:
"""
Loads objects from graph into lists of the python target_class structure,
recursively walking RDF graph from instances of target_class.
Expand All @@ -58,6 +63,9 @@ def from_rdf_graph(self, graph: Graph, schemaview: SchemaView, target_class: Typ
logging.error(f'Inconsistent URI to class map: {uri} -> {c2.name}, {c.name}')
uri_to_class_map[uri] = c
# data prefix map: supplements or overrides existing schema prefix map
if isinstance(prefix_map, Converter):
# TODO replace with `prefix_map = prefix_map.bimap` after making minimum requirement on python 3.8
prefix_map = {record.prefix: record.uri_prefix for record in prefix_map.records}
if prefix_map:
for k, v in prefix_map.items():
namespaces[k] = v
Expand Down Expand Up @@ -224,12 +232,16 @@ def _uri_to_id(self, node: VALID_SUBJECT, id_slot: SlotDefinition, schemaview: S
return schemaview.namespaces().curie_for(node)


def load(self, source: Union[str, TextIO, Graph], target_class: Type[Union[BaseModel, YAMLRoot]], *,
schemaview: SchemaView = None,
prefix_map: Dict[str, str] = None,
fmt: Optional[str] = 'turtle',
metadata: Optional[FileInfo] = None,
**kwargs) -> Union[BaseModel, YAMLRoot]:
def load(
self,
source: Union[str, TextIO, Graph],
target_class: Type[Union[BaseModel, YAMLRoot]], *,
schemaview: SchemaView = None,
prefix_map: Union[Dict[str, str], Converter, None] = None,
fmt: Optional[str] = 'turtle',
metadata: Optional[FileInfo] = None,
**kwargs,
) -> Union[BaseModel, YAMLRoot]:
"""
Load the RDF in source into the python target_class structure
Expand Down
33 changes: 21 additions & 12 deletions tests/test_loaders_dumpers/test_rdflib_dumper.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
from pathlib import Path

from curies import Converter
from rdflib import Graph, Literal, URIRef
from rdflib.namespace import RDF, SKOS, XSD
from rdflib import Namespace
Expand All @@ -26,7 +27,7 @@
DATA_ROUNDTRIP = os.path.join(OUTPUT_DIR, 'example_personinfo_data.roundtrip-rdf.yaml')
UNMAPPED_ROUNDTRIP = os.path.join(OUTPUT_DIR, 'example_personinfo_data.unmapped-preds.yaml')

prefix_map = {
PREFIX_MAP = {
'CODE': 'http://example.org/code/',
'ROR': 'http://example.org/ror/',
'P': 'http://example.org/P/',
Expand Down Expand Up @@ -105,12 +106,14 @@
WD = Namespace('http://www.wikidata.org/entity/')

class RdfLibDumperTestCase(unittest.TestCase):
def setUp(self) -> None:
self.prefix_map = PREFIX_MAP

def test_rdflib_dumper(self):
view = SchemaView(SCHEMA)
container = yaml_loader.load(DATA, target_class=Container)
self._check_objs(view, container)
rdflib_dumper.dump(container, schemaview=view, to_file=OUT, prefix_map=prefix_map)
rdflib_dumper.dump(container, schemaview=view, to_file=OUT, prefix_map=self.prefix_map)
g = Graph()
g.parse(OUT, format='ttl')

Expand All @@ -134,7 +137,7 @@ def test_rdflib_dumper(self):
self.assertIn((container, INFO.organizations, ROR['2']), g)
self.assertIn((container, INFO.persons, P['001']), g)
self.assertIn((container, INFO.persons, P['002']), g)
container: Container = rdflib_loader.load(OUT, target_class=Container, schemaview=view, prefix_map=prefix_map)
container: Container = rdflib_loader.load(OUT, target_class=Container, schemaview=view, prefix_map=self.prefix_map)
self._check_objs(view, container)
#print(yaml_dumper.dumps(container))
#person = next(p for p in container.persons if p.id == 'P:002')
Expand All @@ -148,7 +151,7 @@ def test_enums(self):
assert org1type2.meaning
org1 = Organization('ROR:1', categories=[org1type1, org1type2])
print(org1.categories)
g = rdflib_dumper.as_rdf_graph(org1, schemaview=view, prefix_map=prefix_map)
g = rdflib_dumper.as_rdf_graph(org1, schemaview=view, prefix_map=self.prefix_map)
print(g)
cats = list(g.objects(ROR['1'], INFO['categories']))
print(cats)
Expand All @@ -173,7 +176,7 @@ def test_rdflib_loader(self):
tests loading from an RDF graph
"""
view = SchemaView(SCHEMA)
container: Container = rdflib_loader.load(DATA_TTL, target_class=Container, schemaview=view, prefix_map=prefix_map)
container: Container = rdflib_loader.load(DATA_TTL, target_class=Container, schemaview=view, prefix_map=self.prefix_map)
self._check_objs(view, container)
yaml_dumper.dump(container, to_file=DATA_ROUNDTRIP)

Expand All @@ -186,10 +189,10 @@ def test_unmapped_predicates(self):
# default behavior is to raise error on unmapped predicates
with self.assertRaises(MappingError) as context:
rdflib_loader.loads(unmapped_predicates_test_ttl, target_class=Person,
schemaview=view, prefix_map=prefix_map)
schemaview=view, prefix_map=self.prefix_map)
# called can explicitly allow unmapped predicates to be dropped
person: Person = rdflib_loader.loads(unmapped_predicates_test_ttl, target_class=Person,
schemaview=view, prefix_map=prefix_map,
schemaview=view, prefix_map=self.prefix_map,
ignore_unmapped_predicates=True)
self.assertEqual(person.id, 'P:001')
self.assertEqual(person.age_in_years, 33)
Expand All @@ -204,7 +207,7 @@ def test_any_of_enum(self):
view = SchemaView(SCHEMA)
# default behavior is to raise error on unmapped predicates
person = rdflib_loader.loads(enum_union_type_test_ttl, target_class=Person,
schemaview=view, prefix_map=prefix_map)
schemaview=view, prefix_map=self.prefix_map)
self.assertEqual(person.id, 'P:001')
self.assertEqual(person.age_in_years, 33)
yaml_dumper.dump(person, to_file=UNMAPPED_ROUNDTRIP)
Expand All @@ -225,11 +228,11 @@ def test_unmapped_type(self):
# default behavior is to raise error on unmapped predicates
with self.assertRaises(DataNotFoundError) as context:
rdflib_loader.loads(unmapped_type_test_ttl, target_class=Person,
schemaview=view, prefix_map=prefix_map)
schemaview=view, prefix_map=self.prefix_map)
graph = Graph()
graph.parse(data=unmapped_type_test_ttl, format='ttl')
objs = rdflib_loader.from_rdf_graph(graph, target_class=Person,
schemaview=view, prefix_map=prefix_map)
schemaview=view, prefix_map=self.prefix_map)
self.assertEqual(len(objs), 0)

def test_blank_node(self):
Expand All @@ -238,7 +241,7 @@ def test_blank_node(self):
"""
view = SchemaView(SCHEMA)
address: Address = rdflib_loader.loads(blank_node_test_ttl, target_class=Address,
schemaview=view, prefix_map=prefix_map,
schemaview=view, prefix_map=self.prefix_map,
ignore_unmapped_predicates=True)
self.assertEqual(address.city, 'foo city')
ttl = rdflib_dumper.dumps(address, schemaview=view)
Expand Down Expand Up @@ -368,7 +371,7 @@ def test_phenopackets(self):
pkt = Phenopacket(id='id with spaces',
metaData=MetaData(resources=[Resource(id='id with spaces')]),
phenotypicFeatures=[pf])
ttl = rdflib_dumper.dumps(pkt, view, prefix_map=prefix_map)
ttl = rdflib_dumper.dumps(pkt, view, prefix_map=self.prefix_map)
g = Graph()
g.parse(data=ttl, format='ttl')
self.assertIn(Literal(test_label), list(g.objects(URIRef(expected_uri))),
Expand All @@ -380,6 +383,12 @@ def test_phenopackets(self):



class RDFLibConverterDumperTestCase(RdfLibDumperTestCase):
"""A test case that uses a :class:`curies.Converter` for testing loading and dumping with RDFLib."""

def setUp(self) -> None:
"""Set up the test case using a :class:`curies.Converter` instead of a simple prefix map."""
self.prefix_map = Converter.from_prefix_map(PREFIX_MAP)



Expand Down

0 comments on commit 122ddb9

Please sign in to comment.