Changeset - f13124540331
[Not reviewed]
default
0 1 2
drewp@bigasterisk.com - 20 months ago 2023-05-21 23:43:42
drewp@bigasterisk.com
factor out typedValue, add many tests, and fail to get it to work
3 files changed with 170 insertions and 21 deletions:
0 comments (0 inline, 0 general)
light9/newtypes.py
Show inline comments
 
@@ -10,45 +10,24 @@ Curve = NewType('Curve', URIRef)
 
OutputUri = NewType('OutputUri', URIRef)  # e.g. dmxA
 
DeviceUri = NewType('DeviceUri', URIRef)  # e.g. :aura2
 
DeviceClass = NewType('DeviceClass', URIRef)  # e.g. :Aura
 
DmxIndex = NewType('DmxIndex', int)  # 1..512
 
DmxMessageIndex = NewType('DmxMessageIndex', int)  # 0..511
 
DeviceAttr = NewType('DeviceAttr', URIRef)  # e.g. :rx
 
EffectClass = NewType('EffectClass', URIRef)  # e.g. effect:chase
 
EffectAttr = NewType('EffectAttr', URIRef)  # e.g. :chaseSpeed
 
NoteUri = NewType('NoteUri', URIRef)
 
OutputAttr = NewType('OutputAttr', URIRef)  # e.g. :xFine
 
OutputValue = NewType('OutputValue', int)  # byte in dmx message
 
Song = NewType('Song', URIRef)
 
UnixTime = NewType('UnixTime', float)
 

	
 
VT = TypeVar('VT', float, int, str)  # remove
 
HexColor = NewType('HexColor', str)
 
VTUnion = Union[float, int, HexColor]  # rename to ValueType
 
DeviceSetting = Tuple[DeviceUri, DeviceAttr,
 
                      # currently, floats and hex color strings
 
                      VTUnion]
 

	
 
# Alternate output range for a device. Instead of outputting 0.0 to
 
# 1.0, you can map that range into, say, 0.2 to 0.7
 
OutputRange = NewType('OutputRange', Tuple[float, float])
 

	
 
_ObjType = TypeVar('_ObjType')
 

	
 

	
 
def _isSubclass2(t1: Type, t2: Type) -> bool:
 
    """same as issubclass but t1 can be a NewType"""
 
    if hasattr(t1, '__supertype__'):
 
        t1 = t1.__supertype__
 
    return issubclass(t1, t2)
 

	
 

	
 
def typedValue(objType: Type[_ObjType], graph, subj, pred) -> _ObjType:
 
    """graph.value(subj, pred) with a given return type.
 
    If objType is not an rdflib.Node, we toPython() the value."""
 
    obj = graph.value(subj, pred)
 
    if obj is None:
 
        raise ValueError()
 
    conv = obj if _isSubclass2(objType, Node) else obj.toPython()
 
    if objType is float and isinstance(conv, decimal.Decimal):
 
        conv = float(conv)
 
    return cast(objType, conv)
 
\ No newline at end of file
light9/typedgraph.py
Show inline comments
 
new file 100644
 
import decimal
 
from types import UnionType
 
from typing import Type, TypeVar, cast, get_args
 

	
 
from rdfdb.syncedgraph.syncedgraph import SyncedGraph
 
from rdflib import Graph
 
from rdflib.term import Node
 

	
 
# todo: this ought to just require a suitable graph.value method
 
EitherGraph = Graph | SyncedGraph
 

	
 
_ObjType = TypeVar('_ObjType')
 

	
 

	
 
class ConversionError(ValueError):
 
    """graph had a value, but it does not safely convert to any of the requested types"""
 

	
 

	
 
def _typeIncludes(t1: Type, t2: Type) -> bool:
 
    """same as issubclass but t1 can be a NewType"""
 
    # if hasattr(t1, '__supertype__'):
 
    #     t1 = t1.__supertype__
 
    print(f'{isinstance(t1,  UnionType)=}')
 
    if isinstance(t1, UnionType):
 
        print(f" i see {t1} is union")
 
        return any(_typeIncludes(t, t2) for t in get_args(t1))
 
    # print('iss', t1, t2,     isinstance(t1,t2))
 
    # if t1 is float:
 
    #     return float in get_args(t2)
 
    return issubclass(t1, t2)
 

	
 

	
 
def typedValue(objType: Type[_ObjType], graph: EitherGraph, subj: Node, pred: Node) -> _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.
 
    """
 
    obj = graph.value(subj, pred)
 
    if obj is None:
 
        if type(None) in get_args(objType):
 
            return None
 
        raise ValueError(f'No obj for {subj=} {pred=}')
 
    conv = obj  #if _typeIncludes(objType, Node) else obj.toPython()
 
    # if objType is float and isinstance(conv, decimal.Decimal):
 
    #     conv = float(conv)
 
    return cast(objType, conv)
 
\ No newline at end of file
light9/typedgraph_test.py
Show inline comments
 
new file 100644
 
from typing import NewType, Optional, cast
 

	
 
import pytest
 
from rdflib import Graph, Literal, URIRef
 

	
 
from light9.mock_syncedgraph import MockSyncedGraph
 
from light9.namespaces import L9
 
from light9.typedgraph import ConversionError, typedValue
 

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

	
 
subj = L9['subj']
 

	
 

	
 
class TestTypedValueReturnsBasicTypes:
 

	
 
    def test_getsUri(self):
 
        assert typedValue(URIRef, g, subj, L9['uri']) == L9['c']
 

	
 
    def test_getsFloats(self):
 
        assert typedValue(float, g, subj, L9['float1']) == 0
 
        assert typedValue(float, g, subj, L9['float2']) == 0
 
        assert typedValue(float, g, subj, L9['float3']) == 10
 

	
 
    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 == 'todo'
 

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

	
 

	
 
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]
 
        assert typedValue(float | None, g, subj, L9['float1']) == 0
 
        assert typedValue(float | None, g, subj, L9['missing']) == None
 
        assert typedValue(str | float | None, g, subj, L9['missing']) == None
 

	
 
    def test_cantJustPassNone(self):
 
        with pytest.raises(TypeError):
 
            typedValue(None, g, subj, L9['float1'])  # type: ignore
 

	
 

	
 
class TestTypedValueConvertsToNewTypes:
 

	
 
    def test_castsUri(self):
 
        DeviceUri = NewType('DeviceUri', URIRef)
 
        tv = typedValue(DeviceUri, g, subj, L9['uri'])
 
        assert type(tv) == DeviceUri
 
        assert tv == DeviceUri(L9['c'])
 

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

	
 

	
 
class TestTypedValueAcceptsUnionTypes:
 

	
 
    def test_getsMemberTypeOfUnion(self):
 
        tv1 = typedValue(float | str, g, subj, L9['float1'])
 
        assert type(tv1) == float
 
        assert tv1 == 0.0
 

	
 
        tv2 = typedValue(float | str, g, subj, L9['color'])
 
        assert type(tv2) == str
 
        assert tv2 == '#ffffff'
 

	
 
    def test_failsIfNoUnionTypeMatches(self):
 
        with pytest.raises(ConversionError):
 
            typedValue(float | URIRef, g, subj, L9['color'])
 

	
 
    def test_combinesWithNone(self):
 
        assert typedValue(float | str | None, g, subj, L9['uri']) == L9['c']
 

	
 
    def test_combinedWithNewType(self):
 
        HexColor = NewType('HexColor', str)
 
        assert typedValue(float | HexColor, g, subj, L9['float1']) == 0
 
        assert typedValue(float | HexColor, g, subj, L9['color']) == HexColor('#ffffff')
 

	
 
    def test_whenOneIsUri(self):
 
        assert typedValue(str | URIRef, g, subj, L9['color']) == '#ffffff'
 
        assert typedValue(str | URIRef, g, subj, L9['uri']) == L9['c']
0 comments (0 inline, 0 general)