Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved comparing literals #164

Merged
merged 19 commits into from
Jan 26, 2024
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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ testing = [
"SPARQLWrapper ~=2.0",
"DLite-Python >=0.4.0,<1",
"graphviz ~= 0.20",
"pint>=0.16.1,<0.23",
]
testing-core = [
"pytest ~=7.4",
Expand Down
5 changes: 3 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,12 @@ def expected_function_triplestore(
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ex:sum__{fid} a fno:Function ;
rdfs:label "sum_"@en ;
oteio:hasPythonFunctionName "sum_" ;
oteio:hasPythonModuleName "conftest" ;
oteio:hasPythonFunctionName "sum_"^^xsd:string ;
oteio:hasPythonModuleName "conftest"^^xsd:string ;
dcterms:description "Returns the sum of `first_param` and `second_param`."@en ;
fno:expects ( ex:sum__{fid}_parameter1_first_param ex:sum__{fid}_parameter2_second_param ) ;
fno:returns ( ex:sum__{fid}_output1 ) .
Expand Down
15 changes: 9 additions & 6 deletions tests/test_add_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ def func(a, b):
@prefix oteio: <http://emmo.info/oteio#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<:func_{f_id}> a fno:Function ;
rdfs:label "func"@en ;
oteio:hasPythonFunctionName "func" ;
oteio:hasPythonModuleName "test_add_function" ;
oteio:hasPythonFunctionName "func"^^xsd:string ;
oteio:hasPythonModuleName "{__name__}"^^xsd:string ;
dcterms:description "Returns the sum of `a` and `b`."@en ;
fno:expects ( <:func_{f_id}_parameter1_a> <:func_{f_id}_parameter2_b> ) ;
fno:returns ( <:func_{f_id}_output1> ) .
Expand Down Expand Up @@ -72,14 +73,15 @@ def func(a, b):
@prefix ex: <http://example.com/ex#> .
@prefix oteio: <http://emmo.info/oteio#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<:func_{f_id}> a emmo:EMMO_4299e344_a321_4ef2_a744_bacfcce80afc ;
rdfs:label "func"@en ;
emmo:EMMO_36e69413_8c59_4799_946c_10b05d266e22 ex:arg1,
ex:arg2 ;
emmo:EMMO_c4bace1d_4db0_4cd3_87e9_18122bae2840 ex:sum ;
oteio:hasPythonFunctionName "func" ;
oteio:hasPythonModuleName "test_add_function" ;
oteio:hasPythonFunctionName "func"^^xsd:string ;
oteio:hasPythonModuleName "{__name__}"^^xsd:string ;
dcterms:description "Returns the sum of `a` and `b`."@en .

ex:arg1 a emmo:EMMO_194e367c_9783_4bf5_96d0_9ad597d48d9a ;
Expand Down Expand Up @@ -108,14 +110,15 @@ def func(a, b):
@prefix ex: <http://example.com/ex#> .
@prefix oteio: <http://emmo.info/oteio#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<:func_{f_id}> a emmo:EMMO_4299e344_a321_4ef2_a744_bacfcce80afc ;
rdfs:label "func"@en ;
emmo:EMMO_36e69413_8c59_4799_946c_10b05d266e22 ex:arg1,
ex:arg2 ;
emmo:EMMO_c4bace1d_4db0_4cd3_87e9_18122bae2840 ex:sum ;
oteio:hasPythonFunctionName "func" ;
oteio:hasPythonModuleName "test_add_function" ;
oteio:hasPythonFunctionName "func"^^xsd:string ;
oteio:hasPythonModuleName "{__name__}"^^xsd:string ;
dcterms:description "Returns the sum of `a` and `b`."@en .

ex:arg1 a emmo:EMMO_194e367c_9783_4bf5_96d0_9ad597d48d9a ;
Expand Down
31 changes: 22 additions & 9 deletions tests/test_collection.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"""Test collection."""

import pytest


def test_collection():
"""Test if we can use a DLite collection as backend."""
dlite = pytest.importorskip("dlite")
from tripper import EMMO, MAP, Triplestore
dlite = pytest.importorskip("dlite") # pylint: disable=unused-variable
from tripper import DM, EMMO, MAP, XSD, Literal, Triplestore
from tripper.utils import en

ts = Triplestore(backend="collection")
assert not list(ts.triples())
Expand All @@ -18,21 +18,34 @@ def test_collection():
"cif", "http://emmo.info/0.1/cif-ontology#"
)
triples = [
(STRUCTURE.name, DM.hasLabel, en("Strontium titanate")),
(STRUCTURE.masses, DM.hasUnit, Literal("u", datatype=XSD.string)),
(STRUCTURE.symbols, MAP.mapsTo, EMMO.Symbol),
(STRUCTURE.positions, MAP.mapsTo, EMMO.PositionVector),
(STRUCTURE.cell, MAP.mapsTo, CIF.cell),
(STRUCTURE.masses, MAP.mapsTo, EMMO.Mass),
]

ts.add_triples(triples)

assert set(ts.triples()) == set(triples)

ts.remove(object=EMMO.Mass)
assert set(ts.triples()) == set(triples[:-1])

# Test that we can initialise from an existing collection
coll = dlite.Collection()
for triple in triples:
coll.add_relation(*triple)
ts2 = Triplestore(backend="collection", collection=coll)
assert set(ts2.triples()) == set(triples)

# TODO: Fix handling of Literal in Collections (Issue #160, PR #165) and
# reactivate test.
#
# # Test that we can initialise from an existing collection
# coll = dlite.Collection()
# for triple in triples:
# coll.add_relation(*triple)
# ts2 = Triplestore(backend="collection", collection=coll)
# assert set(ts2.triples()) == set(triples)
#
# # Test serialising/parsing
# dump = ts.serialize(backend="rdflib")
# ts3 = Triplestore(backend="collection")
# ts3.parse(backend="rdflib", data=dump)
# assert set(ts3.triples()) == set(triples)
2 changes: 1 addition & 1 deletion tests/test_eval_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def func(a, b):
# Test to add a function from the standard library. The hashlib module
# is not expected to be imported in the current scope
iri2 = ts.add_function(
EX.shape256,
EX.shake256,
expects=[EX.Bytes],
returns=EX.ShakeVar,
func_name="shake_256",
Expand Down
31 changes: 28 additions & 3 deletions tests/test_literals.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@

def test_string() -> None:
"""Test creating a string literal."""
from tripper.literal import Literal
from tripper.literal import XSD, Literal

literal = Literal("Hello world!")
assert literal == "Hello world!"
assert isinstance(literal, str)
assert literal.lang is None
assert literal.datatype is None
assert literal.datatype == XSD.string
assert literal.to_python() == "Hello world!"
assert literal.value == "Hello world!"
assert literal.n3() == '"Hello world!"'
assert literal.n3() == f'"Hello world!"^^{XSD.string}'


def test_string_lang() -> None:
Expand Down Expand Up @@ -153,3 +153,28 @@ def test_parse_literal() -> None:
assert literal.value == "value"
assert literal.lang is None
assert literal.datatype == "http://example.com/vocab#mytype"


def test_equality() -> None:
"""Test equality."""
from tripper import RDF, XSD, Literal

assert Literal("text", datatype=XSD.string) == "text"
assert Literal("text", lang="en") == "text"
assert Literal("text", lang="en") != Literal("text", lang="dk")
assert Literal("text") == "text"
assert Literal("text") != "text2"
assert Literal("text", datatype=RDF.HTML) != "text"
assert Literal(1) == 1
assert Literal(1) != 1.0
assert Literal(1) != "1"
assert Literal(1, datatype=XSD.double) == 1.0
jesper-friis marked this conversation as resolved.
Show resolved Hide resolved
assert Literal("1", datatype=XSD.double) == 1.0
assert Literal("1.", datatype=XSD.double) == 1.0
assert Literal(1.0) == 1.0
assert Literal(1.0) != 1
assert Literal(True) == True # pylint: disable=singleton-comparison
assert Literal(True) != "True"

# Newer versions of Python also allow reverting equality statements
assert 1.0 == Literal(1, datatype=XSD.double)
1 change: 0 additions & 1 deletion tests/test_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ def test_namespaces(get_ontology_path: "Callable[[str], Path]") -> None:
`pathlib.Path` object pointing to an ontology test file.

"""

pytest.importorskip("rdflib")
from tripper import RDF, Namespace
from tripper.errors import NoSuchIRIError
Expand Down
36 changes: 30 additions & 6 deletions tripper/backends/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,43 @@ def __init__(

def triples(self, triple: "Triple") -> "Generator[Triple, None, None]":
"""Returns a generator over matching triples."""
for s, p, o in self.collection.get_relations(*triple):
yield s, p, parse_object(o)
for s, p, o, d in self.collection.get_relations(*triple, rettype="T"):
if d:
lang = d[1:] if d[0] == "@" else None
dt = None if lang else d
yield s, p, Literal(o, lang=lang, datatype=dt)
else:
yield s, p, o

def add_triples(self, triples: "Sequence[Triple]"):
"""Add a sequence of triples."""
for s, p, o in triples:
# Strange complains by mypy - it assumed that
ajeklund marked this conversation as resolved.
Show resolved Hide resolved
# parse_object() is returning a `str` regardless that it
# has been declared to return the union of `str` and
# `Literal`.
v = parse_object(o)
v_str = v.n3() if isinstance(v, Literal) else v
self.collection.add_relation(s, p, v_str)
o = v if isinstance(v, str) else v.value
d = (
None
if not isinstance(v, Literal)
else f"@{v.lang}"
if v.lang
else v.datatype
)
self.collection.add_relation(s, p, o, d)

def remove(self, triple: "Triple"):
"""Remove all matching triples from the backend."""
s, p, o = triple
v = parse_object(o)
v_str = v.n3() if isinstance(v, Literal) else v
self.collection.remove_relations(s, p, v_str)
o = v if isinstance(v, str) else v.value
d = (
None
if isinstance(v, str)
else f"@{v.lang}"
if v.lang
else v.datatype
)
# v_str = v.n3() if isinstance(v, Literal) else v
self.collection.remove_relations(s, p, o, d)
102 changes: 63 additions & 39 deletions tripper/literal.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,58 +75,82 @@ def __new__(
lang: "Optional[str]" = None,
datatype: "Optional[Any]" = None,
):
# pylint: disable=too-many-branches
string = super().__new__(cls, value)
string.lang = None
string.datatype = None

# Get lang
if lang:
if datatype:
raise TypeError(
"A literal can only have one of `lang` or `datatype`."
)
string.lang = str(lang)
string.datatype = None
else:
string.lang = None
if datatype:
string.datatype = cls.datatypes.get(datatype, (datatype,))[0]
elif isinstance(value, str):
string.datatype = None
elif isinstance(value, bool):
string.datatype = XSD.boolean
elif isinstance(value, int):
string.datatype = XSD.integer
elif isinstance(value, float):
string.datatype = XSD.double
elif isinstance(value, (bytes, bytearray)):

# Get datatype
elif datatype in cls.datatypes:
string.datatype = cls.datatypes[datatype][0]
elif datatype:
# Create canonical representation of value for
# given datatype
val = None
for typ, names in cls.datatypes.items():
for name in names:
if name == datatype:
try:
val = typ(value)
break
except: # pylint: disable=bare-except
pass # nosec
if val:
break
if val is not None:
# Re-initialize the value anew, similarly to what is done in
# the first line of this method.
string = super().__new__(cls, value.hex())
string = super().__new__(cls, val)
string.lang = None
string.datatype = XSD.hexBinary
elif isinstance(value, datetime):
string.datatype = XSD.dateTime
# TODO:
# - XSD.base64Binary
# - XSD.byte, XSD.unsignedByte
else:
string.datatype = None

string.datatype = datatype

# Infer datatype from value
elif isinstance(value, str):
string.datatype = XSD.string
elif isinstance(value, bool):
string.datatype = XSD.boolean
elif isinstance(value, int):
string.datatype = XSD.integer
elif isinstance(value, float):
string.datatype = XSD.double
elif isinstance(value, (bytes, bytearray)):
# Re-initialize the value anew, similarly to what is done in
# the first line of this method.
string = super().__new__(cls, value.hex())
string.lang = None
string.datatype = XSD.hexBinary
elif isinstance(value, datetime):
string.datatype = XSD.dateTime
# TODO:
# - XSD.base64Binary
# - XSD.byte, XSD.unsignedByte
return string

# These two methods are commeted out for now because they cause
# the DLite example/mapping/mappingfunc.py example to fail.
#
# It seems that these methods cause the datatype be changed to
# an "h" in some relations added by the add_function() method.
def __hash__(self):
return hash((str(self), self.lang, self.datatype))

# def __hash__(self):
# return hash((str(self), self.lang, self.datatype))
def __eq__(self, other):
if not isinstance(other, Literal):
if isinstance(other, str) and self.lang:
return str(self) == other
other = Literal(other)
return (
str(self) == str(other)
and self.lang == other.lang
and self.datatype == other.datatype
)

# def __eq__(self, other):
# if isinstance(other, Literal):
# return (
# str(self) == str(other)
# and self.lang == other.lang
# and self.datatype == other.datatype
# )
# return str(self) == str(other)
def __ne__(self, other):
return not self.__eq__(other)

def __repr__(self) -> str:
lang = f", lang='{self.lang}'" if self.lang else ""
Expand All @@ -144,7 +168,7 @@ def to_python(self):
value = str(self)

if self.datatype == XSD.boolean:
value = False if self == "False" else bool(self)
value = False if str(self) == "False" else bool(self)
elif self.datatype in self.datatypes[int]:
value = int(self)
elif self.datatype in self.datatypes[float]:
Expand Down
Loading
Loading