Changeset - d1946cb32121
[Not reviewed]
default
0 2 0
drewp@bigasterisk.com - 8 months ago 2024-05-28 22:34:21
drewp@bigasterisk.com
bool support
2 files changed with 6 insertions and 0 deletions:
0 comments (0 inline, 0 general)
src/light9/typedgraph.py
Show inline comments
 
@@ -23,48 +23,49 @@ def _expandUnion(t: Type) -> List[Type]:
 
def _typeIncludes(t1: Type, t2: Type) -> bool:
 
    """same as issubclass but t1 can be a NewType"""
 
    if t2 is None:
 
        t2 = type(None)
 
    if t1 == t2:
 
        return True
 

	
 
    if getattr(t1, '__supertype__', None) == t2:
 
        return True
 

	
 
    ts = _expandUnion(t1)
 
    if len(ts) > 1:
 
        return any(_typeIncludes(t, t2) for t in ts)
 

	
 
    return False
 

	
 

	
 
def _convLiteral(objType: Type[_ObjType], x: Literal) -> _ObjType:
 
    if _typeIncludes(objType, Literal):
 
        return cast(objType, x)
 

	
 
    for outType, dtypes in [
 
        (float, (XSD['integer'], XSD['double'], XSD['decimal'])),
 
        (int, (XSD['integer'],)),
 
        (bool, (XSD['boolean'],)), # todo tests
 
        (str, ()),
 
    ]:
 
        for t in _expandUnion(objType):
 
            if _typeIncludes(t, outType) and (not dtypes or x.datatype in dtypes):
 
                # e.g. user wants float and we have xsd:double
 
                return cast(objType, outType(x.toPython()))
 
    raise ConversionError
 

	
 

	
 
def typedValue(objType: Type[_ObjType], graph: EitherGraph, subj: Node, pred: URIRef) -> _ObjType:
 
    """graph.value(subj, pred) with a given return type.
 
    If objType is not an rdflib.Node, we toPython() the value.
 

	
 
    Allow objType to include None if you want a None return for not-found.
 
    """
 
    if objType is None:
 
        raise TypeError('must allow non-None result type')
 
    obj = graph.value(subj, pred)
 
    if obj is None:
 
        if _typeIncludes(objType, None):
 
            return cast(objType, None)
 
        raise ValueError(f'No obj for {subj=} {pred=}')
 

	
 
    ConvFrom: Type[Node] = type(obj)
src/light9/typedgraph_test.py
Show inline comments
 
from typing import NewType, Optional, cast
 

	
 
import pytest
 
from rdflib import BNode, Graph, Literal, URIRef
 
from rdflib.term import Node
 
from light9.mock_syncedgraph import MockSyncedGraph
 
from light9.namespaces import L9, XSD
 
from light9.typedgraph import ConversionError, _typeIncludes, typedValue
 

	
 
g = cast(
 
    Graph,
 
    MockSyncedGraph('''
 
    @prefix : <http://light9.bigasterisk.com/> .
 
    :subj
 
        :uri :c;
 
        :bnode [];
 
        # see https://w3c.github.io/N3/spec/#literals for syntaxes.
 
        :int 0;
 
        :float1 0.0;
 
        :float2 1.0e1;
 
        :float3 0.5;
 
        :boolt true;
 
        :boolf false;
 
        :color "#ffffff"^^:hexColor;
 
        :definitelyAString "hello" .
 
'''))
 

	
 
subj = L9['subj']
 

	
 

	
 
class TestTypeIncludes:
 

	
 
    def test_includesItself(self):
 
        assert _typeIncludes(str, str)
 

	
 
    def test_includesUnionMember(self):
 
        assert _typeIncludes(int | str, str)
 

	
 
    def test_notIncludes(self):
 
        assert not _typeIncludes(int | str, None)
 

	
 
    def test_explicitOptionalWorks(self):
 
        assert _typeIncludes(Optional[int], None)
 

	
 
    def test_3WayUnionWorks(self):
 
        assert _typeIncludes(int | str | float, int)
 

	
 
@@ -68,48 +70,51 @@ class TestTypedValueReturnsBasicTypes:
 

	
 
        assert typedValue(int, g, subj, L9['int']) == 0
 
        # These retrieve rdf floats that happen to equal
 
        # ints, but no one should be relying on that.
 
        with pytest.raises(ConversionError):
 
            typedValue(int, g, subj, L9['float1'])
 
        with pytest.raises(ConversionError):
 
            typedValue(int, g, subj, L9['float2'])
 
        with pytest.raises(ConversionError):
 
            typedValue(int, g, subj, L9['float3'])
 

	
 
    def test_getsString(self):
 
        tv = typedValue(str, g, subj, L9['color'])
 
        assert tv == '#ffffff'
 

	
 
    def test_getsLiteral(self):
 
        tv = typedValue(Literal, g, subj, L9['float2'])
 
        assert type(tv) == Literal
 
        assert tv.datatype == XSD['double']
 

	
 
        tv = typedValue(Literal, g, subj, L9['color'])
 
        assert type(tv) == Literal
 
        assert tv.datatype == L9['hexColor']
 

	
 
    def test_getsBoolean(self):
 
        assert typedValue(bool, g, subj, L9['boolt'])
 
        assert not typedValue(bool, g, subj, L9['boolf'])
 

	
 
class TestTypedValueDoesntDoInappropriateUriStringConversions:
 

	
 
    def test_noUriToString(self):
 
        with pytest.raises(ConversionError):
 
            typedValue(str, g, subj, L9['uri'])
 

	
 
    def test_noUriToLiteral(self):
 
        with pytest.raises(ConversionError):
 
            typedValue(Literal, g, subj, L9['uri'])
 

	
 
    def test_noStringToUri(self):
 
        with pytest.raises(ConversionError):
 
            typedValue(URIRef, g, subj, L9['definitelyAString'])
 

	
 

	
 
class TestTypedValueOnMissingValues:
 

	
 
    def test_errorsOnMissingValue(self):
 
        with pytest.raises(ValueError):
 
            typedValue(float, g, subj, L9['missing'])
 

	
 
    def test_returnsNoneForMissingValueIfCallerPermits(self):
 
        assert (float | None) == Optional[float]
0 comments (0 inline, 0 general)